diff --git a/.gitattributes b/.gitattributes index 1d338e0f8..6ee29d818 100644 --- a/.gitattributes +++ b/.gitattributes @@ -14,6 +14,7 @@ *.rs text eol=lf *.go text eol=lf *.cs text eol=lf +*.java text eol=lf *.tsp text eol=lf *.astro text eol=lf *.css text eol=lf @@ -36,6 +37,9 @@ # --- Project files --- *.csproj text eol=lf *.sln text eol=lf +*.gradle text eol=lf +*.kts text eol=lf +*.properties text eol=lf *.ps1 text eol=lf *.bat text eol=lf @@ -51,6 +55,7 @@ Dockerfile text eol=lf # --- Truly binary files --- keep as-is *.png binary *.exe binary +*.jar binary *.svg text eol=lf # --- Lock files: text but don't diff --- diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 1e0bcfd22..949f8a3cd 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -28,11 +28,19 @@ jobs: matrix: include: - language: actions + build-mode: none - language: csharp + build-mode: autobuild - language: go + build-mode: autobuild + - language: java-kotlin + build-mode: none - language: javascript-typescript + build-mode: none - language: python + build-mode: none - language: rust + build-mode: none steps: - name: Checkout repository @@ -42,10 +50,7 @@ jobs: uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} - - - name: Autobuild - if: matrix.language == 'csharp' || matrix.language == 'go' || matrix.language == 'rust' - uses: github/codeql-action/autobuild@v4 + build-mode: ${{ matrix.build-mode }} - name: Perform CodeQL analysis uses: github/codeql-action/analyze@v4 diff --git a/.github/workflows/prompty-java-check.yml b/.github/workflows/prompty-java-check.yml new file mode 100644 index 000000000..3b1258313 --- /dev/null +++ b/.github/workflows/prompty-java-check.yml @@ -0,0 +1,111 @@ +name: prompty Java build and test +on: + pull_request: + paths: + - 'runtime/java/**' + - 'schema/scripts/clean-java-output*' + - 'schema/scripts/normalize-java-output*' + - '.github/workflows/prompty-java-check.yml' + + workflow_call: + +jobs: + generation-scripts: + name: test Java generation scripts + runs-on: ubuntu-latest + permissions: + contents: read + defaults: + run: + working-directory: schema + steps: + - uses: actions/checkout@v5 + + - name: Set up Node + uses: actions/setup-node@v6 + with: + node-version: '22' + + # Guards the generated-output cleaner. Both of its failure modes are + # silent: destroying the hand-written `@method` implementations kept in + # extension seams, or leaving stale generated files behind. + - name: Test generation scripts + run: npm run test:scripts + + generation-drift: + name: verify Java generation is reproducible + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v5 + + - name: Set up Node + uses: actions/setup-node@v6 + with: + node-version: '22' + + - name: Install schema toolchain + working-directory: schema + run: npm ci + + # Regenerating exercises every guard in normalize-java-output.mjs. Those + # guards fail the build when a rewrite stops matching emitter output, + # which is how an emitter upgrade that silently changes Java codegen is + # caught. Running the emitter here is the only place they execute. + - name: Regenerate + working-directory: schema + run: npm run generate + + # The committed Java tree must be exactly what the pinned emitter plus the + # normalization shim produce. A diff means either a hand edit to generated + # code or a stale checked-in tree. + - name: Verify committed Java output matches generated output + run: | + if ! git diff --quiet -- runtime/java; then + echo "::error::Committed Java output differs from freshly generated output." + git --no-pager diff --stat -- runtime/java + git --no-pager diff -- runtime/java | head -200 + exit 1 + fi + echo "Committed Java output matches generated output." + + java-tests: + name: test Java on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + permissions: + contents: read + defaults: + run: + working-directory: runtime/java + steps: + - uses: actions/checkout@v5 + + # The Gradle toolchain pins Java 21, so this is the version that actually + # compiles and runs the tests on every platform. + - name: Set up JDK 21 + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: '21' + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v6 + + # Live provider tests are tagged "live" and are excluded unless + # -PliveTests is passed, so this is the offline suite. It needs no + # secrets and is therefore safe on pull requests from forks. + - name: Build and test + run: ./gradlew build --console=plain + + - name: Upload test reports + if: always() + uses: actions/upload-artifact@v7 + with: + name: java-test-reports-${{ matrix.os }} + path: runtime/java/*/build/reports/tests/test + if-no-files-found: ignore diff --git a/runtime/java/.env.example b/runtime/java/.env.example new file mode 100644 index 000000000..f199dbdb5 --- /dev/null +++ b/runtime/java/.env.example @@ -0,0 +1,21 @@ +# OpenAI +OPENAI_API_KEY= +OPENAI_MODEL=gpt-4o-mini +OPENAI_EMBEDDING_MODEL=text-embedding-3-small +OPENAI_IMAGE_MODEL=dall-e-2 + +# Azure OpenAI / Foundry +AZURE_OPENAI_API_KEY= +AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com +AZURE_OPENAI_CHAT_DEPLOYMENT=gpt-4o-mini +AZURE_OPENAI_EMBEDDING_DEPLOYMENT=text-embedding-3-small + +# Azure AI Foundry project (agent / model discovery) +FOUNDRY_PROJECT_ENDPOINT= + +# Azure Entra ID (for keyless auth tests) +AZURE_TENANT_ID= + +# Anthropic +ANTHROPIC_API_KEY= +ANTHROPIC_MODEL=claude-sonnet-4-20250514 diff --git a/runtime/java/.gitignore b/runtime/java/.gitignore new file mode 100644 index 000000000..8e2f09a80 --- /dev/null +++ b/runtime/java/.gitignore @@ -0,0 +1,11 @@ +# Gradle +.gradle/ +build/ + +# Local credentials for live provider tests — never commit. +.env + +# IDE +.idea/ +*.iml +.vscode/ diff --git a/runtime/java/README.md b/runtime/java/README.md new file mode 100644 index 000000000..2d691db36 --- /dev/null +++ b/runtime/java/README.md @@ -0,0 +1,78 @@ +# Prompty for Java + +The Java implementation of the Prompty runtime. + +Prompty is a markdown-based asset format (`.prompty`) for LLM prompts: YAML +frontmatter describes the model, inputs, outputs, tools and template +configuration, and the markdown body becomes the prompt instructions. The +runtime loads, renders, parses, executes and processes those assets. + +> **Status:** in development. The generated model layer and its example suites +> are complete; the loader, renderers, parser, pipeline and providers are being +> ported from the Rust reference implementation. + +## Layout + +| Module | Contents | +| ------------------- | --------------------------------------------------------------- | +| `prompty` | Canonical generated model + loader, renderers, parser, pipeline | +| `prompty-openai` | OpenAI provider (executor + processor) | +| `prompty-anthropic` | Anthropic provider (executor + processor) | +| `prompty-foundry` | Azure AI Foundry provider (executor + processor) | + +## The model layer is generated — do not edit it + +`prompty/src/main/java/com/microsoft/prompty/model/` and +`prompty/src/test/java/com/microsoft/prompty/model/` are emitted from the +TypeSpec definition in [`schema/`](../../schema) by the Typra emitter. They are +the single canonical model layer for this runtime; there is no hand-written +duplicate. Regenerate with: + +```bash +cd schema +npm install # first time only +npm run generate +``` + +`npm run generate` runs `tsp compile` followed by +`schema/scripts/normalize-typra-output.mjs`, which applies a deterministic, +idempotent normalization pass to the emitted Java (see +`schema/scripts/normalize-java-output.mjs`). That shim exists only because the +Typra Java backend currently emits source that does not compile and that +diverges from the C#/Rust/Go/Python backends; every defect it works around is +documented at the top of the shim and has been reported upstream. The shim is +part of the generation pipeline, not a manual edit — running the pipeline twice +from a clean tree produces byte-identical output. + +The emitter also produces example-driven suites next to the model. They are +package-private, so `GeneratedExamplesTest` discovers the compiled +`*GeneratedTest` classes by reflection and runs each as a named dynamic test, +which keeps the suite in step with the schema without a checked-in registry. +`ModelNormalizationTest` covers the behaviour the shim adds on top of the +emitter output, which the generated examples do not exercise. + +## Building + +The Gradle wrapper provisions Gradle itself; a JDK is required to run it. The +build pins a Java 21 toolchain, so a newer JDK works as long as Gradle can +locate or provision a 21 toolchain. + +```bash +cd runtime/java +./gradlew build # compile + unit tests +./gradlew :prompty:test # unit tests for the core module only +``` + +## Live provider tests + +Tests tagged `live` call real providers and are excluded from the default `test` +task. They read credentials from the process environment; the test support code +also reads `runtime/java/.env` when present so the file does not have to be +exported manually. + +```bash +cp .env.example .env # then fill in credentials +./gradlew test -PliveTests +``` + +`.env` is git-ignored and must never be committed. diff --git a/runtime/java/build.gradle.kts b/runtime/java/build.gradle.kts new file mode 100644 index 000000000..78fef8933 --- /dev/null +++ b/runtime/java/build.gradle.kts @@ -0,0 +1,48 @@ +plugins { + id("java-library") +} + +allprojects { + group = "com.microsoft.prompty" + version = "2.0.0-beta.4" +} + +subprojects { + apply(plugin = "java-library") + + repositories { + mavenCentral() + } + + extensions.configure { + toolchain { + languageVersion.set(JavaLanguageVersion.of(21)) + } + withSourcesJar() + } + + tasks.withType().configureEach { + options.encoding = "UTF-8" + // Generated model sources are emitted by Typra and are not warning-clean by + // design; keep the build strict for hand-written code only. + options.compilerArgs.addAll(listOf("-Xlint:all", "-Xlint:-serial", "-Xlint:-this-escape")) + } + + tasks.withType().configureEach { + // Live provider tests call real endpoints; they are opt-in via -PliveTests. + val liveTests = providers.gradleProperty("liveTests").isPresent + useJUnitPlatform { + if (!liveTests) { + excludeTags("live") + } + } + testLogging { + events("failed") + exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL + } + } + + tasks.withType().configureEach { + (options as StandardJavadocDocletOptions).addStringOption("Xdoclint:none", "-quiet") + } +} diff --git a/runtime/java/gradle/wrapper/gradle-wrapper.jar b/runtime/java/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..b1b8ef56b Binary files /dev/null and b/runtime/java/gradle/wrapper/gradle-wrapper.jar differ diff --git a/runtime/java/gradle/wrapper/gradle-wrapper.properties b/runtime/java/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..a9db11550 --- /dev/null +++ b/runtime/java/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,9 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip +networkTimeout=10000 +retries=0 +retryBackOffMs=500 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/runtime/java/gradlew b/runtime/java/gradlew new file mode 100755 index 000000000..249efbb03 --- /dev/null +++ b/runtime/java/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# gradlew start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh gradlew +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/runtime/java/gradlew.bat b/runtime/java/gradlew.bat new file mode 100644 index 000000000..a51ec4f58 --- /dev/null +++ b/runtime/java/gradlew.bat @@ -0,0 +1,82 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem gradlew startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:execute +@rem Setup the command line + + + +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel + +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/runtime/java/prompty-anthropic/build.gradle.kts b/runtime/java/prompty-anthropic/build.gradle.kts new file mode 100644 index 000000000..fbd3a0d9d --- /dev/null +++ b/runtime/java/prompty-anthropic/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + id("java-library") +} + +dependencies { + api(project(":prompty")) + + testImplementation(platform("org.junit:junit-bom:5.11.4")) + testImplementation("org.junit.jupiter:junit-jupiter") + testImplementation(testFixtures(project(":prompty"))) + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} diff --git a/runtime/java/prompty-anthropic/src/main/java/com/microsoft/prompty/anthropic/AnthropicExecutor.java b/runtime/java/prompty-anthropic/src/main/java/com/microsoft/prompty/anthropic/AnthropicExecutor.java new file mode 100644 index 000000000..9abf1bf38 --- /dev/null +++ b/runtime/java/prompty-anthropic/src/main/java/com/microsoft/prompty/anthropic/AnthropicExecutor.java @@ -0,0 +1,193 @@ +package com.microsoft.prompty.anthropic; + +import com.microsoft.prompty.CancellationToken; +import com.microsoft.prompty.Connections; +import com.microsoft.prompty.Environment; +import com.microsoft.prompty.Executor; +import com.microsoft.prompty.Http; +import com.microsoft.prompty.InvokerException; +import com.microsoft.prompty.Streams; +import com.microsoft.prompty.model.AnonymousConnection; +import com.microsoft.prompty.model.ApiKeyConnection; +import com.microsoft.prompty.model.Connection; +import com.microsoft.prompty.model.FoundryConnection; +import com.microsoft.prompty.model.Message; +import com.microsoft.prompty.model.ModelInvocationRequest; +import com.microsoft.prompty.model.OAuthConnection; +import com.microsoft.prompty.model.Prompty; +import com.microsoft.prompty.model.RemoteConnection; +import com.microsoft.prompty.model.ToolCall; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * Sends requests to the Anthropic Messages API. + * + *

Anthropic offers one endpoint, so unlike the OpenAI executor there is nothing to dispatch on + * beyond rejecting API types the provider does not have. Authentication is an {@code x-api-key} + * header rather than a bearer token, and every request carries an explicit API version — Anthropic + * pins wire-format changes to that header, so sending it is what keeps this code correct as the API + * moves on. + */ +public class AnthropicExecutor implements Executor { + + private static final String DEFAULT_ENDPOINT = "https://api.anthropic.com"; + + /** The provider name used in error messages. */ + protected String providerName() { + return "anthropic"; + } + + @Override + public Object execute(Prompty agent, List messages) { + Map body = buildArgs(agent, messages); + return Http.postJson(providerName(), buildUrl(agent), authHeaders(agent), body); + } + + @Override + public Object executeWithContext( + Prompty agent, ModelInvocationRequest request, CancellationToken cancellation) { + cancellation.throwIfCancelled( + "execution cancelled before " + providerName() + " provider invocation"); + Object result = execute(agent, messagesOf(request)); + // Checked again after the call because a cancellation that arrives mid-flight still means the + // caller no longer wants the result, even though the provider has already acted on it. + cancellation.throwIfCancelled( + "execution cancelled during " + providerName() + " provider invocation"); + return result; + } + + @Override + public Iterator executeStream(Prompty agent, List messages) { + Map body = buildArgs(agent, messages); + Wire.enableStreaming(body); + return Http.postSse(providerName(), buildUrl(agent), authHeaders(agent), body); + } + + @Override + public Iterator executeStreamWithContext( + Prompty agent, ModelInvocationRequest request, CancellationToken cancellation) { + cancellation.throwIfCancelled( + "streaming execution cancelled before " + providerName() + " provider invocation"); + return Streams.cancellable(executeStream(agent, messagesOf(request)), cancellation); + } + + @Override + public List formatToolMessages( + Object rawResponse, List toolCalls, List toolResults, String textContent) { + return Wire.formatToolMessages(rawResponse, toolCalls, toolResults); + } + + @Override + public List formatStreamToolMessages( + List rawChunks, + List toolCalls, + List toolResults, + String textContent) { + return Wire.formatStreamToolMessages(rawChunks, toolCalls, toolResults, textContent); + } + + /** Build the request body without sending it. */ + public Map buildArgs(Prompty agent, List messages) { + String apiType = apiType(agent); + if (!"chat".equals(apiType) && !"agent".equals(apiType)) { + throw InvokerException.execute( + "Anthropic only supports apiType 'chat', got: " + apiType); + } + return Wire.buildChatArgs(agent, messages); + } + + private static String apiType(Prompty agent) { + if (agent == null || agent.model == null) { + return "chat"; + } + String apiType = agent.model.apiType; + return apiType == null || apiType.isEmpty() ? "chat" : apiType; + } + + // -------------------------------------------------------- connection + + /** + * The URL the Messages API lives at for this prompt's connection. + * + *

Deliberately more forgiving than the Rust reference's Anthropic executor, which reads only + * {@code connection.endpoint} and always appends {@code /v1/messages}. Honouring {@code + * ANTHROPIC_BASE_URL} matches the official Anthropic SDKs and the way every other provider here + * resolves a base URL, and collapsing a duplicate {@code /v1} matches what Rust's own OpenAI + * executor does — a gateway base is routinely written with the version already on it. + */ + protected String buildUrl(Prompty agent) { + String endpoint = endpointOf(connection(agent)); + if (endpoint == null || endpoint.isEmpty()) { + endpoint = + Environment.lookup("ANTHROPIC_BASE_URL").filter(v -> !v.isEmpty()).orElse(DEFAULT_ENDPOINT); + } + String base = Connections.trimTrailingSlashes(endpoint); + // A proxy base is commonly written with the version already on it; appending another would + // produce /v1/v1/messages. + return base.endsWith("/v1") ? base + "/messages" : base + "/v1/messages"; + } + + /** The headers that authenticate and version the request. */ + protected Map authHeaders(Prompty agent) { + return Map.of( + "x-api-key", apiKey(agent), + "anthropic-version", Wire.ANTHROPIC_VERSION); + } + + /** Resolve the API key from the prompt's connection, falling back to the environment. */ + protected String apiKey(Prompty agent) { + Connection connection = connection(agent); + if (connection instanceof ApiKeyConnection apiKey + && apiKey.apiKey != null + && !apiKey.apiKey.isEmpty()) { + return apiKey.apiKey; + } + return Environment.lookup("ANTHROPIC_API_KEY") + .filter(key -> !key.isEmpty()) + .orElseThrow( + () -> + InvokerException.execute( + "No API key found. Set ANTHROPIC_API_KEY or configure" + + " model.connection.apiKey")); + } + + /** The prompt's connection with any reference followed to the concrete one. */ + protected static Connection connection(Prompty agent) { + Connection connection = agent == null || agent.model == null ? null : agent.model.connection; + return connection == null ? null : Connections.resolve(connection); + } + + /** + * The endpoint a connection carries. + * + *

{@code endpoint} is declared per connection kind rather than on the base type, so each kind + * that has one is asked directly. + */ + protected static String endpointOf(Connection connection) { + if (connection instanceof ApiKeyConnection apiKey) { + return apiKey.endpoint; + } + if (connection instanceof AnonymousConnection anonymous) { + return anonymous.endpoint; + } + if (connection instanceof RemoteConnection remote) { + return remote.endpoint; + } + if (connection instanceof OAuthConnection oauth) { + return oauth.endpoint; + } + if (connection instanceof FoundryConnection foundry) { + return foundry.endpoint; + } + return null; + } + + private static List messagesOf(ModelInvocationRequest request) { + if (request == null || request.context == null || request.context.messages == null) { + return List.of(); + } + return request.context.messages; + } +} diff --git a/runtime/java/prompty-anthropic/src/main/java/com/microsoft/prompty/anthropic/AnthropicExtension.java b/runtime/java/prompty-anthropic/src/main/java/com/microsoft/prompty/anthropic/AnthropicExtension.java new file mode 100644 index 000000000..f16f1d75b --- /dev/null +++ b/runtime/java/prompty-anthropic/src/main/java/com/microsoft/prompty/anthropic/AnthropicExtension.java @@ -0,0 +1,19 @@ +package com.microsoft.prompty.anthropic; + +import com.microsoft.prompty.PromptyExtension; + +/** + * Registers the Anthropic provider. + * + *

Discovered through {@code ServiceLoader}, so putting this module on the classpath is all it + * takes for {@code provider: anthropic} prompts to run — no registration call in application code. + */ +public final class AnthropicExtension implements PromptyExtension { + + @Override + public void register(Registrar registrar) { + registrar + .executor("anthropic", new AnthropicExecutor()) + .processor("anthropic", new AnthropicProcessor()); + } +} diff --git a/runtime/java/prompty-anthropic/src/main/java/com/microsoft/prompty/anthropic/AnthropicModelLister.java b/runtime/java/prompty-anthropic/src/main/java/com/microsoft/prompty/anthropic/AnthropicModelLister.java new file mode 100644 index 000000000..4a1aa0906 --- /dev/null +++ b/runtime/java/prompty-anthropic/src/main/java/com/microsoft/prompty/anthropic/AnthropicModelLister.java @@ -0,0 +1,159 @@ +package com.microsoft.prompty.anthropic; + +import com.microsoft.prompty.Discovery; +import com.microsoft.prompty.Environment; +import com.microsoft.prompty.Http; +import com.microsoft.prompty.InvokerException; +import com.microsoft.prompty.model.ModelInfo; +import com.microsoft.prompty.model.ModelLister; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** Lists the models an Anthropic connection can reach. */ +public final class AnthropicModelLister implements ModelLister { + + private static final String PROVIDER = "anthropic"; + + /** The API version header, matching the executor. */ + private static final String VERSION = "2023-06-01"; + + /** How many entries to ask for per page; the API caps this. */ + private static final int PAGE_SIZE = 100; + + /** + * A page cursor can only follow a page, so a run of pages is bounded by how many the provider is + * willing to serve. This caps it independently, so a provider that kept reporting more — or a + * cursor that stopped advancing — cannot spin forever. + */ + private static final int MAX_PAGES = 100; + + /** + * Map one raw {@code /v1/models} entry onto the provider-neutral contract. + * + *

This is the only place the Anthropic listing wire format is interpreted, and the shared + * discovery vectors exercise it directly so every runtime agrees on the result. + * + *

Anthropic reports capabilities itself, so the shared dataset only fills what a given entry + * omitted. The owner is not on the wire at all — every model on this endpoint is Anthropic's — so + * it is supplied here to keep the field populated the way other providers populate it. + */ + public static ModelInfo modelInfoFromWire(Object raw) { + ModelInfo info = new ModelInfo(); + if (!(raw instanceof Map map)) { + return info; + } + info.id = map.get("id") instanceof String id ? id : ""; + info.displayName = map.get("display_name") instanceof String name ? name : null; + info.ownedBy = PROVIDER; + info.contextWindow = map.get("context_length") instanceof Number n ? n.intValue() : null; + info.inputModalities = strings(map.get("input_modalities")); + info.outputModalities = strings(map.get("output_modalities")); + info.additionalProperties = copy(map); + Discovery.enrich(PROVIDER, info); + return info; + } + + /** Walk every page of {@code GET /v1/models} and map the entries. */ + @Override + public List listModels(Object connection) { + Map config = connection instanceof Map map ? map : Map.of(); + requireKeyConnection(config); + + String base = modelsUrl(config); + Map headers = + Map.of("x-api-key", apiKey(config), "anthropic-version", VERSION); + + List models = new ArrayList<>(); + String after = null; + for (int page = 0; page < MAX_PAGES; page++) { + String url = base + "?limit=" + PAGE_SIZE; + if (after != null) { + url += "&after_id=" + URLEncoder.encode(after, StandardCharsets.UTF_8); + } + + Object body = Http.getJson(PROVIDER, url, headers); + if (!(body instanceof Map map)) { + break; + } + if (map.get("data") instanceof Iterable data) { + for (Object entry : data) { + models.add(modelInfoFromWire(entry)); + } + } + if (!Boolean.TRUE.equals(map.get("has_more"))) { + break; + } + // Without a cursor there is no way to ask for the next page, so stop rather than re-request + // the one just read. + if (!(map.get("last_id") instanceof String cursor) || cursor.isEmpty()) { + break; + } + after = cursor; + } + return models; + } + + static String modelsUrl(Map connection) { + String endpoint = text(connection.get("endpoint")); + if (endpoint.isEmpty()) { + endpoint = "https://api.anthropic.com"; + } + String base = endpoint; + while (base.endsWith("/")) { + base = base.substring(0, base.length() - 1); + } + return base + "/v1/models"; + } + + static String apiKey(Map connection) { + String key = text(connection.get("apiKey")); + if (key.isEmpty()) { + key = text(connection.get("api_key")); + } + if (key.isEmpty()) { + key = Environment.lookup("ANTHROPIC_API_KEY").orElse(""); + } + if (key.isEmpty()) { + throw InvokerException.execute( + "No API key found. Set ANTHROPIC_API_KEY or configure connection.apiKey"); + } + return key; + } + + private static void requireKeyConnection(Map connection) { + String kind = text(connection.get("kind")); + if (!"key".equals(kind)) { + throw InvokerException.execute( + "Connection kind '" + kind + "' is not supported for Anthropic model listing. Use 'key'."); + } + } + + private static List strings(Object value) { + if (!(value instanceof Iterable items)) { + return null; + } + List result = new ArrayList<>(); + for (Object item : items) { + if (item instanceof String text) { + result.add(text); + } + } + return result; + } + + private static String text(Object value) { + return value instanceof String s ? s : ""; + } + + private static Map copy(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } +} diff --git a/runtime/java/prompty-anthropic/src/main/java/com/microsoft/prompty/anthropic/AnthropicProcessor.java b/runtime/java/prompty-anthropic/src/main/java/com/microsoft/prompty/anthropic/AnthropicProcessor.java new file mode 100644 index 000000000..c53f22184 --- /dev/null +++ b/runtime/java/prompty-anthropic/src/main/java/com/microsoft/prompty/anthropic/AnthropicProcessor.java @@ -0,0 +1,368 @@ +package com.microsoft.prompty.anthropic; + +import com.microsoft.prompty.InvokerException; +import com.microsoft.prompty.Messages; +import com.microsoft.prompty.Processor; +import com.microsoft.prompty.Streams; +import com.microsoft.prompty.model.ErrorChunk; +import com.microsoft.prompty.model.InvocationContextPortability; +import com.microsoft.prompty.model.InvocationContextState; +import com.microsoft.prompty.model.InvocationUsage; +import com.microsoft.prompty.model.Message; +import com.microsoft.prompty.model.ModelInvocationRequest; +import com.microsoft.prompty.model.ModelInvocationResponse; +import com.microsoft.prompty.model.ModelToolRequest; +import com.microsoft.prompty.model.Prompty; +import com.microsoft.prompty.model.StreamChunk; +import com.microsoft.prompty.model.TextChunk; +import com.microsoft.prompty.model.ThinkingChunk; +import com.microsoft.prompty.model.ToolCall; +import com.microsoft.prompty.model.ToolChunk; +import com.microsoft.prompty.model.TypraJson; +import com.microsoft.prompty.model.UsageChunk; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.TreeMap; + +/** + * Interprets Anthropic Messages API responses. + * + *

An Anthropic response is a list of typed content blocks rather than a single message, so + * "what did the model say" is a question about which blocks are present. Tool use wins over text: + * when the model asks to call something, the text alongside it is commentary, and returning it as + * the result would let a caller act on an answer the model had not finished forming. + */ +public final class AnthropicProcessor implements Processor { + + @Override + public Object process(Prompty agent, Object response) { + return processResponse(agent, response); + } + + @Override + public Iterator processStream(Prompty agent, Iterator response) { + return new StreamProcessor(response); + } + + @Override + public ModelInvocationResponse processWithContext( + Prompty agent, Object response, ModelInvocationRequest request) { + Object output = processResponse(agent, response); + List calls = extractToolCalls(response); + + List toolRequests = new ArrayList<>(); + for (ToolCall call : calls) { + ModelToolRequest tool = new ModelToolRequest(); + tool.id = call.id; + tool.name = call.name; + tool.arguments = decodeArguments(call.arguments); + toolRequests.add(tool); + } + + ModelInvocationResponse result = new ModelInvocationResponse(); + result.output = toolRequests.isEmpty() ? output : null; + result.assistantMessages = assistantMessages(response); + result.toolRequests = toolRequests; + // The Messages API is stateless: nothing on the provider side survives the response, so the + // whole context travels with the next request and there is no handle worth pretending to hold. + result.nextContextState = portableState(); + return result; + } + + @Override + public ModelInvocationResponse processRawWithContext( + Prompty agent, Object response, ModelInvocationRequest request) { + ModelInvocationResponse result = new ModelInvocationResponse(); + result.output = response; + result.assistantMessages = assistantMessages(response); + result.toolRequests = new ArrayList<>(); + result.nextContextState = portableState(); + return result; + } + + private static InvocationContextState portableState() { + InvocationContextState state = new InvocationContextState(); + state.portability = InvocationContextPortability.PORTABLE; + state.delegatedState = new ArrayList<>(); + return state; + } + + // -------------------------------------------------------- single response + + /** Extract the usable result from a raw Messages API response. */ + public static Object processResponse(Prompty agent, Object response) { + List content = contentBlocks(response); + if (content == null) { + throw InvokerException.process("Invalid Anthropic response: missing 'content' array"); + } + + List toolCalls = toolCallsIn(content); + if (!toolCalls.isEmpty()) { + List encoded = new ArrayList<>(); + for (ToolCall call : toolCalls) { + Map entry = new LinkedHashMap<>(); + entry.put("id", call.id); + entry.put("name", call.name); + entry.put("arguments", call.arguments); + encoded.add(entry); + } + return encoded; + } + + String text = Wire.joinText(content); + + if (agent != null && agent.outputs != null && !agent.outputs.isEmpty()) { + // Structured output is a request, not a guarantee. A model that answers in prose has still + // answered, so an undecodable reply is returned as written rather than raised as a failure. + try { + return TypraJson.parse(text); + } catch (RuntimeException e) { + return text; + } + } + + return text; + } + + /** Collect the tool calls a response is asking for. */ + public static List extractToolCalls(Object response) { + List content = contentBlocks(response); + return content == null ? new ArrayList<>() : toolCallsIn(content); + } + + private static List contentBlocks(Object response) { + Object content = Streams.pointer(response, "content"); + return content instanceof List list ? list : null; + } + + private static List toolCallsIn(List content) { + List calls = new ArrayList<>(); + for (Object block : content) { + if (!"tool_use".equals(Streams.pointer(block, "type"))) { + continue; + } + ToolCall call = new ToolCall(); + call.id = stringOrEmpty(Streams.pointer(block, "id")); + call.name = stringOrEmpty(Streams.pointer(block, "name")); + Object input = Streams.pointer(block, "input"); + call.arguments = input == null ? "" : TypraJson.stringify(input); + calls.add(call); + } + return calls; + } + + /** + * The assistant turn this response represents. + * + *

The raw blocks ride along in metadata because Anthropic expects an assistant turn replayed + * exactly — thinking blocks carry signatures that cannot be reconstructed from their text. + */ + private static List assistantMessages(Object response) { + List content = contentBlocks(response); + Message assistant = Messages.assistant(content == null ? "" : Wire.joinText(content)); + if (content != null && !content.isEmpty()) { + // Deep-copied for the same reason as in Wire.formatToolMessages: this outlives the response. + Messages.metadata(assistant).put("content", Streams.deepCopy(content)); + } + List messages = new ArrayList<>(); + messages.add(assistant); + return messages; + } + + private static Object decodeArguments(String arguments) { + if (arguments == null || arguments.isEmpty()) { + return arguments; + } + try { + return TypraJson.parse(arguments); + } catch (RuntimeException e) { + // The turn engine can still hand the raw string to a tool that knows what to do with it. + return arguments; + } + } + + private static String stringOrEmpty(Object value) { + return value instanceof String text ? text : ""; + } + + // -------------------------------------------------------- streaming + + /** + * Turns Anthropic's SSE event stream into typed chunks. + * + *

Text and thinking are forwarded the moment they arrive, because that is the point of + * streaming. Tool calls are not: their arguments arrive as JSON fragments across many events, and + * a half-parsed argument object is not something a caller can act on — so they are accumulated and + * emitted once the stream ends. Usage comes last of all, since Anthropic reports input tokens at + * the start and output tokens at the end, and only their sum is meaningful. + */ + private static final class StreamProcessor + implements Iterator, java.io.Closeable { + + private final Iterator source; + /** Accumulating tool calls keyed by content-block index, which is how deltas identify them. */ + private final Map partialCalls = new TreeMap<>(); + + private final List pending = new ArrayList<>(); + private long inputTokens; + private long outputTokens; + private boolean hasUsage; + private boolean drained; + private boolean finished; + + StreamProcessor(Iterator source) { + this.source = source; + } + + @Override + public boolean hasNext() { + advance(); + return !pending.isEmpty(); + } + + @Override + public StreamChunk next() { + advance(); + if (pending.isEmpty()) { + throw new NoSuchElementException("stream exhausted"); + } + return pending.remove(0); + } + + /** Pull events until something is ready to hand out, or the stream is genuinely over. */ + private void advance() { + while (pending.isEmpty() && !finished) { + if (drained) { + emitTerminal(); + finished = true; + return; + } + if (!source.hasNext()) { + drained = true; + continue; + } + consume(source.next()); + } + } + + private void consume(Object event) { + String type = stringOrEmpty(Streams.pointer(event, "type")); + switch (type) { + case "message_start" -> { + Object usage = Streams.pointer(event, "message", "usage"); + if (usage != null) { + inputTokens = longAt(usage, "input_tokens"); + hasUsage = true; + } + } + case "message_delta" -> { + Object usage = Streams.pointer(event, "usage"); + if (usage != null) { + outputTokens = longAt(usage, "output_tokens"); + hasUsage = true; + } + } + case "content_block_start" -> { + Object block = Streams.pointer(event, "content_block"); + if ("tool_use".equals(Streams.pointer(block, "type"))) { + ToolCall call = new ToolCall(); + call.id = stringOrEmpty(Streams.pointer(block, "id")); + call.name = stringOrEmpty(Streams.pointer(block, "name")); + call.arguments = ""; + partialCalls.put(indexOf(event), call); + } + } + case "content_block_delta" -> consumeDelta(event); + case "error" -> { + // Deliberately unlike the Rust reference, which has no arm for a top-level `error` event + // and lets its catch-all skip it. Anthropic emits these mid-stream for overload and + // rate-limit conditions, so ignoring one hands the caller a silently truncated answer + // with no indication that anything went wrong. + String message = stringOrEmpty(Streams.pointer(event, "error", "message")); + ErrorChunk error = new ErrorChunk(); + error.message = message.isEmpty() ? "Anthropic stream reported an error" : message; + pending.add(error); + // Nothing after an error is trustworthy, so the connection is released rather than read. + drained = true; + finished = true; + close(); + } + default -> {} + } + } + + private void consumeDelta(Object event) { + Object delta = Streams.pointer(event, "delta"); + if (delta == null) { + return; + } + switch (stringOrEmpty(Streams.pointer(delta, "type"))) { + case "text_delta" -> { + String text = stringOrEmpty(Streams.pointer(delta, "text")); + if (!text.isEmpty()) { + TextChunk chunk = new TextChunk(); + chunk.value = text; + pending.add(chunk); + } + } + case "thinking_delta" -> { + String thinking = stringOrEmpty(Streams.pointer(delta, "thinking")); + if (!thinking.isEmpty()) { + ThinkingChunk chunk = new ThinkingChunk(); + chunk.value = thinking; + pending.add(chunk); + } + } + case "input_json_delta" -> { + ToolCall call = partialCalls.get(indexOf(event)); + if (call != null) { + call.arguments = + (call.arguments == null ? "" : call.arguments) + + stringOrEmpty(Streams.pointer(delta, "partial_json")); + } + } + default -> {} + } + } + + /** Emit whatever was withheld until the end: completed tool calls, then cumulative usage. */ + private void emitTerminal() { + for (ToolCall call : partialCalls.values()) { + ToolChunk chunk = new ToolChunk(); + chunk.toolCall = call; + pending.add(chunk); + } + partialCalls.clear(); + + if (hasUsage) { + InvocationUsage usage = new InvocationUsage(); + usage.inputTokens = inputTokens; + usage.outputTokens = outputTokens; + usage.totalTokens = inputTokens + outputTokens; + UsageChunk chunk = new UsageChunk(); + chunk.usage = usage; + pending.add(chunk); + hasUsage = false; + } + } + + private static int indexOf(Object event) { + Object index = Streams.pointer(event, "index"); + return index instanceof Number number ? number.intValue() : 0; + } + + private static long longAt(Object node, String key) { + Object value = Streams.pointer(node, key); + return value instanceof Number number ? number.longValue() : 0L; + } + + @Override + public void close() { + Streams.close(source); + } + } +} diff --git a/runtime/java/prompty-anthropic/src/main/java/com/microsoft/prompty/anthropic/SchemaException.java b/runtime/java/prompty-anthropic/src/main/java/com/microsoft/prompty/anthropic/SchemaException.java new file mode 100644 index 000000000..2769e84d0 --- /dev/null +++ b/runtime/java/prompty-anthropic/src/main/java/com/microsoft/prompty/anthropic/SchemaException.java @@ -0,0 +1,18 @@ +package com.microsoft.prompty.anthropic; + +/** + * Raised when a portable {@code Property} schema cannot be expressed as an Anthropic tool or output + * schema. + * + *

Failing at conversion rather than at the API boundary keeps the error attributable: the prompt + * author wrote a schema the provider will not take, and the message says which construct was at + * fault. + */ +public class SchemaException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public SchemaException(String message) { + super(message); + } +} diff --git a/runtime/java/prompty-anthropic/src/main/java/com/microsoft/prompty/anthropic/Wire.java b/runtime/java/prompty-anthropic/src/main/java/com/microsoft/prompty/anthropic/Wire.java new file mode 100644 index 000000000..ba9aea2ee --- /dev/null +++ b/runtime/java/prompty-anthropic/src/main/java/com/microsoft/prompty/anthropic/Wire.java @@ -0,0 +1,743 @@ +package com.microsoft.prompty.anthropic; + +import com.microsoft.prompty.model.ArrayProperty; +import com.microsoft.prompty.model.Binding; +import com.microsoft.prompty.model.ContentPart; +import com.microsoft.prompty.model.FilePart; +import com.microsoft.prompty.model.FunctionTool; +import com.microsoft.prompty.model.ImagePart; +import com.microsoft.prompty.model.Message; +import com.microsoft.prompty.model.ModelOptions; +import com.microsoft.prompty.model.ObjectProperty; +import com.microsoft.prompty.model.Property; +import com.microsoft.prompty.model.Prompty; +import com.microsoft.prompty.model.Role; +import com.microsoft.prompty.model.TextPart; +import com.microsoft.prompty.model.Tool; +import com.microsoft.prompty.model.ToolCall; +import com.microsoft.prompty.model.TypraJson; +import com.microsoft.prompty.model.UnionProperty; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +/** + * Request shaping for the Anthropic Messages API. + * + *

Anthropic and OpenAI agree on what a conversation is and disagree on almost every detail of how + * to write one down. System messages leave the message list and become a top-level {@code system} + * string; content is always an array of typed blocks rather than sometimes a bare string; tools put + * their schema under {@code input_schema} instead of nesting it inside a {@code function} wrapper; + * and {@code max_tokens} is mandatory rather than optional. Those differences are the entire reason + * this class exists separately from the OpenAI one — sharing an abstraction across them would cost + * more than it saves. + * + *

Everything here is a pure function of the prompt and its messages, which is what lets the shared + * wire vectors grade it without a network. + */ +public final class Wire { + + /** Anthropic rejects a request without {@code max_tokens}, so an unset value still needs one. */ + private static final long DEFAULT_MAX_TOKENS = 4096; + + /** The API version this wire format was written against, sent on every request. */ + public static final String ANTHROPIC_VERSION = "2023-06-01"; + + private Wire() {} + + // -------------------------------------------------------- request building + + /** Build the request body for {@code POST /v1/messages}. */ + public static Map buildChatArgs(Prompty agent, List messages) { + Map body = new LinkedHashMap<>(); + body.put("model", modelId(agent)); + + String system = extractSystem(messages); + if (!system.isEmpty()) { + body.put("system", system); + } + + List wireMessages = new ArrayList<>(); + for (Message message : messages) { + if (!isSystem(message)) { + wireMessages.add(messageToWire(message)); + } + } + body.put("messages", wireMessages); + + applyOptions(agent, body); + + List tools = toolsToWire(agent); + if (!tools.isEmpty()) { + body.put("tools", tools); + } + + Map outputConfig = outputConfigToWire(agent); + if (outputConfig != null) { + body.put("output_config", outputConfig); + } + + return body; + } + + /** Mark a request body as streaming. */ + public static void enableStreaming(Map body) { + body.put("stream", true); + } + + private static String modelId(Prompty agent) { + if (agent != null && agent.model != null && agent.model.id != null && !agent.model.id.isEmpty()) { + return agent.model.id; + } + return ""; + } + + // -------------------------------------------------------- messages + + private static boolean isSystem(Message message) { + return message != null && (message.role == Role.SYSTEM || message.role == Role.DEVELOPER); + } + + /** + * Collect every system message into the single string Anthropic expects. + * + *

Blank-line joining keeps separately authored instructions from running together, which is what + * they would otherwise do once the message boundaries are gone. + */ + private static String extractSystem(List messages) { + List blocks = new ArrayList<>(); + for (Message message : messages) { + if (isSystem(message)) { + blocks.add(textOf(message)); + } + } + return String.join("\n\n", blocks); + } + + private static String textOf(Message message) { + StringBuilder text = new StringBuilder(); + if (message != null && message.parts != null) { + for (ContentPart part : message.parts) { + if (part instanceof TextPart textPart && textPart.value != null) { + text.append(textPart.value); + } + } + } + return text.toString(); + } + + /** + * Convert one message to its wire form. + * + *

Three metadata keys change the shape entirely, because each records something the parts list + * cannot: {@code tool_results} carries a whole batch of results, {@code tool_use_id} identifies a + * single one, and {@code content} preserves the exact blocks a previous assistant turn produced. + * That last one matters most — Anthropic wants an assistant turn replayed verbatim, including + * thinking blocks and their signatures, and reconstructing it from text would lose them. + */ + public static Map messageToWire(Message message) { + Map wire = new LinkedHashMap<>(); + wire.put("role", roleName(message)); + + Map metadata = metadataOf(message); + + Object batched = metadata.get("tool_results"); + if (batched != null) { + wire.put("content", batched); + return wire; + } + + Object toolUseId = metadata.get("tool_use_id"); + if (toolUseId instanceof String id) { + // Present-but-empty still means "this is a tool result". Reinterpreting it as ordinary content + // would lose the framing and could produce a confusing success; forwarding it earns a clear + // rejection from the API instead. + Map block = new LinkedHashMap<>(); + block.put("type", "tool_result"); + block.put("tool_use_id", id); + block.put("content", textOf(message)); + wire.put("content", List.of(block)); + return wire; + } + + Object raw = metadata.get("content"); + if (raw != null) { + wire.put("content", raw); + return wire; + } + + List blocks = new ArrayList<>(); + if (message != null && message.parts != null) { + for (ContentPart part : message.parts) { + blocks.add(partToWire(part)); + } + } + wire.put("content", blocks); + return wire; + } + + /** + * Anthropic recognises only {@code user} and {@code assistant}. + * + *

Tool results arrive as user turns, which is how the API models them: the tool is something the + * caller ran on the model's behalf, so its output is the caller speaking. + */ + private static String roleName(Message message) { + if (message == null || message.role == null) { + return "user"; + } + return message.role == Role.ASSISTANT ? "assistant" : "user"; + } + + private static Map partToWire(ContentPart part) { + Map block = new LinkedHashMap<>(); + if (part instanceof TextPart text) { + block.put("type", "text"); + block.put("text", text.value == null ? "" : text.value); + return block; + } + if (part instanceof ImagePart image) { + String source = image.source == null ? "" : image.source; + block.put("type", "image"); + Map descriptor = new LinkedHashMap<>(); + if (source.startsWith("http://") || source.startsWith("https://")) { + descriptor.put("type", "url"); + descriptor.put("url", source); + } else { + descriptor.put("type", "base64"); + // Only an unset media type is defaulted. Guessing a type the caller explicitly blanked out + // risks labelling the bytes wrongly, which produces a garbled image rather than an error. + descriptor.put("media_type", image.mediaType == null ? "image/png" : image.mediaType); + descriptor.put("data", source); + } + block.put("source", descriptor); + return block; + } + // Anthropic accepts neither audio nor documents on this endpoint. A placeholder keeps the turn + // structurally valid and tells the model something was there, which silently dropping would not. + block.put("type", "text"); + block.put( + "text", + part instanceof FilePart + ? "[file content not supported by Anthropic]" + : "[audio content not supported by Anthropic]"); + return block; + } + + // -------------------------------------------------------- options + + private static ModelOptions options(Prompty agent) { + return agent == null || agent.model == null ? null : agent.model.options; + } + + /** + * Copy model options onto the body, then guarantee {@code max_tokens}. + * + *

The per-provider option names come from the generated model rather than a table kept here, so + * a schema change reaches every runtime at once instead of drifting between them. + */ + private static void applyOptions(Prompty agent, Map body) { + long maxTokens = DEFAULT_MAX_TOKENS; + ModelOptions options = options(agent); + + if (options != null) { + Map wire = options.toWire("anthropic"); + if (wire != null) { + for (Map.Entry entry : wire.entrySet()) { + if (entry.getValue() == null) { + continue; + } + if ("max_tokens".equals(entry.getKey())) { + if (entry.getValue() instanceof Number number) { + maxTokens = number.longValue(); + } + } else { + body.put(entry.getKey(), narrowFloat(entry.getValue())); + } + } + } + if (options.additionalProperties instanceof Map extra) { + for (Map.Entry entry : extra.entrySet()) { + String key = String.valueOf(entry.getKey()); + if (!body.containsKey(key)) { + body.put(key, entry.getValue()); + } + } + } + } + + body.put("max_tokens", maxTokens); + } + + /** + * Round a float back through its own shortest decimal form. + * + *

Widening a {@code float} to {@code double} exposes the binary approximation, so a temperature + * authored as 0.7 would otherwise reach the provider as 0.699999988079071. + */ + private static Object narrowFloat(Object value) { + if (value instanceof Float number) { + return Double.parseDouble(Float.toString(number)); + } + return value; + } + + // -------------------------------------------------------- tools + + /** Convert declared function tools to Anthropic's flat tool definitions. */ + public static List toolsToWire(Prompty agent) { + List wire = new ArrayList<>(); + if (agent == null || agent.tools == null) { + return wire; + } + for (Tool tool : agent.tools) { + wire.add(toolToWire(tool)); + } + return wire; + } + + private static Map toolToWire(Tool tool) { + Map wire = new LinkedHashMap<>(); + wire.put("name", tool.name == null ? "" : tool.name); + if (tool.description != null && !tool.description.isEmpty()) { + wire.put("description", tool.description); + } + + if (tool instanceof FunctionTool function) { + wire.put("input_schema", parametersToJsonSchema(unboundParameters(function))); + } else { + // A non-function tool is something the provider resolves itself; it still needs a schema slot, + // and an empty object is the honest description of "takes nothing we know about". + Map empty = new LinkedHashMap<>(); + empty.put("type", "object"); + empty.put("properties", new LinkedHashMap()); + wire.put("input_schema", empty); + } + return wire; + } + + /** + * The parameters the model is expected to supply. + * + *

A bound parameter is filled in by the host, so exposing it would invite the model to argue + * with a value it cannot influence. + */ + private static List unboundParameters(FunctionTool tool) { + List parameters = tool.parameters == null ? List.of() : tool.parameters; + Set bound = new LinkedHashSet<>(); + if (tool.bindings != null) { + for (Binding binding : tool.bindings) { + if (binding != null && binding.name != null) { + bound.add(binding.name); + } + } + } + if (bound.isEmpty()) { + return parameters; + } + List unbound = new ArrayList<>(); + for (Property parameter : parameters) { + if (parameter != null && !bound.contains(parameter.name)) { + unbound.add(parameter); + } + } + return unbound; + } + + private static Map parametersToJsonSchema(List parameters) { + Map properties = new LinkedHashMap<>(); + List required = new ArrayList<>(); + for (Property parameter : parameters) { + if (parameter == null || parameter.name == null || parameter.name.isEmpty()) { + continue; + } + properties.put(parameter.name, propertySchema(parameter)); + if (Boolean.TRUE.equals(parameter.required)) { + required.add(parameter.name); + } + } + + Map schema = new LinkedHashMap<>(); + schema.put("type", "object"); + schema.put("properties", properties); + if (!required.isEmpty()) { + schema.put("required", required); + } + return schema; + } + + // -------------------------------------------------------- schemas + + /** Convert one portable property into JSON Schema. */ + private static Map propertySchema(Property property) { + Map schema = new LinkedHashMap<>(); + + String jsonType = kindToJsonType(property.kind); + if (jsonType != null) { + schema.put("type", jsonType); + } + if (property.description != null && !property.description.isEmpty()) { + schema.put("description", property.description); + } + if (property.enumValues != null && !property.enumValues.isEmpty()) { + schema.put("enum", new ArrayList(property.enumValues)); + } + + if (property instanceof ArrayProperty array) { + // An array with no declared element type still needs one, and a string is the only guess that + // never makes the schema stricter than the author intended. + schema.put( + "items", array.items == null ? Map.of("type", "string") : propertySchema(array.items)); + } else if (property instanceof ObjectProperty object) { + Map nested = new LinkedHashMap<>(); + List required = new ArrayList<>(); + if (object.properties != null) { + for (Property member : object.properties) { + if (member == null || member.name == null || member.name.isEmpty()) { + continue; + } + nested.put(member.name, propertySchema(member)); + if (Boolean.TRUE.equals(member.required)) { + required.add(member.name); + } + } + } + schema.put("properties", nested); + if (!required.isEmpty()) { + schema.put("required", required); + } + schema.put("additionalProperties", false); + } else if (property instanceof UnionProperty union) { + boolean hasOneOf = union.oneOf != null && !union.oneOf.isEmpty(); + boolean hasAnyOf = union.anyOf != null && !union.anyOf.isEmpty(); + if (hasOneOf == hasAnyOf) { + // Both or neither leaves the branch set ambiguous, and guessing one would silently change + // what the model is allowed to return. + throw new SchemaException( + "UnionProperty must contain exactly one non-empty `oneOf` or `anyOf` array"); + } + List branches = new ArrayList<>(); + for (Property branch : hasOneOf ? union.oneOf : union.anyOf) { + branches.add(propertySchema(branch)); + } + schema.put(hasOneOf ? "oneOf" : "anyOf", branches); + } + + if (Boolean.TRUE.equals(property.nullable)) { + addNullability(schema); + } + return schema; + } + + /** + * Widen a schema so null is a legal value. + * + *

JSON Schema has four ways to say this depending on what the schema already is, and picking the + * wrong one produces a schema that validates nothing. + */ + private static void addNullability(Map schema) { + Object type = schema.get("type"); + if (type instanceof String single) { + // Re-inserting at the head keeps `type` first, which matters only for readability. + Map reordered = new LinkedHashMap<>(); + reordered.put("type", new ArrayList(List.of(single, "null"))); + for (Map.Entry entry : schema.entrySet()) { + if (!"type".equals(entry.getKey())) { + reordered.put(entry.getKey(), entry.getValue()); + } + } + schema.clear(); + schema.putAll(reordered); + } else if (schema.get("anyOf") instanceof List anyOf) { + List branches = new ArrayList<>(anyOf); + branches.add(Map.of("type", "null")); + schema.put("anyOf", branches); + } else if (schema.get("oneOf") instanceof List oneOf) { + List branches = new ArrayList<>(oneOf); + branches.add(Map.of("type", "null")); + schema.put("oneOf", branches); + } else if (!schema.isEmpty()) { + // Deliberately unlike the reference implementation, which inserts `anyOf` while leaving the + // original keys in place, yielding a schema that repeats its own constraints inside one of + // its branches. Both accept the same values; replacing is the shape a reader can follow. + // Only reachable for a property whose kind maps to no JSON type, so no vector grades it. + Map wrapped = new LinkedHashMap<>(schema); + schema.clear(); + schema.put("anyOf", new ArrayList(List.of(wrapped, Map.of("type", "null")))); + } + + if (schema.get("enum") instanceof List values) { + List widened = new ArrayList<>(values); + if (!widened.contains(null)) { + widened.add(null); + } + schema.put("enum", widened); + } + } + + private static String kindToJsonType(String kind) { + if (kind == null) { + return null; + } + return switch (kind) { + case "string" -> "string"; + case "integer" -> "integer"; + case "float", "number" -> "number"; + case "boolean" -> "boolean"; + case "array" -> "array"; + case "object" -> "object"; + default -> null; + }; + } + + // -------------------------------------------------------- structured output + + /** Convert declared outputs into Anthropic's {@code output_config}, or null when there are none. */ + private static Map outputConfigToWire(Prompty agent) { + if (agent == null || agent.outputs == null || agent.outputs.isEmpty()) { + return null; + } + + Map properties = new LinkedHashMap<>(); + List required = new ArrayList<>(); + for (Property output : agent.outputs) { + if (output == null || output.name == null || output.name.isEmpty()) { + continue; + } + properties.put(output.name, propertySchema(output)); + if (Boolean.TRUE.equals(output.required)) { + required.add(output.name); + } + } + + Map schema = new LinkedHashMap<>(); + schema.put("type", "object"); + schema.put("properties", properties); + if (!required.isEmpty()) { + schema.put("required", required); + } + schema.put("additionalProperties", false); + + Map format = new LinkedHashMap<>(); + format.put("type", "json_schema"); + format.put("schema", schema); + return Map.of("format", format); + } + + // -------------------------------------------------------- agent loop + + /** + * Replay a completed round of tool calls as conversation messages. + * + *

Two messages, always: the assistant turn that requested the calls, carrying its original + * content blocks so thinking signatures survive the round trip, then a single user turn holding + * every result. Anthropic batches results into one message rather than one message per result, + * which is the opposite of OpenAI and the reason this cannot be shared. + */ + public static List formatToolMessages( + Object rawResponse, List toolCalls, List toolResults) { + List messages = new ArrayList<>(); + + Object blocks = rawResponse instanceof Map map ? map.get("content") : null; + Message assistant = com.microsoft.prompty.Messages.assistant(""); + // Deep-copied, not aliased: the metadata outlives the response object, so sharing any node of + // it would let a caller that reuses or mutates the response rewrite a message already sent. + com.microsoft.prompty.Messages.metadata(assistant) + .put( + "content", + blocks instanceof List list + ? com.microsoft.prompty.Streams.deepCopy(list) + : new ArrayList<>()); + messages.add(assistant); + + List results = new ArrayList<>(); + for (int i = 0; i < toolCalls.size(); i++) { + ToolCall call = toolCalls.get(i); + // Every requested call needs an answer or the next request is rejected, so a missing result + // becomes an empty one rather than a gap. + String result = i < toolResults.size() ? toolResults.get(i) : ""; + Map block = new LinkedHashMap<>(); + block.put("type", "tool_result"); + block.put("tool_use_id", call.id); + block.put("content", result); + results.add(block); + } + + Message user = com.microsoft.prompty.Messages.user(""); + com.microsoft.prompty.Messages.metadata(user).put("tool_results", results); + messages.add(user); + + return messages; + } + + /** + * Rebuild the assistant turn from streamed events, then replay it with its tool results. + * + *

A streamed response never arrives as one object, so the content blocks have to be reassembled + * from their start events and deltas before they can be echoed back. Doing this faithfully is what + * lets a streamed tool round continue as if it had never been streamed. + */ + public static List formatStreamToolMessages( + List rawChunks, + List toolCalls, + List toolResults, + String textContent) { + Map> blocks = new TreeMap<>(); + Map partialInputs = new TreeMap<>(); + + for (Object event : rawChunks == null ? List.of() : rawChunks) { + int index = intAt(event, "index"); + String type = stringAt(event, "type"); + if ("content_block_start".equals(type)) { + Object block = com.microsoft.prompty.Streams.pointer(event, "content_block"); + if (block instanceof Map map) { + blocks.put(index, copyOf(map)); + } + continue; + } + if (!"content_block_delta".equals(type)) { + continue; + } + Object delta = com.microsoft.prompty.Streams.pointer(event, "delta"); + if (delta == null) { + continue; + } + switch (String.valueOf(stringAt(delta, "type"))) { + case "text_delta" -> + appendTo(blocks, index, "text", stringAt(delta, "text"), seed("text", "text")); + case "thinking_delta" -> + appendTo( + blocks, index, "thinking", stringAt(delta, "thinking"), seed("thinking", "thinking")); + case "signature_delta" -> + appendTo( + blocks, + index, + "signature", + stringAt(delta, "signature"), + // A thinking block is only valid with its `thinking` field present, so a signature + // arriving before any thinking text still has to seed one. + seed("thinking", "thinking", "signature")); + case "input_json_delta" -> + partialInputs + .computeIfAbsent(index, key -> new StringBuilder()) + .append(orEmpty(stringAt(delta, "partial_json"))); + default -> {} + } + } + + for (Map.Entry entry : partialInputs.entrySet()) { + Map block = blocks.get(entry.getKey()); + if (block != null && entry.getValue().length() > 0) { + block.put("input", parseOrEmpty(entry.getValue().toString())); + } + } + + if (blocks.isEmpty()) { + // No raw events survived — a replayed or synthesised stream. The accumulated text and tool + // calls still describe the turn, so rebuild it from those rather than sending an empty one. + int index = 0; + if (textContent != null && !textContent.isEmpty()) { + Map text = new LinkedHashMap<>(); + text.put("type", "text"); + text.put("text", textContent); + blocks.put(index++, text); + } + for (ToolCall call : toolCalls) { + Map use = new LinkedHashMap<>(); + use.put("type", "tool_use"); + use.put("id", call.id); + use.put("name", call.name); + use.put("input", parseOrEmpty(call.arguments)); + blocks.put(index++, use); + } + } + + Map response = Map.of("content", new ArrayList(blocks.values())); + return formatToolMessages(response, toolCalls, toolResults); + } + + /** + * A freshly seeded content block of the given type, with each named field present but empty. + * + *

A block has to be structurally complete even when only one of its fields ever receives a + * delta, because Anthropic rejects a thinking block that arrives without its {@code thinking} + * field on the replay. + */ + private static Map seed(String type, String... fields) { + Map block = new LinkedHashMap<>(); + block.put("type", type); + for (String field : fields) { + block.put(field, ""); + } + return block; + } + + private static void appendTo( + Map> blocks, + int index, + String field, + String addition, + Map seed) { + Map block = blocks.computeIfAbsent(index, key -> new LinkedHashMap<>(seed)); + Object current = block.get(field); + block.put(field, (current instanceof String text ? text : "") + orEmpty(addition)); + } + + private static Map copyOf(Map map) { + Map copy = new LinkedHashMap<>(); + for (Map.Entry entry : map.entrySet()) { + copy.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return copy; + } + + private static Object parseOrEmpty(String json) { + if (json == null || json.isEmpty()) { + return new LinkedHashMap(); + } + try { + return TypraJson.parse(json); + } catch (RuntimeException e) { + // Truncated or malformed arguments are the model's problem to have made, not ours to raise; + // an empty object keeps the replayed turn well-formed. + return new LinkedHashMap(); + } + } + + private static Map metadataOf(Message message) { + if (message == null || message.metadata == null) { + return Map.of(); + } + return message.metadata; + } + + private static String stringAt(Object node, String key) { + Object value = com.microsoft.prompty.Streams.pointer(node, key); + return value instanceof String text ? text : null; + } + + private static String orEmpty(String value) { + return value == null ? "" : value; + } + + private static int intAt(Object node, String key) { + Object value = com.microsoft.prompty.Streams.pointer(node, key); + return value instanceof Number number ? number.intValue() : 0; + } + + /** Exposed so the processor can share one notion of "these are the text blocks". */ + static String joinText(Collection blocks) { + StringBuilder text = new StringBuilder(); + for (Object block : blocks) { + if ("text".equals(stringAt(block, "type"))) { + text.append(orEmpty(stringAt(block, "text"))); + } + } + return text.toString(); + } +} diff --git a/runtime/java/prompty-anthropic/src/main/resources/META-INF/services/com.microsoft.prompty.PromptyExtension b/runtime/java/prompty-anthropic/src/main/resources/META-INF/services/com.microsoft.prompty.PromptyExtension new file mode 100644 index 000000000..edced96bc --- /dev/null +++ b/runtime/java/prompty-anthropic/src/main/resources/META-INF/services/com.microsoft.prompty.PromptyExtension @@ -0,0 +1 @@ +com.microsoft.prompty.anthropic.AnthropicExtension \ No newline at end of file diff --git a/runtime/java/prompty-anthropic/src/test/java/com/microsoft/prompty/anthropic/AnthropicExecutorTest.java b/runtime/java/prompty-anthropic/src/test/java/com/microsoft/prompty/anthropic/AnthropicExecutorTest.java new file mode 100644 index 000000000..32c13f0f2 --- /dev/null +++ b/runtime/java/prompty-anthropic/src/test/java/com/microsoft/prompty/anthropic/AnthropicExecutorTest.java @@ -0,0 +1,149 @@ +package com.microsoft.prompty.anthropic; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.microsoft.prompty.Environment; +import com.microsoft.prompty.InvokerException; +import com.microsoft.prompty.model.LoadContext; +import com.microsoft.prompty.model.Prompty; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +/** + * Covers how a prompt's connection turns into an addressed, authenticated request. + * + *

Everything here happens before a byte is sent, and getting it wrong produces either a 404 + * against a plausible-looking URL or a 401 that reads like a bad key — both of which are far harder + * to diagnose from a live call than from a test. + */ +class AnthropicExecutorTest { + + private final AnthropicExecutor executor = new AnthropicExecutor(); + + @AfterEach + void clearEnvironment() { + Environment.clear("ANTHROPIC_API_KEY"); + Environment.clear("ANTHROPIC_BASE_URL"); + } + + private static Prompty agent(Map model) { + Map data = new LinkedHashMap<>(); + data.put("name", "test"); + data.put("kind", "prompt"); + data.put("instructions", "test"); + data.put("model", model); + return Prompty.load(data, new LoadContext()); + } + + private static Prompty agentWithConnection(Map connection) { + Map model = new LinkedHashMap<>(); + model.put("id", "claude-3"); + model.put("provider", "anthropic"); + if (connection != null) { + model.put("connection", connection); + } + return agent(model); + } + + @Test + void theDefaultEndpointIsUsedWhenTheConnectionNamesNone() { + assertEquals( + "https://api.anthropic.com/v1/messages", executor.buildUrl(agentWithConnection(null))); + } + + @Test + void anEnvironmentBaseUrlOverridesTheDefault() { + Environment.set("ANTHROPIC_BASE_URL", "https://proxy.internal"); + assertEquals("https://proxy.internal/v1/messages", executor.buildUrl(agentWithConnection(null))); + } + + @Test + void aConnectionEndpointWinsOverTheEnvironment() { + Environment.set("ANTHROPIC_BASE_URL", "https://proxy.internal"); + Prompty agent = + agentWithConnection( + Map.of("kind", "key", "endpoint", "https://declared.example", "apiKey", "k")); + assertEquals("https://declared.example/v1/messages", executor.buildUrl(agent)); + } + + @Test + void anEndpointThatAlreadyNamesTheVersionIsNotVersionedTwice() { + Prompty agent = agentWithConnection(Map.of("kind", "anonymous", "endpoint", "https://proxy/v1")); + // A proxy base is commonly written with the version on it; appending another yields /v1/v1. + assertEquals("https://proxy/v1/messages", executor.buildUrl(agent)); + } + + @Test + void aTrailingSlashDoesNotProduceADoubledSeparator() { + Prompty agent = agentWithConnection(Map.of("kind", "anonymous", "endpoint", "https://proxy/")); + assertEquals("https://proxy/v1/messages", executor.buildUrl(agent)); + } + + @Test + void severalTrailingSlashesAreAllRemoved() { + // Endpoints get pasted from consoles; leaving `https://proxy//v1/messages` behind is routed + // differently by some gateways and rejected outright by others. + Prompty agent = agentWithConnection(Map.of("kind", "anonymous", "endpoint", "https://proxy///")); + assertEquals("https://proxy/v1/messages", executor.buildUrl(agent)); + } + + @Test + void theKeyOnTheConnectionIsPreferredOverTheEnvironment() { + Environment.set("ANTHROPIC_API_KEY", "from-env"); + Prompty agent = agentWithConnection(Map.of("kind", "key", "apiKey", "from-connection")); + assertEquals("from-connection", executor.apiKey(agent)); + } + + @Test + void theEnvironmentSuppliesTheKeyWhenTheConnectionDoesNot() { + Environment.set("ANTHROPIC_API_KEY", "from-env"); + assertEquals("from-env", executor.apiKey(agentWithConnection(null))); + } + + @Test + void aMissingKeyFailsWithAMessageThatNamesBothPlacesToPutOne() { + // Masked rather than left to chance: a machine that exports ANTHROPIC_API_KEY for live runs + // would otherwise satisfy the lookup and leave no absence to assert on. + Environment.mask("ANTHROPIC_API_KEY"); + InvokerException failure = + assertThrows(InvokerException.class, () -> executor.apiKey(agentWithConnection(null))); + assertTrue(failure.getMessage().contains("ANTHROPIC_API_KEY")); + assertTrue(failure.getMessage().contains("connection.apiKey")); + } + + @Test + void requestsCarryAnApiKeyHeaderAndAPinnedApiVersion() { + Environment.set("ANTHROPIC_API_KEY", "sk-test"); + Map headers = executor.authHeaders(agentWithConnection(null)); + + // Anthropic authenticates with x-api-key, not a bearer token, and pins wire-format changes to + // the version header — omitting it opts the client into whatever the API defaults to. + assertEquals("sk-test", headers.get("x-api-key")); + assertEquals(Wire.ANTHROPIC_VERSION, headers.get("anthropic-version")); + } + + @Test + void chatAndAgentBothBuildAMessagesRequest() { + for (String apiType : List.of("chat", "agent")) { + Prompty agent = + agent(Map.of("id", "claude-3", "provider", "anthropic", "apiType", apiType)); + assertEquals("claude-3", executor.buildArgs(agent, List.of()).get("model")); + } + } + + @Test + void anApiTypeAnthropicDoesNotOfferIsRejectedBeforeSending() { + Prompty agent = + agent(Map.of("id", "claude-3", "provider", "anthropic", "apiType", "embedding")); + // Failing here names the real problem; letting it through produces a 404 on a URL that looks + // correct. + InvokerException failure = + assertThrows(InvokerException.class, () -> executor.buildArgs(agent, List.of())); + assertTrue(failure.getMessage().contains("embedding")); + } +} diff --git a/runtime/java/prompty-anthropic/src/test/java/com/microsoft/prompty/anthropic/AnthropicLiveTest.java b/runtime/java/prompty-anthropic/src/test/java/com/microsoft/prompty/anthropic/AnthropicLiveTest.java new file mode 100644 index 000000000..d157e2364 --- /dev/null +++ b/runtime/java/prompty-anthropic/src/test/java/com/microsoft/prompty/anthropic/AnthropicLiveTest.java @@ -0,0 +1,199 @@ +package com.microsoft.prompty.anthropic; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.microsoft.prompty.LiveEnv; +import com.microsoft.prompty.Pipeline; +import com.microsoft.prompty.Registry; +import com.microsoft.prompty.TurnOptions; +import com.microsoft.prompty.model.Message; +import com.microsoft.prompty.model.ModelInfo; +import com.microsoft.prompty.model.Prompty; +import com.microsoft.prompty.model.StreamChunk; +import com.microsoft.prompty.model.TextChunk; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +/** + * End-to-end coverage against the real Anthropic API. + * + *

Anthropic's wire format differs from OpenAI's in ways the fixtures encode but cannot validate — + * the system prompt is a top-level field rather than a message, tool results are user-role content + * blocks, and structured output is expressed through a tool rather than a response format. These + * tests confirm the service accepts what the runtime actually sends. + * + *

Excluded from the normal build by the {@code live} tag. Run with {@code -PliveTests} and an + * {@code ANTHROPIC_API_KEY} in {@code runtime/java/.env}. + */ +@Tag("live") +@DisplayName("live: Anthropic") +final class AnthropicLiveTest { + + @BeforeAll + static void setUp() { + LiveEnv.load(); + } + + private static String modelId() { + return LiveEnv.get("ANTHROPIC_MODEL", "claude-3-5-haiku-latest"); + } + + private static Prompty chatAgent(String question, Map options) { + return LiveEnv.agent( + new LiveEnv.Spec("anthropic", modelId()) + .chat("You are a helpful assistant. Be very brief.", question) + .options(options)); + } + + @Test + void chatCompletionReturnsText() { + LiveEnv.require("ANTHROPIC_API_KEY"); + + Object result = + Pipeline.invoke( + chatAgent("Say hello in exactly 3 words.", Map.of("temperature", 0, "maxOutputTokens", 100)), + Map.of()); + + String text = Pipeline.textOf(result); + assertNotNull(text); + assertFalse(text.isBlank(), "chat completion returned no text"); + System.out.println("[anthropic] chat -> " + text); + } + + @Test + void chatStreamingYieldsIncrementalChunksOverSse() { + LiveEnv.require("ANTHROPIC_API_KEY"); + + Map options = new LinkedHashMap<>(); + options.put("temperature", 0); + options.put("maxOutputTokens", 400); + // `stream` is not a declared model option; it rides in the passthrough bag, which is also where + // the executor looks for it. + options.put("additionalProperties", Map.of("stream", true)); + // Long enough that the service splits it across content-block deltas. A short answer arrives in + // a single delta, which would let a parser that ignored incremental framing pass. + Prompty agent = + chatAgent("Count from 1 to 60, separated by spaces. Output only the numbers.", options); + + // Anthropic's SSE framing differs from OpenAI's — named events carrying content-block deltas + // rather than one delta shape — so driving the parser directly is what proves the runtime reads + // the real framing rather than a fixture's idealised version of it. + List messages = Pipeline.prepare(agent, Map.of()); + Iterator raw = Registry.executor("anthropic").executeStream(agent, messages); + Iterator chunks = Registry.processor("anthropic").processStream(agent, raw); + + StringBuilder text = new StringBuilder(); + int textChunks = 0; + while (chunks.hasNext()) { + StreamChunk chunk = chunks.next(); + if (chunk instanceof TextChunk t) { + textChunks++; + text.append(t.value); + } + } + + assertTrue(textChunks > 1, "expected more than one text chunk, got " + textChunks); + assertTrue(text.toString().contains("60"), "expected the counted answer to reach 60: " + text); + System.out.println("[anthropic] stream over sse -> " + textChunks + " chunks: " + text); + } + + @Test + void listModelsReturnsAtLeastOneModel() { + LiveEnv.require("ANTHROPIC_API_KEY"); + + Map connection = new LinkedHashMap<>(); + connection.put("kind", "key"); + List models = new AnthropicModelLister().listModels(connection); + + assertFalse(models.isEmpty(), "expected at least one model"); + assertTrue( + models.stream().anyMatch(m -> m.id != null && !m.id.isBlank()), + "every listed model should carry an id"); + System.out.println("[anthropic] models -> " + models.size() + " available"); + } + + @Test + void structuredOutputParsesIntoDeclaredFields() { + LiveEnv.require("ANTHROPIC_API_KEY"); + + Prompty agent = + LiveEnv.agent( + new LiveEnv.Spec("anthropic", modelId()) + .chat( + "Extract structured data from the user's message.", + "My name is Ada Lovelace and I am 36 years old.") + .options(Map.of("temperature", 0, "maxOutputTokens", 512)) + .outputs( + List.of( + Map.of("name", "name", "kind", "string", "required", true), + Map.of("name", "age", "kind", "integer", "required", true)))); + + Object result = Pipeline.invoke(agent, Map.of()); + + Map fields = assertInstanceOf(Map.class, result, "structured output should yield a map"); + assertTrue(fields.containsKey("name"), "missing declared field 'name' in " + fields); + assertTrue(fields.containsKey("age"), "missing declared field 'age' in " + fields); + assertTrue( + String.valueOf(fields.get("name")).contains("Ada"), + "expected the extracted name to mention Ada but got: " + fields.get("name")); + System.out.println("[anthropic] structured -> " + fields); + } + + @Test + void agentTurnCallsAToolAndUsesTheResult() { + LiveEnv.require("ANTHROPIC_API_KEY"); + + Map location = new LinkedHashMap<>(); + location.put("name", "location"); + location.put("kind", "string"); + location.put("description", "The city to report on"); + location.put("required", true); + + Map tool = new LinkedHashMap<>(); + tool.put("name", "get_weather"); + tool.put("kind", "function"); + tool.put("description", "Get the current weather for a city"); + tool.put("parameters", List.of(location)); + + Prompty agent = + LiveEnv.agent( + new LiveEnv.Spec("anthropic", modelId()) + .apiType("agent") + .chat( + "You are a helpful assistant. Use the provided tools when they apply.", + "What is the weather in Paris? Use the get_weather tool.") + .options(Map.of("temperature", 0, "maxOutputTokens", 512)) + .tools(List.of(tool))); + + boolean[] called = {false}; + TurnOptions options = + TurnOptions.builder() + .tool( + "get_weather", + arguments -> { + called[0] = true; + System.out.println("[anthropic] tool invoked with " + arguments); + return "{\"temperature\":\"18C\",\"conditions\":\"cloudy\"}"; + }) + .build(); + + Object result = Pipeline.turn(agent, Map.of(), options); + + assertTrue(called[0], "the model never called the tool"); + String text = Pipeline.textOf(result); + assertFalse(text.isBlank(), "the turn produced no final text"); + assertTrue( + text.contains("18") || text.toLowerCase().contains("cloud"), + "expected the answer to reflect the tool result but got: " + text); + System.out.println("[anthropic] agent -> " + text); + } +} diff --git a/runtime/java/prompty-anthropic/src/test/java/com/microsoft/prompty/anthropic/AnthropicStreamingTest.java b/runtime/java/prompty-anthropic/src/test/java/com/microsoft/prompty/anthropic/AnthropicStreamingTest.java new file mode 100644 index 000000000..9101722a7 --- /dev/null +++ b/runtime/java/prompty-anthropic/src/test/java/com/microsoft/prompty/anthropic/AnthropicStreamingTest.java @@ -0,0 +1,303 @@ +package com.microsoft.prompty.anthropic; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.microsoft.prompty.Messages; +import com.microsoft.prompty.model.ErrorChunk; +import com.microsoft.prompty.model.Message; +import com.microsoft.prompty.model.StreamChunk; +import com.microsoft.prompty.model.TextChunk; +import com.microsoft.prompty.model.ThinkingChunk; +import com.microsoft.prompty.model.ToolCall; +import com.microsoft.prompty.model.ToolChunk; +import com.microsoft.prompty.model.UsageChunk; +import java.io.Closeable; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +/** + * Covers the Anthropic streaming path, which no spec vector reaches. + * + *

A streamed response arrives as a sequence of partial events that only mean something in + * aggregate, so the behaviour worth pinning down is how those fragments are assembled: what is + * forwarded immediately, what is held back until the stream ends, and what happens when the stream + * ends badly. + */ +class AnthropicStreamingTest { + + private static Map event(String type, Object... pairs) { + Map map = new LinkedHashMap<>(); + map.put("type", type); + for (int i = 0; i + 1 < pairs.length; i += 2) { + map.put(String.valueOf(pairs[i]), pairs[i + 1]); + } + return map; + } + + private static List drain(List events) { + Iterator chunks = + new AnthropicProcessor().processStream(null, new ArrayList<>(events).iterator()); + List collected = new ArrayList<>(); + while (chunks.hasNext()) { + collected.add(chunks.next()); + } + return collected; + } + + @Test + void textDeltasAreForwardedAsTheyArrive() { + List chunks = + drain( + List.of( + event("content_block_delta", "index", 0, "delta", event("text_delta", "text", "He")), + event( + "content_block_delta", "index", 0, "delta", event("text_delta", "text", "llo")))); + + assertEquals(2, chunks.size()); + assertEquals("He", assertInstanceOf(TextChunk.class, chunks.get(0)).value); + assertEquals("llo", assertInstanceOf(TextChunk.class, chunks.get(1)).value); + } + + @Test + void thinkingDeltasAreForwardedSeparatelyFromText() { + List chunks = + drain( + List.of( + event( + "content_block_delta", + "index", + 0, + "delta", + event("thinking_delta", "thinking", "hmm")), + event( + "content_block_delta", "index", 1, "delta", event("text_delta", "text", "done")))); + + assertEquals("hmm", assertInstanceOf(ThinkingChunk.class, chunks.get(0)).value); + assertEquals("done", assertInstanceOf(TextChunk.class, chunks.get(1)).value); + } + + @Test + void toolArgumentsAccumulateAcrossDeltasAndEmitOnceComplete() { + List chunks = + drain( + List.of( + event( + "content_block_start", + "index", + 0, + "content_block", + event("tool_use", "id", "toolu_1", "name", "get_weather")), + event( + "content_block_delta", + "index", + 0, + "delta", + event("input_json_delta", "partial_json", "{\"city\":")), + event( + "content_block_delta", + "index", + 0, + "delta", + event("input_json_delta", "partial_json", "\"Paris\"}")))); + + // Nothing is emitted while the arguments are still arriving: a half-parsed tool call is not + // something a caller can act on, so the call surfaces only once it is whole. + List tools = new ArrayList<>(); + for (StreamChunk chunk : chunks) { + if (chunk instanceof ToolChunk tool) { + tools.add(tool); + } + } + assertEquals(1, tools.size()); + ToolCall call = tools.get(0).toolCall; + assertEquals("toolu_1", call.id); + assertEquals("get_weather", call.name); + assertEquals("{\"city\":\"Paris\"}", call.arguments); + } + + @Test + void usageFromBothEndsOfTheStreamIsCombinedIntoOneFinalChunk() { + List chunks = + drain( + List.of( + event("message_start", "message", event("message", "usage", Map.of("input_tokens", 10))), + event("content_block_delta", "index", 0, "delta", event("text_delta", "text", "hi")), + event("message_delta", "usage", Map.of("output_tokens", 5)))); + + UsageChunk usage = assertInstanceOf(UsageChunk.class, chunks.get(chunks.size() - 1)); + assertEquals(Long.valueOf(10), usage.usage.inputTokens); + assertEquals(Long.valueOf(5), usage.usage.outputTokens); + assertEquals(Long.valueOf(15), usage.usage.totalTokens); + } + + @Test + void anErrorEventEndsTheStream() { + List chunks = + drain( + List.of( + event("content_block_delta", "index", 0, "delta", event("text_delta", "text", "hi")), + event("error", "error", Map.of("message", "overloaded")), + event("content_block_delta", "index", 0, "delta", event("text_delta", "text", "no")))); + + assertEquals(2, chunks.size()); + ErrorChunk error = assertInstanceOf(ErrorChunk.class, chunks.get(1)); + assertTrue(error.message.contains("overloaded")); + } + + @Test + void terminatingOnErrorReleasesTheUnderlyingStream() { + CloseTrackingIterator source = + new CloseTrackingIterator( + List.of( + event("error", "error", Map.of("message", "overloaded")), + event("content_block_delta", "index", 0, "delta", event("text_delta", "text", "no")))); + + Iterator chunks = new AnthropicProcessor().processStream(null, source); + while (chunks.hasNext()) { + chunks.next(); + } + + // An abandoned HTTP response holds a connection open until something closes it, and an error + // is exactly the case where nobody is going to read the rest. + assertTrue(source.closed, "the transport should be closed once the stream terminates"); + } + + @Test + void streamedBlocksAreRebuiltSoAToolRoundCanContinue() { + List events = + List.of( + event( + "content_block_start", + "index", + 0, + "content_block", + event("thinking", "thinking", "", "signature", "")), + event( + "content_block_delta", + "index", + 0, + "delta", + event("thinking_delta", "thinking", "weighing options")), + event( + "content_block_delta", + "index", + 0, + "delta", + event("signature_delta", "signature", "sig-abc")), + event( + "content_block_start", + "index", + 1, + "content_block", + event("tool_use", "id", "toolu_1", "name", "get_weather")), + event( + "content_block_delta", + "index", + 1, + "delta", + event("input_json_delta", "partial_json", "{\"city\":\"Paris\"}"))); + + ToolCall call = new ToolCall(); + call.id = "toolu_1"; + call.name = "get_weather"; + call.arguments = "{\"city\":\"Paris\"}"; + + List messages = + Wire.formatStreamToolMessages(events, List.of(call), List.of("sunny"), ""); + + assertEquals(2, messages.size()); + Object blocks = Messages.metadata(messages.get(0)).get("content"); + List content = assertInstanceOf(List.class, blocks); + assertEquals(2, content.size()); + + // The thinking block must come back with its signature intact, or Anthropic rejects the replay. + Map thinking = assertInstanceOf(Map.class, content.get(0)); + assertEquals("thinking", thinking.get("type")); + assertEquals("weighing options", thinking.get("thinking")); + assertEquals("sig-abc", thinking.get("signature")); + + Map use = assertInstanceOf(Map.class, content.get(1)); + assertEquals("tool_use", use.get("type")); + assertEquals(Map.of("city", "Paris"), use.get("input")); + + Object results = Messages.metadata(messages.get(1)).get("tool_results"); + List resultBlocks = assertInstanceOf(List.class, results); + assertEquals(1, resultBlocks.size()); + assertEquals("sunny", assertInstanceOf(Map.class, resultBlocks.get(0)).get("content")); + } + + @Test + void aReplayedStreamWithNoRawEventsIsRebuiltFromWhatWasAccumulated() { + ToolCall call = new ToolCall(); + call.id = "toolu_1"; + call.name = "get_weather"; + call.arguments = "{\"city\":\"Paris\"}"; + + List messages = + Wire.formatStreamToolMessages(List.of(), List.of(call), List.of("sunny"), "Let me check."); + + List content = assertInstanceOf(List.class, Messages.metadata(messages.get(0)).get("content")); + assertEquals(2, content.size()); + assertEquals("Let me check.", assertInstanceOf(Map.class, content.get(0)).get("text")); + assertEquals("tool_use", assertInstanceOf(Map.class, content.get(1)).get("type")); + } + + @Test + void everyToolCallGetsAnAnswerEvenWhenOneIsMissing() { + ToolCall first = new ToolCall(); + first.id = "toolu_1"; + first.name = "a"; + ToolCall second = new ToolCall(); + second.id = "toolu_2"; + second.name = "b"; + + List messages = + Wire.formatToolMessages( + Map.of("content", List.of()), List.of(first, second), List.of("only one")); + + // Anthropic rejects a conversation containing a tool_use with no matching tool_result, so a + // short result list is padded rather than truncating the calls it fails to cover. + List results = + assertInstanceOf(List.class, Messages.metadata(messages.get(1)).get("tool_results")); + assertEquals(2, results.size()); + assertEquals("toolu_2", assertInstanceOf(Map.class, results.get(1)).get("tool_use_id")); + assertEquals("", assertInstanceOf(Map.class, results.get(1)).get("content")); + } + + /** An iterator that records whether the consumer closed it. */ + private static final class CloseTrackingIterator implements Iterator, Closeable { + private final Iterator delegate; + boolean closed; + + CloseTrackingIterator(List events) { + this.delegate = new ArrayList<>(events).iterator(); + } + + @Override + public boolean hasNext() { + return delegate.hasNext(); + } + + @Override + public Object next() { + return delegate.next(); + } + + @Override + public void close() { + closed = true; + } + } + + @Test + void anEmptyStreamProducesNothingRatherThanAnEmptyTurn() { + assertFalse(drain(List.of()).iterator().hasNext()); + } +} diff --git a/runtime/java/prompty-anthropic/src/test/java/com/microsoft/prompty/anthropic/DiscoveryVectorsTest.java b/runtime/java/prompty-anthropic/src/test/java/com/microsoft/prompty/anthropic/DiscoveryVectorsTest.java new file mode 100644 index 000000000..6a3c3c2df --- /dev/null +++ b/runtime/java/prompty-anthropic/src/test/java/com/microsoft/prompty/anthropic/DiscoveryVectorsTest.java @@ -0,0 +1,63 @@ +package com.microsoft.prompty.anthropic; + +import com.microsoft.prompty.Discovery; +import com.microsoft.prompty.SpecVectors; +import com.microsoft.prompty.model.ModelInfo; +import com.microsoft.prompty.model.SaveContext; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; + +/** + * Grades the Anthropic half of the shared discovery and enrichment suites. + * + *

The discovery vectors pin the wire mapping; the enrichment vectors pin the shared capability + * dataset and the fill-only-missing rule that applies it. Both are the same fixtures every other + * runtime is measured by. + */ +class DiscoveryVectorsTest { + + @TestFactory + Iterable discoveryVectors() { + List tests = new ArrayList<>(); + for (Map vector : SpecVectors.readCases("discovery/discovery_vectors.json", "vectors")) { + if (!"anthropic".equals(vector.get("provider"))) { + continue; + } + String name = SpecVectors.string(vector, "name"); + tests.add( + DynamicTest.dynamicTest( + name, + () -> { + ModelInfo actual = + AnthropicModelLister.modelInfoFromWire(SpecVectors.map(vector, "input")); + SpecVectors.assertEquivalent( + name, vector.get("expected"), actual.save(new SaveContext())); + })); + } + return tests; + } + + @TestFactory + Iterable enrichmentVectors() { + List tests = new ArrayList<>(); + for (Map vector : SpecVectors.readCases("discovery/enrichment_vectors.json", "vectors")) { + if (!"anthropic".equals(vector.get("provider"))) { + continue; + } + String name = SpecVectors.string(vector, "name"); + tests.add( + DynamicTest.dynamicTest( + name, + () -> { + ModelInfo info = ModelInfo.load(SpecVectors.map(vector, "input"), null); + Discovery.enrich("anthropic", info); + SpecVectors.assertEquivalent( + name, vector.get("expected"), info.save(new SaveContext())); + })); + } + return tests; + } +} diff --git a/runtime/java/prompty-anthropic/src/test/java/com/microsoft/prompty/anthropic/ProcessVectorsTest.java b/runtime/java/prompty-anthropic/src/test/java/com/microsoft/prompty/anthropic/ProcessVectorsTest.java new file mode 100644 index 000000000..ba627979f --- /dev/null +++ b/runtime/java/prompty-anthropic/src/test/java/com/microsoft/prompty/anthropic/ProcessVectorsTest.java @@ -0,0 +1,46 @@ +package com.microsoft.prompty.anthropic; + +import com.microsoft.prompty.SpecVectors; +import com.microsoft.prompty.VectorAgents; +import com.microsoft.prompty.model.Prompty; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; + +/** + * Grades Anthropic response interpretation against the shared {@code spec/vectors/process} suite. + */ +class ProcessVectorsTest { + + @TestFactory + Iterable processVectors() { + List tests = new ArrayList<>(); + for (Map vector : SpecVectors.readArray("process/process_vectors.json")) { + String name = SpecVectors.string(vector, "name"); + Map input = SpecVectors.map(vector, "input"); + + if (!"anthropic".equals(input.get("provider"))) { + continue; + } + + tests.add( + DynamicTest.dynamicTest( + name, + () -> { + Prompty agent = VectorAgents.buildProcessAgent(input, "claude-3", "anthropic"); + Object actual = AnthropicProcessor.processResponse(agent, input.get("response")); + Object expected = SpecVectors.map(vector, "expected").get("result"); + + // A response with nothing to say and a response that said nothing are the same + // outcome to a caller; the fixtures spell one of them as an empty string. + if ("".equals(expected) && (actual == null || "".equals(actual))) { + return; + } + SpecVectors.assertEquivalent(name, expected, actual); + })); + } + return tests; + } +} diff --git a/runtime/java/prompty-anthropic/src/test/java/com/microsoft/prompty/anthropic/WireMessageTest.java b/runtime/java/prompty-anthropic/src/test/java/com/microsoft/prompty/anthropic/WireMessageTest.java new file mode 100644 index 000000000..c39399e5d --- /dev/null +++ b/runtime/java/prompty-anthropic/src/test/java/com/microsoft/prompty/anthropic/WireMessageTest.java @@ -0,0 +1,344 @@ +package com.microsoft.prompty.anthropic; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.microsoft.prompty.Messages; +import com.microsoft.prompty.Streams; +import com.microsoft.prompty.VectorAgents; +import com.microsoft.prompty.model.LoadContext; +import com.microsoft.prompty.model.Message; +import com.microsoft.prompty.model.Prompty; +import com.microsoft.prompty.model.ToolCall; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +/** + * Covers request shaping the shared vectors leave ungraded. + * + *

There are only five Anthropic wire vectors, so most of the conversion is reached by no fixture + * at all. These tests aim at the parts a plausible refactor could change without a vector noticing: + * how several system messages combine, how options merge, and how a message's metadata reshapes it. + */ +class WireMessageTest { + + private static Message message(String role, String text, Map metadata) { + Map data = new LinkedHashMap<>(); + data.put("role", role); + data.put("parts", List.of(Map.of("kind", "text", "value", text))); + if (metadata != null) { + data.put("metadata", metadata); + } + return Message.load(data, new LoadContext()); + } + + private static Prompty plainAgent() { + return VectorAgents.buildAgent(new LinkedHashMap<>(), "claude-3", "anthropic"); + } + + // ------------------------------------------------------------- system + + @Test + void severalSystemMessagesAreJoinedWithABlankLine() { + Map body = + Wire.buildChatArgs( + plainAgent(), + List.of( + message("system", "First rule.", null), + message("system", "Second rule.", null), + message("user", "hi", null))); + + // The blank line is what keeps two instructions from reading as one run-on sentence. + assertEquals("First rule.\n\nSecond rule.", body.get("system")); + } + + @Test + void developerMessagesJoinTheSystemPromptRatherThanTheConversation() { + Map body = + Wire.buildChatArgs( + plainAgent(), + List.of( + message("system", "Be brief.", null), + message("developer", "Never guess.", null), + message("user", "hi", null))); + + assertEquals("Be brief.\n\nNever guess.", body.get("system")); + // Anthropic has no system role in the message list, so both must have left it. + List messages = assertInstanceOf(List.class, body.get("messages")); + assertEquals(1, messages.size()); + assertEquals("user", assertInstanceOf(Map.class, messages.get(0)).get("role")); + } + + @Test + void aPromptWithNoSystemMessageSendsNoSystemField() { + Map body = Wire.buildChatArgs(plainAgent(), List.of(message("user", "hi", null))); + assertFalse(body.containsKey("system")); + } + + @Test + void toolRoleMessagesBecomeUserTurns() { + Map body = Wire.buildChatArgs(plainAgent(), List.of(message("tool", "42", null))); + // A tool ran on the model's behalf, so its output is the caller speaking. + assertEquals("user", Streams.pointer(body, "messages", 0, "role")); + } + + // ------------------------------------------------------------- metadata reshaping + + @Test + void aBatchOfToolResultsReplacesTheMessageContentWholesale() { + List results = + List.of(Map.of("type", "tool_result", "tool_use_id", "toolu_1", "content", "sunny")); + Map body = + Wire.buildChatArgs( + plainAgent(), + List.of(message("user", "ignored", Map.of("tool_results", results)))); + + assertEquals(results, Streams.pointer(body, "messages", 0, "content")); + } + + @Test + void aSingleToolResultIsWrappedAsAToolResultBlock() { + Map body = + Wire.buildChatArgs( + plainAgent(), + List.of(message("user", "sunny", Map.of("tool_use_id", "toolu_1")))); + + assertEquals("tool_result", Streams.pointer(body, "messages", 0, "content", 0, "type")); + assertEquals("toolu_1", Streams.pointer(body, "messages", 0, "content", 0, "tool_use_id")); + assertEquals("sunny", Streams.pointer(body, "messages", 0, "content", 0, "content")); + } + + @Test + void anEmptyToolUseIdIsStillTreatedAsAToolResult() { + // Present-but-empty means the caller built a broken message. Reinterpreting it as ordinary + // content would hide that behind a confusing success instead of a clear rejection. + Map body = + Wire.buildChatArgs( + plainAgent(), + List.of(message("user", "x", Map.of("tool_use_id", "")))); + assertEquals("tool_result", Streams.pointer(body, "messages", 0, "content", 0, "type")); + } + + @Test + void rawAssistantBlocksAreReplayedVerbatim() { + List blocks = + List.of(Map.of("type", "thinking", "thinking", "hmm", "signature", "sig-abc")); + Map body = + Wire.buildChatArgs( + plainAgent(), + List.of(message("assistant", "ignored", Map.of("content", blocks)))); + + // A thinking block's signature only validates against the exact bytes Anthropic produced, so + // re-deriving the block from the message text would break the replay. + assertEquals(blocks, Streams.pointer(body, "messages", 0, "content")); + } + + // ------------------------------------------------------------- parts + + @Test + void remoteImagesAreSentByUrlAndLocalOnesAsBase64() { + Map remote = + Wire.messageToWire( + imageMessage("https://example.com/cat.png", null)); + assertEquals("url", Streams.pointer(remote, "content", 0, "source", "type")); + assertEquals("https://example.com/cat.png", Streams.pointer(remote, "content", 0, "source", "url")); + + Map local = Wire.messageToWire(imageMessage("aGVsbG8=", "image/jpeg")); + assertEquals("base64", Streams.pointer(local, "content", 0, "source", "type")); + assertEquals("image/jpeg", Streams.pointer(local, "content", 0, "source", "media_type")); + assertEquals("aGVsbG8=", Streams.pointer(local, "content", 0, "source", "data")); + } + + @Test + void anUndeclaredMediaTypeDefaultsButABlankOneIsSentAsGiven() { + assertEquals( + "image/png", + Streams.pointer(Wire.messageToWire(imageMessage("aGVsbG8=", null)), "content", 0, "source", "media_type")); + // Guessing a type the caller explicitly blanked out risks labelling the bytes wrongly, which + // yields a garbled image rather than an error. + assertEquals( + "", + Streams.pointer(Wire.messageToWire(imageMessage("aGVsbG8=", "")), "content", 0, "source", "media_type")); + } + + @Test + void audioAndFilePartsDegradeToVisiblePlaceholders() { + for (Map.Entry entry : + Map.of("audio", "[audio content not supported by Anthropic]", + "file", "[file content not supported by Anthropic]") + .entrySet()) { + Map data = new LinkedHashMap<>(); + data.put("role", "user"); + data.put("parts", List.of(Map.of("kind", entry.getKey(), "source", "x"))); + Map wire = Wire.messageToWire(Message.load(data, new LoadContext())); + + // Silently dropping the part would leave the model answering a question it cannot see. + assertEquals("text", Streams.pointer(wire, "content", 0, "type")); + assertEquals(entry.getValue(), Streams.pointer(wire, "content", 0, "text")); + } + } + + private static Message imageMessage(String source, String mediaType) { + Map part = new LinkedHashMap<>(); + part.put("kind", "image"); + part.put("source", source); + if (mediaType != null) { + part.put("mediaType", mediaType); + } + Map data = new LinkedHashMap<>(); + data.put("role", "user"); + data.put("parts", List.of(part)); + return Message.load(data, new LoadContext()); + } + + // ------------------------------------------------------------- options + + @Test + void additionalPropertiesFillGapsWithoutOverridingDeclaredOptions() { + Prompty agent = + VectorAgents.buildAgent( + Map.of( + "options", + Map.of( + "temperature", + 0.5, + "additionalProperties", + Map.of("temperature", 0.9, "top_logprobs", 3))), + "claude-3", + "anthropic"); + + Map body = Wire.buildChatArgs(agent, List.of()); + // A declared option is the author's explicit intent; an escape-hatch property is a fallback for + // things the model has no field for, so it must not quietly win. + assertEquals(0.5, ((Number) body.get("temperature")).doubleValue(), 1e-9); + assertEquals(3, ((Number) body.get("top_logprobs")).intValue()); + } + + @Test + void anExplicitMaxTokensReplacesTheDefault() { + Prompty agent = + VectorAgents.buildAgent( + Map.of("options", Map.of("maxOutputTokens", 512)), + "claude-3", + "anthropic"); + assertEquals(512, ((Number) Wire.buildChatArgs(agent, List.of()).get("max_tokens")).intValue()); + } + + @Test + void temperatureSurvivesAsTheAuthorWroteIt() { + Prompty agent = + VectorAgents.buildAgent( + Map.of("options", Map.of("temperature", 0.7)), "claude-3", "anthropic"); + // Widening a 32-bit 0.7 to a double naively yields 0.699999988079071 on the wire. + assertEquals("0.7", String.valueOf(Wire.buildChatArgs(agent, List.of()).get("temperature"))); + } + + // ------------------------------------------------------------- tool replay + + @Test + void aNonEmptyAssistantTurnIsReplayedWithItsBlocksIntact() { + List blocks = + List.of( + Map.of("type", "text", "text", "Let me check."), + Map.of("type", "tool_use", "id", "toolu_1", "name", "get_weather", "input", Map.of())); + ToolCall call = new ToolCall(); + call.id = "toolu_1"; + call.name = "get_weather"; + + List messages = + Wire.formatToolMessages(Map.of("content", blocks), List.of(call), List.of("sunny")); + + assertEquals(blocks, Messages.metadata(messages.get(0)).get("content")); + } + + @Test + void theReplayedBlocksAreCopiedRatherThanAliased() { + Map block = new LinkedHashMap<>(); + block.put("type", "text"); + block.put("text", "hi"); + List blocks = new ArrayList<>(List.of(block)); + ToolCall call = new ToolCall(); + call.id = "toolu_1"; + + List messages = + Wire.formatToolMessages(Map.of("content", blocks), List.of(call), List.of("ok")); + Object stored = Messages.metadata(messages.get(0)).get("content"); + + // The metadata outlives the response object; sharing any node of it would let a caller that + // reuses the response silently rewrite a message already sent. + assertNotSame(blocks, stored); + blocks.clear(); + List storedList = assertInstanceOf(List.class, stored); + assertEquals(1, storedList.size()); + + // The nested block has to be copied too — duplicating only the outer list would leave the + // block maps aliased, which is the case a shallow copy quietly misses. + block.put("text", "rewritten"); + assertEquals("hi", Streams.pointer(stored, 0, "text")); + } + + @Test + void aResponseWithNoContentStillProducesTheTwoTurnShape() { + ToolCall call = new ToolCall(); + call.id = "toolu_1"; + List messages = Wire.formatToolMessages(Map.of(), List.of(call), List.of("ok")); + + assertEquals(2, messages.size()); + assertTrue(assertInstanceOf(List.class, Messages.metadata(messages.get(0)).get("content")).isEmpty()); + } + + // ------------------------------------------------------------- streamed replay + + @Test + void aDeltaWithNoPrecedingBlockStartStillProducesAValidBlock() { + // Anthropic always sends content_block_start first, but a replayed or trimmed capture may not. + // The reconstructed block still has to be one the API will accept on the next request. + List chunks = + List.of( + delta(0, Map.of("type", "text_delta", "text", "hi")), + delta(1, Map.of("type", "thinking_delta", "thinking", "hmm")), + delta(2, Map.of("type", "signature_delta", "signature", "sig-abc"))); + + List blocks = replayBlocks(chunks); + + assertEquals(Map.of("type", "text", "text", "hi"), blocks.get(0)); + assertEquals(Map.of("type", "thinking", "thinking", "hmm"), blocks.get(1)); + // A thinking block is rejected without its `thinking` field, so a signature arriving alone has + // to seed one rather than produce `{type: thinking, signature: ...}`. + assertEquals(Map.of("type", "thinking", "thinking", "", "signature", "sig-abc"), blocks.get(2)); + } + + @Test + void thinkingTextAndSignatureAccumulateIntoTheSameBlock() { + List chunks = + List.of( + delta(0, Map.of("type", "thinking_delta", "thinking", "one ")), + delta(0, Map.of("type", "thinking_delta", "thinking", "two")), + delta(0, Map.of("type", "signature_delta", "signature", "sig-abc"))); + + // The signature only validates against the exact thinking text it was produced for, so both + // have to land on one block in the order they arrived. + assertEquals( + Map.of("type", "thinking", "thinking", "one two", "signature", "sig-abc"), + replayBlocks(chunks).get(0)); + } + + private static List replayBlocks(List chunks) { + List messages = Wire.formatStreamToolMessages(chunks, List.of(), List.of(), ""); + return assertInstanceOf(List.class, Messages.metadata(messages.get(0)).get("content")); + } + + private static Map delta(int index, Map delta) { + Map event = new LinkedHashMap<>(); + event.put("type", "content_block_delta"); + event.put("index", index); + event.put("delta", delta); + return event; + } +} diff --git a/runtime/java/prompty-anthropic/src/test/java/com/microsoft/prompty/anthropic/WireSchemaTest.java b/runtime/java/prompty-anthropic/src/test/java/com/microsoft/prompty/anthropic/WireSchemaTest.java new file mode 100644 index 000000000..0e013dfaa --- /dev/null +++ b/runtime/java/prompty-anthropic/src/test/java/com/microsoft/prompty/anthropic/WireSchemaTest.java @@ -0,0 +1,337 @@ +package com.microsoft.prompty.anthropic; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.microsoft.prompty.Streams; +import com.microsoft.prompty.VectorAgents; +import com.microsoft.prompty.model.Prompty; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +/** + * Covers Anthropic's schema conversion, which differs from OpenAI's in ways no shared vector reaches. + * + *

Anthropic takes tools flat rather than nested under a function wrapper, describes structured + * output through {@code output_config} rather than {@code response_format}, and — unlike OpenAI's + * strict mode — never widens {@code required} to include optional properties. Each of those is a + * place a shared implementation would quietly produce the wrong request. + */ +class WireSchemaTest { + + private static Prompty agentWith(String key, Object value) { + Map input = new LinkedHashMap<>(); + input.put(key, value); + return VectorAgents.buildAgent(input, "claude-3", "anthropic"); + } + + private static Map toolSchema(Object tools) { + List wire = Wire.toolsToWire(agentWith("tools", tools)); + Map tool = assertInstanceOf(Map.class, wire.get(0)); + return castSchema(tool.get("input_schema")); + } + + @SuppressWarnings("unchecked") + private static Map castSchema(Object node) { + return (Map) assertInstanceOf(Map.class, node); + } + + @Test + void toolsAreFlatRatherThanNestedUnderAFunctionWrapper() { + List wire = + Wire.toolsToWire( + agentWith( + "tools", + List.of( + Map.of( + "name", + "get_weather", + "kind", + "function", + "description", + "Look up the weather")))); + + Map tool = assertInstanceOf(Map.class, wire.get(0)); + assertEquals("get_weather", tool.get("name")); + assertEquals("Look up the weather", tool.get("description")); + // OpenAI would wrap this as {type:"function", function:{...}}; Anthropic must not. + assertNull(tool.get("type")); + assertNull(tool.get("function")); + assertTrue(tool.containsKey("input_schema")); + } + + @Test + void onlyGenuinelyRequiredParametersAreListedAsRequired() { + Map schema = + toolSchema( + List.of( + Map.of( + "name", + "search", + "kind", + "function", + "parameters", + List.of( + Map.of("name", "query", "kind", "string", "required", true), + Map.of("name", "limit", "kind", "integer"))))); + + // OpenAI's strict mode requires listing every property and marking the optional ones nullable. + // Anthropic has no such rule, so widening `required` here would misdescribe the tool. + assertEquals(List.of("query"), schema.get("required")); + } + + @Test + void aToolWithNoRequiredParametersOmitsTheRequiredKeyEntirely() { + Map schema = + toolSchema( + List.of( + Map.of( + "name", + "ping", + "kind", + "function", + "parameters", + List.of(Map.of("name", "note", "kind", "string"))))); + + assertFalse(schema.containsKey("required"), "an empty required list should not be sent"); + } + + @Test + void nestedObjectsCarryTheirOwnRequiredListAndRejectExtraProperties() { + Map schema = + toolSchema( + List.of( + Map.of( + "name", + "book", + "kind", + "function", + "parameters", + List.of( + Map.of( + "name", + "trip", + "kind", + "object", + "required", + true, + "properties", + List.of( + Map.of("name", "city", "kind", "string", "required", true), + Map.of("name", "hotel", "kind", "string"))))))); + + Object nested = Streams.pointer(schema, "properties", "trip"); + assertEquals(List.of("city"), castSchema(nested).get("required")); + assertEquals(false, castSchema(nested).get("additionalProperties")); + } + + @Test + void boundParametersAreHiddenFromTheModel() { + Map schema = + toolSchema( + List.of( + Map.of( + "name", + "search", + "kind", + "function", + "parameters", + List.of( + Map.of("name", "query", "kind", "string", "required", true), + Map.of("name", "api_key", "kind", "string", "required", true)), + "bindings", + List.of(Map.of("name", "api_key", "input", "secret"))))); + + // A bound parameter is supplied by the host, so offering it to the model invites an argument + // about a value the model cannot influence. + Map properties = castSchema(schema.get("properties")); + assertTrue(properties.containsKey("query")); + assertFalse(properties.containsKey("api_key")); + assertEquals(List.of("query"), schema.get("required")); + } + + @Test + void aNonFunctionToolStillGetsAnEmptyObjectSchema() { + List wire = + Wire.toolsToWire(agentWith("tools", List.of(Map.of("name", "web_search", "kind", "mcp")))); + + Map schema = castSchema(assertInstanceOf(Map.class, wire.get(0)).get("input_schema")); + assertEquals("object", schema.get("type")); + assertEquals(Map.of(), schema.get("properties")); + } + + @Test + void nullablePropertiesWidenTheirTypeRatherThanBeingDropped() { + Map schema = + toolSchema( + List.of( + Map.of( + "name", + "note", + "kind", + "function", + "parameters", + List.of(Map.of("name", "body", "kind", "string", "nullable", true))))); + + Object body = Streams.pointer(schema, "properties", "body"); + assertEquals(List.of("string", "null"), castSchema(body).get("type")); + } + + @Test + void aNullableEnumGainsNullAsAPermittedValue() { + Map schema = + toolSchema( + List.of( + Map.of( + "name", + "note", + "kind", + "function", + "parameters", + List.of( + Map.of( + "name", + "level", + "kind", + "string", + "nullable", + true, + "enumValues", + List.of("low", "high")))))); + + Object level = Streams.pointer(schema, "properties", "level"); + // Widening only `type` would leave an enum that still rejects null, so the schema would + // contradict the declaration it came from. + assertEquals(List.of("string", "null"), castSchema(level).get("type")); + List values = assertInstanceOf(List.class, castSchema(level).get("enum")); + assertEquals(java.util.Arrays.asList("low", "high", null), values); + } + + @Test + void aNullableUnionGainsANullBranchRatherThanANullType() { + Map schema = + toolSchema( + List.of( + Map.of( + "name", + "note", + "kind", + "function", + "parameters", + List.of( + Map.of( + "name", + "value", + "kind", + "union", + "nullable", + true, + "anyOf", + List.of( + Map.of("name", "a", "kind", "string"), + Map.of("name", "b", "kind", "integer"))))))); + + Object value = Streams.pointer(schema, "properties", "value"); + // A union has no single `type` to widen, so null has to become another alternative. + assertNull(castSchema(value).get("type")); + List branches = assertInstanceOf(List.class, castSchema(value).get("anyOf")); + assertEquals(3, branches.size()); + assertEquals(Map.of("type", "null"), branches.get(2)); + } + + @Test + void aNullableObjectIsWrappedSoItsOwnConstraintsSurvive() { + Map schema = + toolSchema( + List.of( + Map.of( + "name", + "note", + "kind", + "function", + "parameters", + List.of( + Map.of( + "name", + "who", + "kind", + "object", + "nullable", + true, + "properties", + List.of(Map.of("name", "id", "kind", "string"))))))); + + Map who = castSchema(Streams.pointer(schema, "properties", "who")); + assertEquals(List.of("object", "null"), who.get("type")); + // The nested shape has to survive the widening; losing it would accept any object at all. + assertEquals( + Map.of("type", "string"), Streams.pointer(who, "properties", "id")); + } + + @Test + void structuredOutputUsesOutputConfigRatherThanResponseFormat() { + Prompty agent = + agentWith( + "outputs", + List.of( + Map.of("name", "answer", "kind", "string", "required", true), + Map.of("name", "confidence", "kind", "float"))); + + Map body = Wire.buildChatArgs(agent, List.of()); + + // OpenAI spells this `response_format`; sending that to Anthropic is silently ignored, which + // would turn a structured-output prompt into a plain one without any error. + assertNull(body.get("response_format")); + assertEquals("json_schema", Streams.pointer(body, "output_config", "format", "type")); + + Object schema = Streams.pointer(body, "output_config", "format", "schema"); + assertEquals("object", castSchema(schema).get("type")); + assertEquals(List.of("answer"), castSchema(schema).get("required")); + assertEquals(false, castSchema(schema).get("additionalProperties")); + assertEquals("number", Streams.pointer(schema, "properties", "confidence", "type")); + } + + @Test + void aPromptWithNoOutputsSendsNoOutputConfig() { + assertNull(Wire.buildChatArgs(agentWith("model_id", "claude-3"), List.of()).get("output_config")); + } + + @Test + void anAmbiguousUnionIsRejectedRatherThanGuessed() { + // Both branch sets populated leaves it undecidable which one constrains the model, and picking + // one would silently change what the model is allowed to return. + assertThrows( + SchemaException.class, + () -> + toolSchema( + List.of( + Map.of( + "name", + "choose", + "kind", + "function", + "parameters", + List.of( + Map.of( + "name", + "value", + "kind", + "union", + "oneOf", + List.of(Map.of("name", "a", "kind", "string")), + "anyOf", + List.of(Map.of("name", "b", "kind", "integer")))))))); + } + + @Test + void maxTokensIsAlwaysSentBecauseAnthropicRequiresIt() { + Map body = Wire.buildChatArgs(agentWith("model_id", "claude-3"), List.of()); + assertEquals(4096L, ((Number) body.get("max_tokens")).longValue()); + } +} diff --git a/runtime/java/prompty-anthropic/src/test/java/com/microsoft/prompty/anthropic/WireVectorsTest.java b/runtime/java/prompty-anthropic/src/test/java/com/microsoft/prompty/anthropic/WireVectorsTest.java new file mode 100644 index 000000000..39edbcf9f --- /dev/null +++ b/runtime/java/prompty-anthropic/src/test/java/com/microsoft/prompty/anthropic/WireVectorsTest.java @@ -0,0 +1,52 @@ +package com.microsoft.prompty.anthropic; + +import com.microsoft.prompty.SpecVectors; +import com.microsoft.prompty.VectorAgents; +import com.microsoft.prompty.model.Message; +import com.microsoft.prompty.model.Prompty; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; + +/** + * Grades the Anthropic wire conversion against the shared {@code spec/vectors/wire} suite. + * + *

These are the same fixtures every other runtime is measured by, so a vector that passes here + * is evidence of cross-runtime agreement rather than merely of internal consistency. + */ +class WireVectorsTest { + + @TestFactory + Iterable wireVectors() { + List tests = new ArrayList<>(); + for (Map vector : SpecVectors.readArray("wire/wire_vectors.json")) { + String name = SpecVectors.string(vector, "name"); + Map input = SpecVectors.map(vector, "input"); + + // Vectors for other providers are graded by those providers' suites. + if (!"anthropic".equals(input.get("provider"))) { + continue; + } + + tests.add(DynamicTest.dynamicTest(name, () -> runVector(name, vector, input))); + } + return tests; + } + + private static void runVector(String name, Map vector, Map input) { + Prompty agent = VectorAgents.buildAgent(input, "claude-3", "anthropic"); + List messages = VectorAgents.buildMessages(input); + String apiType = String.valueOf(input.getOrDefault("apiType", "chat")); + + // Anthropic exposes a single endpoint; anything else is a vector this provider cannot serve. + if (!"chat".equals(apiType) && !"agent".equals(apiType)) { + throw new AssertionError("Anthropic vectors must use apiType chat or agent, got: " + apiType); + } + + Map actual = Wire.buildChatArgs(agent, messages); + Object expected = SpecVectors.map(vector, "expected").get("request_body"); + SpecVectors.assertEquivalent(name, expected, actual); + } +} diff --git a/runtime/java/prompty-foundry/build.gradle.kts b/runtime/java/prompty-foundry/build.gradle.kts new file mode 100644 index 000000000..d7d26f302 --- /dev/null +++ b/runtime/java/prompty-foundry/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + id("java-library") +} + +dependencies { + api(project(":prompty")) + // The Foundry wire format is OpenAI's, so the provider builds on that module rather than + // restating it. + api(project(":prompty-openai")) + + testImplementation(platform("org.junit:junit-bom:5.11.4")) + testImplementation("org.junit.jupiter:junit-jupiter") + testImplementation(testFixtures(project(":prompty"))) + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} diff --git a/runtime/java/prompty-foundry/src/main/java/com/microsoft/prompty/foundry/FoundryArm.java b/runtime/java/prompty-foundry/src/main/java/com/microsoft/prompty/foundry/FoundryArm.java new file mode 100644 index 000000000..9b9ac516e --- /dev/null +++ b/runtime/java/prompty-foundry/src/main/java/com/microsoft/prompty/foundry/FoundryArm.java @@ -0,0 +1,317 @@ +package com.microsoft.prompty.foundry; + +import com.microsoft.prompty.Http; +import com.microsoft.prompty.model.AiResourceInfo; +import com.microsoft.prompty.model.ProjectInfo; +import com.microsoft.prompty.model.SubscriptionInfo; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +/** + * Read-only resource enumeration against the Azure Resource Manager control plane. + * + *

Answers the three questions a Foundry resource picker has to ask in order: which subscriptions + * can this identity see, which AI resources live in one, and which projects belong to a resource. + * Every call needs a management-plane bearer token — {@link FoundryOAuth#AZURE_MANAGEMENT_SCOPE}, + * not the inference scope used to call a model. + * + *

Only the protocol lives here. Selection, ordering, caching, and any interactive wizard are host + * concerns, and results are returned as the generated provider-neutral model rather than an + * ARM-shaped one so a host is not coupled to Azure's payload layout. + */ +public final class FoundryArm { + + private static final String ARM_BASE = "https://management.azure.com"; + + /** + * The bound on a single control-plane exchange. + * + *

A person is usually waiting on a picker while these run, so a stalled response has to fail + * rather than hang. Model calls get no such bound because a slow answer there is still an answer. + */ + private static final Duration REQUEST_TIMEOUT = Duration.ofSeconds(30); + private static final String SUBSCRIPTIONS_API_VERSION = "2022-12-01"; + private static final String ACCOUNTS_API_VERSION = "2023-05-01"; + private static final String COGNITIVE_PROJECTS_API_VERSION = "2025-04-01-preview"; + private static final String ML_WORKSPACES_API_VERSION = "2024-10-01"; + + /** + * The endpoint entry a model call should prefer. + * + *

An AI Services account publishes several endpoints; this is the one that speaks the OpenAI + * inference API, and picking any other would yield a URL that authenticates but cannot complete. + */ + private static final String ENDPOINT_PREFERENCE_KEY = "OpenAI Language Model Instance API"; + + private static final String PROVIDER = "azure-arm"; + + private FoundryArm() {} + + /** List the subscriptions this token can see, keeping only those that are enabled. */ + public static List listSubscriptions(String token) { + List subscriptions = new ArrayList<>(); + for (Map item : + fetchAll(token, ARM_BASE + "/subscriptions?api-version=" + SUBSCRIPTIONS_API_VERSION)) { + SubscriptionInfo subscription = parseSubscription(item); + if (subscription != null) { + subscriptions.add(subscription); + } + } + return subscriptions; + } + + /** + * List the Azure OpenAI and AI Services accounts in a subscription. + * + *

Accounts of other kinds, and accounts with no usable inference endpoint, are dropped: a + * picker entry the caller cannot actually send a request to is worse than no entry at all. + */ + public static List listAiResources(String token, String subscriptionId) { + String url = + ARM_BASE + + "/subscriptions/" + + subscriptionId + + "/providers/Microsoft.CognitiveServices/accounts?api-version=" + + ACCOUNTS_API_VERSION; + List resources = new ArrayList<>(); + for (Map item : fetchAll(token, url)) { + AiResourceInfo resource = parseAiResource(item); + if (resource != null) { + resources.add(resource); + } + } + return resources; + } + + /** + * List the Foundry projects belonging to an account. + * + *

Projects exist in two shapes. New Foundry exposes them as a sub-resource of the account; + * classic hubs model them as Machine Learning workspaces. The classic endpoint is consulted only + * when the new one yields nothing, so a modern account is never charged for a second round trip. + * + *

Each strategy soft-fails to empty. A tenant that denies one of the two providers is common + * and is not a reason to fail the whole call; an empty list means "none found", which is exactly + * what a picker should show. + */ + public static List listFoundryProjects( + String token, String subscriptionId, String resourceGroup, String resourceName) { + List projects = new ArrayList<>(); + + String modern = + ARM_BASE + + "/subscriptions/" + + subscriptionId + + "/resourceGroups/" + + resourceGroup + + "/providers/Microsoft.CognitiveServices/accounts/" + + resourceName + + "/projects?api-version=" + + COGNITIVE_PROJECTS_API_VERSION; + for (Map item : fetchAllOrEmpty(token, modern)) { + projects.add(parseModernProject(item, resourceName)); + } + + if (projects.isEmpty()) { + String classic = + ARM_BASE + + "/subscriptions/" + + subscriptionId + + "/providers/Microsoft.MachineLearningServices/workspaces?api-version=" + + ML_WORKSPACES_API_VERSION; + for (Map item : fetchAllOrEmpty(token, classic)) { + ProjectInfo project = parseClassicWorkspace(item, resourceName); + if (project != null) { + projects.add(project); + } + } + } + + return projects; + } + + // --------------------------------------------------------------------------------------------- + // Paging + // --------------------------------------------------------------------------------------------- + + /** + * One page read against the control plane. + * + *

Taken as a parameter so the paging loop can be driven without a network, the same way {@link + * FoundryOAuth} takes its token endpoint rather than reaching for the transport directly. + */ + @FunctionalInterface + interface PageEndpoint { + Object get(String url); + } + + /** + * Read every page of an ARM list endpoint. + * + *

ARM returns absolute {@code nextLink} URLs, so each page dictates where the next one is + * rather than the caller computing offsets. + */ + static List> fetchAll(String token, String firstUrl) { + return fetchAll(bearerEndpoint(token), firstUrl); + } + + /** {@link #fetchAll(String, String)} with its transport supplied. */ + @SuppressWarnings("unchecked") + static List> fetchAll(PageEndpoint endpoint, String firstUrl) { + List> items = new ArrayList<>(); + String next = firstUrl; + + while (next != null && !next.isEmpty()) { + Object body = endpoint.get(next); + if (!(body instanceof Map page)) { + break; + } + if (page.get("value") instanceof List values) { + for (Object value : values) { + if (value instanceof Map entry) { + items.add((Map) entry); + } + } + } + next = page.get("nextLink") instanceof String link ? link : null; + } + + return items; + } + + /** {@link #fetchAll} with failure treated as an empty page, for the soft-failing project probes. */ + private static List> fetchAllOrEmpty(String token, String url) { + return fetchAllOrEmpty(bearerEndpoint(token), url); + } + + /** {@link #fetchAllOrEmpty(String, String)} with its transport supplied. */ + static List> fetchAllOrEmpty(PageEndpoint endpoint, String url) { + try { + return fetchAll(endpoint, url); + } catch (RuntimeException e) { + return List.of(); + } + } + + /** The real transport: a bearer-authenticated control-plane GET. */ + private static PageEndpoint bearerEndpoint(String token) { + return url -> + Http.getJson(PROVIDER, url, Map.of("Authorization", "Bearer " + token), REQUEST_TIMEOUT); + } + + // --------------------------------------------------------------------------------------------- + // Parsing + // --------------------------------------------------------------------------------------------- + + static SubscriptionInfo parseSubscription(Map item) { + String state = string(item, "state"); + if (!"Enabled".equals(state)) { + return null; + } + SubscriptionInfo subscription = new SubscriptionInfo(); + subscription.subscriptionId = string(item, "subscriptionId"); + subscription.displayName = string(item, "displayName"); + subscription.state = state; + return subscription; + } + + static AiResourceInfo parseAiResource(Map item) { + String kind = string(item, "kind"); + if (!"AIServices".equals(kind) && !"OpenAI".equals(kind)) { + return null; + } + + Map properties = object(item, "properties"); + String endpoint = string(object(properties, "endpoints"), ENDPOINT_PREFERENCE_KEY); + if (endpoint.isEmpty()) { + endpoint = string(properties, "endpoint"); + } + if (endpoint.isEmpty()) { + // No inference endpoint means nothing can be sent here, so the entry is not offerable. + return null; + } + + String name = string(item, "name"); + AiResourceInfo resource = new AiResourceInfo(); + resource.name = name; + resource.kind = kind; + resource.endpoint = endpoint; + resource.location = string(item, "location"); + resource.resourceGroup = extractResourceGroup(string(item, "id")); + // Only AI Services accounts have the project-style host; an OpenAI account has no such alias. + resource.serviceUrl = + "AIServices".equals(kind) ? "https://" + name + ".services.ai.azure.com" : null; + return resource; + } + + static ProjectInfo parseModernProject(Map item, String resourceName) { + // ARM names a child resource "parent/child"; the picker wants the child alone. + String full = string(item, "name"); + int slash = full.lastIndexOf('/'); + String shortName = slash < 0 ? full : full.substring(slash + 1); + + String displayName = string(object(item, "properties"), "displayName"); + + ProjectInfo project = new ProjectInfo(); + project.name = shortName; + // Absence falls back to the resource name; an explicit empty string is kept. A project the + // author deliberately left unnamed reads the same in every runtime this way. + project.displayName = present(object(item, "properties"), "displayName") ? displayName : shortName; + project.endpoint = + "https://" + resourceName + ".services.ai.azure.com/api/projects/" + shortName; + return project; + } + + static ProjectInfo parseClassicWorkspace(Map item, String resourceName) { + if (!"Project".equals(string(item, "kind"))) { + return null; + } + String name = string(item, "name"); + String friendly = string(object(item, "properties"), "friendlyName"); + + ProjectInfo project = new ProjectInfo(); + project.name = name; + project.displayName = present(object(item, "properties"), "friendlyName") ? friendly : name; + project.endpoint = "https://" + resourceName + ".services.ai.azure.com/api/projects/" + name; + return project; + } + + /** + * Pull the resource-group segment out of an ARM resource id. + * + *

Matched case-insensitively because ARM is inconsistent about whether it writes + * {@code resourceGroups} or {@code resourcegroups}, and the two refer to the same thing. + */ + static String extractResourceGroup(String id) { + String[] segments = id.split("/"); + for (int i = 0; i < segments.length - 1; i++) { + if (segments[i].equalsIgnoreCase("resourceGroups")) { + return segments[i + 1]; + } + } + return ""; + } + + private static String string(Map source, String key) { + return source.get(key) instanceof String text ? text : ""; + } + + /** + * Whether a key carries a string, as distinct from carrying an empty one. + * + *

{@link #string} flattens "absent", "not a string", and "" to the same value, which is right + * nearly everywhere. It is wrong where a fallback applies only on absence, because it would then + * also overwrite a deliberately empty value. + */ + private static boolean present(Map source, String key) { + return source.get(key) instanceof String; + } + + @SuppressWarnings("unchecked") + private static Map object(Map source, String key) { + return source.get(key) instanceof Map nested + ? (Map) nested + : Map.of(); + } +} diff --git a/runtime/java/prompty-foundry/src/main/java/com/microsoft/prompty/foundry/FoundryAuth.java b/runtime/java/prompty-foundry/src/main/java/com/microsoft/prompty/foundry/FoundryAuth.java new file mode 100644 index 000000000..021444794 --- /dev/null +++ b/runtime/java/prompty-foundry/src/main/java/com/microsoft/prompty/foundry/FoundryAuth.java @@ -0,0 +1,78 @@ +package com.microsoft.prompty.foundry; + +import com.microsoft.prompty.model.Connection; +import com.microsoft.prompty.model.SaveContext; +import java.util.Map; +import java.util.Optional; + +/** + * Credential resolution for Foundry provider operations. + * + *

Credentials are read off the connection's saved form rather than a specific typed field, + * because the same credential travels under several names depending on which connection shape a + * host wrote. Blank values are treated as absent, so a connection that carries an empty key still + * falls through to the environment rather than authenticating with nothing. + * + *

Java's connections are typed, so an alias the model does not declare — {@code api_key}, + * {@code bearer_token} — is dropped at load and cannot be seen here. Rust reads the connection as + * raw JSON and so still accepts them. The aliases are kept in the lookup order regardless, so that + * any of them the generated model does carry resolves in the same precedence as Rust. + */ +final class FoundryAuth { + + private static final String[] API_KEY_FIELDS = {"apiKey", "api_key"}; + private static final String[] BEARER_TOKEN_FIELDS = { + "bearerToken", "bearer_token", "apiKey", "api_key" + }; + + private FoundryAuth() {} + + /** An API key for Azure OpenAI key-authenticated operations. */ + static Optional apiKey(Connection connection) { + return apiKey(saved(connection)); + } + + /** As {@link #apiKey(Connection)}, but over a connection already in raw form. */ + static Optional apiKey(Map connection) { + return firstNonBlank(connection, API_KEY_FIELDS); + } + + /** + * A caller-supplied bearer token for Foundry operations. + * + *

Explicit token fields win. The API-key names stay in the list because hosts commonly carry + * an OAuth token in {@code apiKey}. + */ + static Optional bearerToken(Connection connection) { + return bearerToken(saved(connection)); + } + + /** + * As {@link #bearerToken(Connection)}, but over a connection already in raw form. + * + *

Callers holding raw JSON — model listing takes its connection as {@code Object} — reach the + * undeclared aliases this way, which is what Rust does everywhere. + */ + static Optional bearerToken(Map connection) { + return firstNonBlank(connection, BEARER_TOKEN_FIELDS); + } + + private static Map saved(Connection connection) { + return connection == null ? Map.of() : connection.save(new SaveContext()); + } + + private static Optional firstNonBlank(Map saved, String[] fields) { + if (saved == null) { + return Optional.empty(); + } + for (String field : fields) { + if (saved.get(field) instanceof String value) { + String trimmed = value.trim(); + if (!trimmed.isEmpty()) { + return Optional.of(trimmed); + } + } + } + return Optional.empty(); + } +} diff --git a/runtime/java/prompty-foundry/src/main/java/com/microsoft/prompty/foundry/FoundryExecutor.java b/runtime/java/prompty-foundry/src/main/java/com/microsoft/prompty/foundry/FoundryExecutor.java new file mode 100644 index 000000000..e20f8b799 --- /dev/null +++ b/runtime/java/prompty-foundry/src/main/java/com/microsoft/prompty/foundry/FoundryExecutor.java @@ -0,0 +1,203 @@ +package com.microsoft.prompty.foundry; + +import com.microsoft.prompty.Connections; +import com.microsoft.prompty.Environment; +import com.microsoft.prompty.InvokerException; +import com.microsoft.prompty.model.Connection; +import com.microsoft.prompty.model.ModelOptions; +import com.microsoft.prompty.model.Prompty; +import com.microsoft.prompty.openai.OpenAIExecutor; +import java.util.Map; + +/** + * Sends requests to Azure OpenAI and Foundry endpoints. + * + *

The wire format is OpenAI's, so this builds on {@link OpenAIExecutor} and changes only the two + * things Azure does differently: where the request goes and how it is authenticated. + * + *

Two connection shapes are supported. A {@code foundry} connection addresses the OpenAI/v1 + * surface directly and authenticates with a bearer token. Every other kind addresses a named + * deployment and authenticates with the {@code api-key} header, which is Azure's own scheme rather + * than {@code Authorization: Bearer}. + */ +public class FoundryExecutor extends OpenAIExecutor { + + /** The API version used when the prompt does not name one. */ + static final String DEFAULT_API_VERSION = "2025-04-01-preview"; + + private static final String FOUNDRY_KIND = "foundry"; + + @Override + protected String providerName() { + return "foundry"; + } + + @Override + protected String buildUrl(Prompty agent, String path) { + Connection connection = connection(agent); + String operation = azureOperation(path); + String endpoint = Connections.trimTrailingSlashes(endpointOf(agent, connection)); + + // A Foundry endpoint already points at the OpenAI/v1 surface, which routes by the model named + // in the body rather than by a deployment in the path. + if (isFoundry(connection)) { + return endpoint + "/" + operation; + } + return endpoint + + "/openai/deployments/" + + deploymentOf(agent) + + "/" + + operation + + "?api-version=" + + apiVersionOf(agent); + } + + @Override + protected Map authHeaders(Prompty agent) { + Connection connection = connection(agent); + + if (isFoundry(connection)) { + String token = + FoundryAuth.bearerToken(connection) + .or(() -> Environment.lookup("AZURE_INFERENCE_CREDENTIAL").filter(v -> !v.isEmpty())) + .orElseThrow( + () -> + InvokerException.execute( + "Foundry connection requires a bearer token. Set" + + " AZURE_INFERENCE_CREDENTIAL or configure a token on" + + " model.connection")); + return Map.of("Authorization", "Bearer " + token); + } + + String key = + FoundryAuth.apiKey(connection) + .or(() -> Environment.lookup("AZURE_OPENAI_API_KEY").filter(v -> !v.isEmpty())) + .orElseThrow( + () -> + InvokerException.execute( + "No Azure API key found. Set AZURE_OPENAI_API_KEY or configure" + + " model.connection.apiKey")); + // Azure authenticates with its own header rather than an Authorization bearer. + return Map.of("api-key", key); + } + + // -------------------------------------------------------------------- url + + /** + * The Azure path for an OpenAI operation. + * + *

The inherited executor decides which operations an apiType permits and hands the OpenAI path + * down; this maps the ones Azure serves and rejects the rest. Azure has no Responses surface, so + * a prompt asking for one is refused here rather than sent somewhere that would 404. + */ + private static String azureOperation(String path) { + return switch (path) { + case "/v1/chat/completions" -> "chat/completions"; + case "/v1/embeddings" -> "embeddings"; + case "/v1/images/generations" -> "images/generations"; + case "/v1/responses" -> throw InvokerException.execute( + "Unsupported apiType for Azure: responses"); + default -> throw InvokerException.execute("Unsupported apiType for Azure: " + path); + }; + } + + /** + * The endpoint to address: the prompt's connection, then the environment. + * + *

A Foundry project endpoint names a project rather than an inference surface, so it is + * rewritten before use. + */ + private static String endpointOf(Prompty agent, Connection connection) { + String endpoint = endpointOf(connection); + if (endpoint != null && !endpoint.isEmpty()) { + return isFoundry(connection) ? stripProjectPath(endpoint) : endpoint; + } + return Environment.lookup("AZURE_OPENAI_ENDPOINT") + .filter(value -> !value.isEmpty()) + .orElseThrow( + () -> + InvokerException.execute( + "No Azure OpenAI endpoint found. Set AZURE_OPENAI_ENDPOINT or configure" + + " model.connection.endpoint")); + } + + /** + * Rewrite a Foundry project endpoint as its OpenAI/v1 base URL. + * + *

A project endpoint looks like {@code https://resource.services.ai.azure.com/api/projects/p}, + * but inference is served from {@code https://resource.openai.azure.com/openai/v1}. Anything that + * does not look like a project endpoint is returned with only the project path removed, so an + * endpoint that already points at the inference surface is left alone. + */ + static String stripProjectPath(String endpoint) { + int projects = endpoint.indexOf("/api/projects"); + String base = + Connections.trimTrailingSlashes(projects >= 0 ? endpoint.substring(0, projects) : endpoint); + + int schemeEnd = base.indexOf("://"); + if (schemeEnd < 0) { + return base; + } + String scheme = base.substring(0, schemeEnd); + String rest = base.substring(schemeEnd + 3); + int slash = rest.indexOf('/'); + String authority = slash >= 0 ? rest.substring(0, slash) : rest; + + String host = authority; + String port = ""; + int colon = authority.lastIndexOf(':'); + if (colon >= 0 && isAllDigits(authority.substring(colon + 1))) { + host = authority.substring(0, colon); + port = authority.substring(colon); + } + + String suffix = ".services.ai.azure.com"; + if (host.endsWith(suffix)) { + host = host.substring(0, host.length() - suffix.length()) + ".openai.azure.com"; + } + return scheme + "://" + host + port + "/openai/v1"; + } + + private static boolean isAllDigits(String value) { + if (value.isEmpty()) { + return false; + } + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + if (c < '0' || c > '9') { + return false; + } + } + return true; + } + + /** The deployment to address: the prompt's model id, then the environment. */ + private static String deploymentOf(Prompty agent) { + String id = agent == null || agent.model == null ? null : agent.model.id; + if (id != null && !id.isEmpty()) { + return id; + } + return Environment.lookup("AZURE_OPENAI_DEPLOYMENT") + .filter(value -> !value.isEmpty()) + .orElseThrow( + () -> + InvokerException.execute( + "No deployment name found. Set model.id or AZURE_OPENAI_DEPLOYMENT")); + } + + /** The API version the prompt asked for, or the default. */ + private static String apiVersionOf(Prompty agent) { + ModelOptions options = agent == null || agent.model == null ? null : agent.model.options; + if (options != null + && options.additionalProperties != null + && options.additionalProperties.get("apiVersion") instanceof String version + && !version.isEmpty()) { + return version; + } + return DEFAULT_API_VERSION; + } + + private static boolean isFoundry(Connection connection) { + return connection != null && FOUNDRY_KIND.equals(connection.kind); + } +} diff --git a/runtime/java/prompty-foundry/src/main/java/com/microsoft/prompty/foundry/FoundryExtension.java b/runtime/java/prompty-foundry/src/main/java/com/microsoft/prompty/foundry/FoundryExtension.java new file mode 100644 index 000000000..b908bf727 --- /dev/null +++ b/runtime/java/prompty-foundry/src/main/java/com/microsoft/prompty/foundry/FoundryExtension.java @@ -0,0 +1,17 @@ +package com.microsoft.prompty.foundry; + +import com.microsoft.prompty.PromptyExtension; + +/** + * Registers the Foundry provider. + * + *

Discovered through {@code ServiceLoader}, so putting this module on the classpath is all it + * takes for {@code provider: foundry} prompts to run — no registration call in application code. + */ +public final class FoundryExtension implements PromptyExtension { + + @Override + public void register(Registrar registrar) { + registrar.executor("foundry", new FoundryExecutor()).processor("foundry", new FoundryProcessor()); + } +} diff --git a/runtime/java/prompty-foundry/src/main/java/com/microsoft/prompty/foundry/FoundryModelLister.java b/runtime/java/prompty-foundry/src/main/java/com/microsoft/prompty/foundry/FoundryModelLister.java new file mode 100644 index 000000000..2f33eec8b --- /dev/null +++ b/runtime/java/prompty-foundry/src/main/java/com/microsoft/prompty/foundry/FoundryModelLister.java @@ -0,0 +1,143 @@ +package com.microsoft.prompty.foundry; + +import com.microsoft.prompty.Environment; +import com.microsoft.prompty.Http; +import com.microsoft.prompty.InvokerException; +import com.microsoft.prompty.model.ModelInfo; +import com.microsoft.prompty.model.ModelLister; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Lists the models a Foundry or Azure OpenAI connection can reach. + * + *

The two connection kinds answer different questions, so they hit different services. A {@code + * foundry} project connection lists deployments, because a deployment name is what a user + * actually writes in {@code model.id} — the underlying model catalog is not directly invokable + * there. An Azure OpenAI {@code key} connection has no deployment sub-resource on its data plane, + * so it lists the lower-level model catalog instead. + * + *

The wire-to-{@link ModelInfo} mapping itself lives in {@link FoundryModels} and is exercised + * by the shared discovery vectors; this class is only the transport and dispatch around it. + */ +public final class FoundryModelLister implements ModelLister { + + private static final String PROVIDER = "foundry"; + + /** + * The catalog API version used when a connection does not pin one. + * + *

Kept identical to the Rust runtime so both report the same catalog for the same account. + */ + static final String DEFAULT_API_VERSION = "2025-04-01-preview"; + + @Override + public List listModels(Object connection) { + Map config = connection instanceof Map map ? map : Map.of(); + String kind = text(config.get("kind")); + return switch (kind) { + case "foundry" -> listDeployments(config); + case "key" -> listCatalog(config); + default -> + throw InvokerException.execute( + "Connection kind '" + + kind + + "' is not supported for Foundry model listing. Use 'foundry' for project" + + " deployments or 'key' for Azure OpenAI model catalogs."); + }; + } + + /** Call the project data plane's deployment list and map every entry it returns. */ + private static List listDeployments(Map connection) { + Object body = + Http.getJson( + PROVIDER, + deploymentsUrl(connection), + Map.of("Authorization", "Bearer " + deploymentToken(connection))); + return mapEntries(body, "value", FoundryModels::deploymentToModelInfo); + } + + /** Call the Azure OpenAI catalog and map every entry it returns. */ + private static List listCatalog(Map connection) { + Object body = + Http.getJson(PROVIDER, catalogUrl(connection), Map.of("api-key", catalogKey(connection))); + return mapEntries(body, "data", FoundryModels::catalogModelToModelInfo); + } + + static String deploymentsUrl(Map connection) { + String endpoint = trimTrailingSlashes(text(connection.get("endpoint"))); + if (endpoint.isEmpty()) { + throw InvokerException.execute( + "Foundry connection requires a non-empty endpoint to list deployments."); + } + return endpoint + "/deployments?api-version=v1"; + } + + static String catalogUrl(Map connection) { + String endpoint = text(connection.get("endpoint")); + if (endpoint.isEmpty()) { + endpoint = Environment.lookup("AZURE_OPENAI_ENDPOINT").orElse(""); + } + endpoint = trimTrailingSlashes(endpoint); + if (endpoint.isEmpty()) { + throw InvokerException.execute("Azure endpoint is required to list model catalog entries."); + } + + String apiVersion = text(connection.get("apiVersion")); + if (apiVersion.isEmpty()) { + apiVersion = DEFAULT_API_VERSION; + } + return endpoint + "/openai/models?api-version=" + apiVersion; + } + + /** + * The bearer token a deployment listing authenticates with. + * + *

A caller-supplied token wins. Failing that this would need an ambient Entra credential, + * which this runtime does not carry a dependency for — so it says so rather than sending an + * unauthenticated request and reporting whatever 401 comes back. + */ + static String deploymentToken(Map connection) { + return FoundryAuth.bearerToken(connection) + .orElseThrow( + () -> + InvokerException.execute( + "Foundry deployment listing requires a bearer token. Set connection.apiKey to" + + " an Entra ID token, or acquire one via the device code flow.")); + } + + static String catalogKey(Map connection) { + return FoundryAuth.apiKey(connection) + .or(() -> Environment.lookup("AZURE_OPENAI_API_KEY").filter(key -> !key.isBlank())) + .orElseThrow( + () -> + InvokerException.execute( + "Azure API key is required to list model catalog entries.")); + } + + private static List mapEntries( + Object body, String key, java.util.function.Function mapper) { + List models = new ArrayList<>(); + // A response missing the collection is an empty catalog, not a failure: a fresh project with no + // deployments legitimately answers this way. + if (body instanceof Map map && map.get(key) instanceof Iterable entries) { + for (Object entry : entries) { + models.add(mapper.apply(entry)); + } + } + return models; + } + + private static String trimTrailingSlashes(String value) { + String result = value; + while (result.endsWith("/")) { + result = result.substring(0, result.length() - 1); + } + return result; + } + + private static String text(Object value) { + return value instanceof String s ? s : ""; + } +} diff --git a/runtime/java/prompty-foundry/src/main/java/com/microsoft/prompty/foundry/FoundryModels.java b/runtime/java/prompty-foundry/src/main/java/com/microsoft/prompty/foundry/FoundryModels.java new file mode 100644 index 000000000..72d0ab215 --- /dev/null +++ b/runtime/java/prompty-foundry/src/main/java/com/microsoft/prompty/foundry/FoundryModels.java @@ -0,0 +1,189 @@ +package com.microsoft.prompty.foundry; + +import com.microsoft.prompty.Discovery; +import com.microsoft.prompty.model.ModelInfo; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Maps Foundry's model listings onto the provider-neutral contract. + * + *

Foundry answers from two different endpoints with two different shapes, so the mapping is split + * accordingly. Both are pure functions over a decoded payload, which is what lets the shared + * discovery vectors exercise them without a network or a token. + */ +public final class FoundryModels { + + private static final String PROVIDER = "foundry"; + + private FoundryModels() {} + + /** + * Map one deployment entry onto the provider-neutral contract. + * + *

Two shapes reach this method. The data plane ({@code /deployments?api-version=v1}) returns a + * flat object with {@code modelName}, {@code modelPublisher} and top-level {@code capabilities}; + * the ARM management plane nests the same facts under {@code properties.model}. Rather than ask + * callers to tell the two apart, each field is looked for in every place it is known to appear, + * flat form first. + */ + public static ModelInfo deploymentToModelInfo(Object raw) { + ModelInfo info = new ModelInfo(); + if (!(raw instanceof Map map)) { + return info; + } + + Map properties = asMap(map.get("properties")); + Map model = asMap(properties.get("model")); + // The first capability block that is *present* wins, even when it is empty — an endpoint that + // deliberately reports no capabilities should not have a sibling block substituted for it. + Map capabilities = Map.of(); + for (Map candidate : List.of(properties, model, map)) { + if (candidate.containsKey("capabilities")) { + capabilities = asMap(candidate.get("capabilities")); + break; + } + } + + // The id is the one field a caller cannot do without, so an entry that names none still round + // trips as an empty string rather than vanishing from the saved shape. + info.id = firstString(string(map.get("name")), ""); + info.displayName = firstString(string(map.get("modelName")), string(model.get("name"))); + // Everything on this endpoint is served by Azure, so an entry that names no publisher is still + // attributable; leaving it null would lose that. + String publisher = + firstString(string(map.get("modelPublisher")), string(model.get("publisher"))); + info.ownedBy = publisher == null ? "azure" : publisher; + info.contextWindow = + firstInt( + integer(capabilities, "maxContextLength", "contextWindow", "context_length"), + integer(model, "maxContextLength"), + integer(map, "maxContextLength")); + info.inputModalities = + strings(capabilities, "inputModalities", "input_modalities", "supportedInputModalities"); + info.outputModalities = + strings(capabilities, "outputModalities", "output_modalities", "supportedOutputModalities"); + info.additionalProperties = copy(map); + + Discovery.enrich(PROVIDER, info); + return info; + } + + /** + * Map one Azure OpenAI model-catalog entry onto the provider-neutral contract. + * + *

The catalog describes models rather than deployments, so it carries neither a display name + * nor modality information; those are left for the shared dataset to fill. + */ + public static ModelInfo catalogModelToModelInfo(Object raw) { + ModelInfo info = new ModelInfo(); + if (!(raw instanceof Map map)) { + return info; + } + info.id = firstString(string(map.get("id")), ""); + info.ownedBy = string(map.get("owned_by")); + info.contextWindow = integer(map, "maxContextLength"); + info.additionalProperties = copy(map); + + Discovery.enrich(PROVIDER, info); + return info; + } + + /** + * Read an integer from the first of {@code keys} that carries one. + * + *

Capability values arrive as numbers from ARM and as strings from the data plane, so both are + * accepted. + */ + private static Integer integer(Map source, String... keys) { + for (String key : keys) { + Object value = source.get(key); + if (value instanceof Number number) { + return number.intValue(); + } + if (value instanceof String text) { + try { + return Integer.valueOf(text.trim()); + } catch (NumberFormatException ignored) { + // Not a number after all; keep looking under the remaining keys. + } + } + } + return null; + } + + /** + * Read a string list from the first of {@code keys} that carries one. + * + *

Modalities arrive as a JSON array from ARM and as a comma-separated string from the data + * plane, so both are accepted. + */ + private static List strings(Map source, String... keys) { + for (String key : keys) { + Object value = source.get(key); + if (value instanceof Iterable items) { + List result = new ArrayList<>(); + for (Object item : items) { + if (item instanceof String text) { + result.add(text); + } + } + return result; + } + if (value instanceof String text) { + List result = new ArrayList<>(); + for (String part : text.split(",")) { + String trimmed = part.trim(); + if (!trimmed.isEmpty()) { + result.add(trimmed); + } + } + return result; + } + } + return null; + } + + private static Map asMap(Object value) { + return value instanceof Map map ? map : Map.of(); + } + + /** + * A string member, kept verbatim. + * + *

An empty string is deliberately preserved rather than folded into {@code null}: the + * reference runtime distinguishes "the endpoint sent an empty publisher" from "the endpoint sent + * no publisher at all", and only the latter earns the {@code azure} fallback. + */ + private static String string(Object value) { + return value instanceof String text ? text : null; + } + + private static String firstString(String... values) { + for (String value : values) { + if (value != null) { + return value; + } + } + return null; + } + + private static Integer firstInt(Integer... values) { + for (Integer value : values) { + if (value != null) { + return value; + } + } + return null; + } + + private static Map copy(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } +} diff --git a/runtime/java/prompty-foundry/src/main/java/com/microsoft/prompty/foundry/FoundryOAuth.java b/runtime/java/prompty-foundry/src/main/java/com/microsoft/prompty/foundry/FoundryOAuth.java new file mode 100644 index 000000000..25e482c1b --- /dev/null +++ b/runtime/java/prompty-foundry/src/main/java/com/microsoft/prompty/foundry/FoundryOAuth.java @@ -0,0 +1,420 @@ +package com.microsoft.prompty.foundry; + +import com.microsoft.prompty.Http; +import com.microsoft.prompty.InvokerException; +import com.microsoft.prompty.model.AuthorizationCodeFlow; +import com.microsoft.prompty.model.DeviceAuthorization; +import com.microsoft.prompty.model.LoadContext; +import com.microsoft.prompty.model.OAuthToken; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Base64; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Interactive Azure sign-in flows: device authorization, authorization code with PKCE, and refresh. + * + *

{@link FoundryAuth} covers the non-interactive credentials — an API key, a bearer token from + * the environment. Those assume a credential already exists. This class is how one is obtained when + * a human has to approve it, which is the usual case for a developer running locally against their + * own Azure tenant. + * + *

What this owns, and what it does not. This is the protocol only: PKCE generation, + * authorize-URL construction, the device-code request and poll state machine, code exchange, and + * refresh. Opening a browser, binding a loopback listener to receive the redirect, serving the + * post-redirect page, and storing the resulting tokens are all host concerns. Keeping the split here + * means the same protocol serves a CLI, an editor extension, and a test without any of them + * inheriting the others' assumptions about how a user is present. + * + *

Endpoints, scopes, and the default client id are Azure-concrete deliberately: interactive OAuth + * has exactly one provider today. If a second ever appears, lift them into a configuration value + * rather than generalising speculatively now. + */ +public final class FoundryOAuth { + + /** The Azure CLI public client id, used when the caller supplies none. */ + public static final String DEFAULT_CLIENT_ID = "1950a258-227b-4e31-a9cf-717495945fc2"; + + /** + * Default scope for Foundry and Azure OpenAI access. + * + *

{@code offline_access} is included so the response carries a refresh token; without it the + * user would have to sign in again the moment the access token expires. + */ + public static final String AZURE_OPENAI_SCOPE = "https://ai.azure.com/.default offline_access"; + + /** Default scope for Azure Resource Manager access, used by {@link FoundryArm}. */ + public static final String AZURE_MANAGEMENT_SCOPE = + "https://management.azure.com/.default offline_access"; + + /** Tenant used when the caller supplies an empty one. */ + static final String DEFAULT_TENANT = "organizations"; + + /** RFC 8628 mandates a poll interval of at least five seconds. */ + static final long MIN_POLL_INTERVAL_SECONDS = 5; + + /** RFC 7636 allows 43..128; 64 is comfortably inside that and a round number of bytes to read. */ + static final int PKCE_VERIFIER_LENGTH = 64; + + private static final String UNRESERVED = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"; + + private static final SecureRandom RANDOM = new SecureRandom(); + + private static final String PROVIDER = "azure-oauth"; + + private FoundryOAuth() {} + + /** + * Build the authorize URL for an authorization-code flow with PKCE. + * + *

Performs no I/O. The returned value carries both the URL to send the user to and the verifier + * that must be presented later at {@link #exchangeCodeForToken}; they are generated together + * because a challenge is meaningless without the verifier it was derived from. + * + * @param tenantId the directory to sign in against; empty means {@value #DEFAULT_TENANT} + * @param clientId the application id, or {@code null} for {@value #DEFAULT_CLIENT_ID} + * @param scope the requested scope, or {@code null} for {@link #AZURE_OPENAI_SCOPE} + * @param redirectUri where the provider should return the user, chosen by the host + */ + public static AuthorizationCodeFlow buildAuthCodeUrl( + String tenantId, String clientId, String scope, String redirectUri) { + Pkce pkce = generatePkce(); + + Map query = new LinkedHashMap<>(); + query.put("client_id", clientIdOrDefault(clientId)); + query.put("response_type", "code"); + query.put("redirect_uri", redirectUri); + query.put("response_mode", "query"); + query.put("scope", scopeOrDefault(scope)); + query.put("code_challenge", pkce.challenge()); + query.put("code_challenge_method", "S256"); + + AuthorizationCodeFlow flow = new AuthorizationCodeFlow(); + flow.authUrl = authorizeUrl(tenantId) + "?" + Http.encodeForm(query); + flow.codeVerifier = pkce.verifier(); + return flow; + } + + /** + * Request a device authorization code (RFC 8628 §3.1). + * + *

The returned value carries the code to display to the user and the interval the provider + * wants between polls; feed both to {@link #pollForToken}. + */ + public static DeviceAuthorization requestDeviceCode( + String tenantId, String clientId, String scope) { + Map form = new LinkedHashMap<>(); + form.put("client_id", clientIdOrDefault(clientId)); + form.put("scope", scopeOrDefault(scope)); + + Http.FormResult result = Http.postForm(PROVIDER, deviceCodeUrl(tenantId), form); + if (!result.isSuccess()) { + throw InvokerException.execute( + "device code request failed (HTTP " + result.status() + "): " + result.body()); + } + return parseDeviceAuthorization(result.body()); + } + + /** + * Poll the token endpoint until the user approves the device or the flow times out (RFC 8628 + * §3.4–3.5). + * + *

The interval is floored at {@value #MIN_POLL_INTERVAL_SECONDS} seconds and grows by five more + * whenever the provider answers {@code slow_down}; polling faster than asked risks being throttled + * outright. The scope is deliberately not resent — it was fixed when the device code was issued. + */ + public static OAuthToken pollForToken( + String tenantId, String deviceCode, long intervalSeconds, long timeoutSeconds, String clientId) { + return pollForToken( + tenantId, + deviceCode, + intervalSeconds, + timeoutSeconds, + clientId, + (url, form) -> Http.postForm(PROVIDER, url, form), + FoundryOAuth::sleepSeconds, + System::nanoTime); + } + + /** + * The poll loop with its transport, sleep, and clock supplied. + * + *

Split out so the state machine — the pending/slow-down/expired branches and the interval + * floor — can be tested in microseconds instead of minutes, and without reaching Azure. + */ + static OAuthToken pollForToken( + String tenantId, + String deviceCode, + long intervalSeconds, + long timeoutSeconds, + String clientId, + TokenEndpoint endpoint, + Sleeper sleeper, + Clock clock) { + long interval = Math.max(intervalSeconds, MIN_POLL_INTERVAL_SECONDS); + long deadline = clock.nanoTime() + timeoutSeconds * 1_000_000_000L; + String resolvedClientId = clientIdOrDefault(clientId); + String url = tokenUrl(tenantId); + + while (true) { + if (clock.nanoTime() >= deadline) { + throw InvokerException.execute("device code authorization timed out"); + } + sleeper.sleep(interval); + + Map form = new LinkedHashMap<>(); + form.put("client_id", resolvedClientId); + form.put("grant_type", "urn:ietf:params:oauth:grant-type:device_code"); + form.put("device_code", deviceCode); + + Http.FormResult result = endpoint.post(url, form); + if (result.isSuccess()) { + return parseToken(result.body()); + } + + String error = errorCode(result); + switch (error) { + case "authorization_pending" -> { + // The user simply has not finished yet; this is the expected steady state. + } + case "slow_down" -> interval += 5; + case "expired_token" -> + throw InvokerException.execute("device code expired before authorization"); + default -> throw InvokerException.execute("device code authorization failed: " + error); + } + } + } + + /** Exchange an authorization code for a token (RFC 6749 §4.1.3, with PKCE). */ + public static OAuthToken exchangeCodeForToken( + String tenantId, + String code, + String redirectUri, + String codeVerifier, + String clientId, + String scope) { + Map form = new LinkedHashMap<>(); + form.put("client_id", clientIdOrDefault(clientId)); + form.put("grant_type", "authorization_code"); + form.put("code", code); + form.put("redirect_uri", redirectUri); + form.put("code_verifier", codeVerifier); + form.put("scope", scopeOrDefault(scope)); + return postToken(tenantId, form); + } + + /** Exchange a refresh token for a fresh access token (RFC 6749 §6). */ + public static OAuthToken refreshToken( + String tenantId, String refreshToken, String clientId, String scope) { + Map form = new LinkedHashMap<>(); + form.put("client_id", clientIdOrDefault(clientId)); + form.put("grant_type", "refresh_token"); + form.put("refresh_token", refreshToken); + form.put("scope", scopeOrDefault(scope)); + return postToken(tenantId, form); + } + + private static OAuthToken postToken(String tenantId, Map form) { + Http.FormResult result = Http.postForm(PROVIDER, tokenUrl(tenantId), form); + if (!result.isSuccess()) { + throw InvokerException.execute( + "token request failed (HTTP " + result.status() + "): " + result.body()); + } + return parseToken(result.body()); + } + + /** Read the OAuth error code from a failed token response (RFC 6749 §5.2). */ + private static String errorCode(Http.FormResult result) { + // The body is reported only by way of the parser's complaint, never verbatim. A token endpoint + // answers a failed poll with an OAuth error rather than a credential, so this is not a leak + // either way — but echoing a whole response body into an exception is a habit worth not + // forming on a code path that handles tokens. + Object parsed; + try { + parsed = com.microsoft.prompty.model.TypraJson.parse(result.body()); + } catch (RuntimeException e) { + throw parseFailure(result.status(), e.getMessage()); + } + if (parsed instanceof Map map && map.get("error") instanceof String code) { + return code; + } + // Parsed cleanly but carries no error code: the service has given a final answer this code + // cannot act on. Continuing would poll until the deadline against a request that will never + // succeed, so it is surfaced instead. + throw parseFailure(result.status(), "missing field `error`"); + } + + private static InvokerException parseFailure(int status, String reason) { + return InvokerException.execute( + "failed to parse token error response (HTTP " + status + "): " + reason); + } + + // --------------------------------------------------------------------------------------------- + // Response parsing + // --------------------------------------------------------------------------------------------- + + /** + * The OAuth wire is snake_case; the generated model is camelCase. + * + *

Renaming through a load hook rather than a hand-written mapper means the model stays the + * single source of truth for what these values are — a field added upstream arrives here without + * this class changing. + */ + private static LoadContext wireContext() { + return new LoadContext( + value -> { + if (!(value instanceof Map source)) { + return value; + } + Map renamed = new LinkedHashMap<>(); + source.forEach((key, item) -> renamed.put(String.valueOf(key), item)); + for (String[] pair : + new String[][] { + {"access_token", "accessToken"}, + {"token_type", "tokenType"}, + {"expires_in", "expiresIn"}, + {"refresh_token", "refreshToken"}, + {"device_code", "deviceCode"}, + {"user_code", "userCode"}, + {"verification_uri", "verificationUri"}, + }) { + if (renamed.containsKey(pair[0])) { + renamed.put(pair[1], renamed.remove(pair[0])); + } + } + return renamed; + }, + null); + } + + static OAuthToken parseToken(String body) { + Map value = asObject(body); + requireString(value, "access_token"); + requireString(value, "token_type"); + requireNonNegative(value, "expires_in"); + return OAuthToken.load(value, wireContext()); + } + + static DeviceAuthorization parseDeviceAuthorization(String body) { + Map value = asObject(body); + requireString(value, "device_code"); + requireString(value, "user_code"); + requireString(value, "verification_uri"); + requireNonNegative(value, "expires_in"); + requireNonNegative(value, "interval"); + return DeviceAuthorization.load(value, wireContext()); + } + + @SuppressWarnings("unchecked") + private static Map asObject(String body) { + Object parsed; + try { + parsed = com.microsoft.prompty.model.TypraJson.parse(body); + } catch (RuntimeException e) { + throw InvokerException.execute("failed to parse response: " + e.getMessage(), e); + } + if (!(parsed instanceof Map)) { + throw InvokerException.execute("failed to parse response: expected a JSON object"); + } + return (Map) parsed; + } + + private static void requireString(Map value, String field) { + if (!(value.get(field) instanceof String text) || text.isEmpty()) { + throw InvokerException.execute( + "missing or invalid field '" + field + "'; expected a non-empty string"); + } + } + + private static void requireNonNegative(Map value, String field) { + if (!(value.get(field) instanceof Number number) || number.longValue() < 0) { + throw InvokerException.execute( + "missing or invalid field '" + field + "'; expected a non-negative integer"); + } + } + + // --------------------------------------------------------------------------------------------- + // PKCE + // --------------------------------------------------------------------------------------------- + + /** A PKCE verifier and the S256 challenge derived from it (RFC 7636). */ + record Pkce(String verifier, String challenge) {} + + static Pkce generatePkce() { + StringBuilder verifier = new StringBuilder(PKCE_VERIFIER_LENGTH); + for (int i = 0; i < PKCE_VERIFIER_LENGTH; i++) { + verifier.append(UNRESERVED.charAt(RANDOM.nextInt(UNRESERVED.length()))); + } + return new Pkce(verifier.toString(), challengeFor(verifier.toString())); + } + + static String challengeFor(String verifier) { + try { + byte[] digest = + MessageDigest.getInstance("SHA-256").digest(verifier.getBytes(StandardCharsets.US_ASCII)); + return Base64.getUrlEncoder().withoutPadding().encodeToString(digest); + } catch (NoSuchAlgorithmException e) { + // SHA-256 is required of every Java platform, so this cannot happen on a conforming runtime. + throw new IllegalStateException("SHA-256 is unavailable", e); + } + } + + // --------------------------------------------------------------------------------------------- + // URLs and defaults + // --------------------------------------------------------------------------------------------- + + static String tenantOrDefault(String tenantId) { + return tenantId == null || tenantId.isEmpty() ? DEFAULT_TENANT : tenantId; + } + + private static String clientIdOrDefault(String clientId) { + return clientId == null || clientId.isEmpty() ? DEFAULT_CLIENT_ID : clientId; + } + + private static String scopeOrDefault(String scope) { + return scope == null || scope.isEmpty() ? AZURE_OPENAI_SCOPE : scope; + } + + static String deviceCodeUrl(String tenantId) { + return "https://login.microsoftonline.com/" + tenantOrDefault(tenantId) + "/oauth2/v2.0/devicecode"; + } + + static String tokenUrl(String tenantId) { + return "https://login.microsoftonline.com/" + tenantOrDefault(tenantId) + "/oauth2/v2.0/token"; + } + + static String authorizeUrl(String tenantId) { + return "https://login.microsoftonline.com/" + tenantOrDefault(tenantId) + "/oauth2/v2.0/authorize"; + } + + private static void sleepSeconds(long seconds) { + try { + Thread.sleep(seconds * 1000L); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw InvokerException.cancelled("Interrupted while awaiting device code authorization"); + } + } + + /** The token endpoint, as the poll loop sees it. */ + @FunctionalInterface + interface TokenEndpoint { + Http.FormResult post(String url, Map form); + } + + /** The delay between polls, as the poll loop sees it. */ + @FunctionalInterface + interface Sleeper { + void sleep(long seconds); + } + + /** The passage of time, as the poll loop sees it. */ + @FunctionalInterface + interface Clock { + long nanoTime(); + } +} diff --git a/runtime/java/prompty-foundry/src/main/java/com/microsoft/prompty/foundry/FoundryProcessor.java b/runtime/java/prompty-foundry/src/main/java/com/microsoft/prompty/foundry/FoundryProcessor.java new file mode 100644 index 000000000..d3a5eb7a6 --- /dev/null +++ b/runtime/java/prompty-foundry/src/main/java/com/microsoft/prompty/foundry/FoundryProcessor.java @@ -0,0 +1,24 @@ +package com.microsoft.prompty.foundry; + +import com.microsoft.prompty.openai.OpenAIProcessor; + +/** + * Reads Azure OpenAI and Foundry responses. + * + *

Azure returns OpenAI's response shape, so all of the reading is inherited. Only the provider + * name changes, and the fact that none of Azure's endpoints hand back a handle that would let a + * later request resume model-visible state — so the conversation is retained as explicitly portable + * context instead of a continuation the provider could not honour. + */ +public class FoundryProcessor extends OpenAIProcessor { + + @Override + protected String providerName() { + return "foundry"; + } + + @Override + protected boolean supportsResponsesContinuation() { + return false; + } +} diff --git a/runtime/java/prompty-foundry/src/main/resources/META-INF/services/com.microsoft.prompty.PromptyExtension b/runtime/java/prompty-foundry/src/main/resources/META-INF/services/com.microsoft.prompty.PromptyExtension new file mode 100644 index 000000000..38a6baed6 --- /dev/null +++ b/runtime/java/prompty-foundry/src/main/resources/META-INF/services/com.microsoft.prompty.PromptyExtension @@ -0,0 +1 @@ +com.microsoft.prompty.foundry.FoundryExtension diff --git a/runtime/java/prompty-foundry/src/test/java/com/microsoft/prompty/foundry/DiscoveryVectorsTest.java b/runtime/java/prompty-foundry/src/test/java/com/microsoft/prompty/foundry/DiscoveryVectorsTest.java new file mode 100644 index 000000000..329818201 --- /dev/null +++ b/runtime/java/prompty-foundry/src/test/java/com/microsoft/prompty/foundry/DiscoveryVectorsTest.java @@ -0,0 +1,46 @@ +package com.microsoft.prompty.foundry; + +import com.microsoft.prompty.SpecVectors; +import com.microsoft.prompty.model.ModelInfo; +import com.microsoft.prompty.model.SaveContext; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; + +/** + * Grades the Foundry half of the shared discovery suite. + * + *

Foundry answers from two endpoints, so each vector names the shape it carries and is routed to + * the matching mapper. + */ +class DiscoveryVectorsTest { + + @TestFactory + Iterable discoveryVectors() { + List tests = new ArrayList<>(); + for (Map vector : SpecVectors.readCases("discovery/discovery_vectors.json", "vectors")) { + if (!"foundry".equals(vector.get("provider"))) { + continue; + } + String name = SpecVectors.string(vector, "name"); + String shape = SpecVectors.string(vector, "shape"); + tests.add( + DynamicTest.dynamicTest( + name, + () -> { + Map input = SpecVectors.map(vector, "input"); + ModelInfo actual = + switch (shape) { + case "deployment" -> FoundryModels.deploymentToModelInfo(input); + case "catalog" -> FoundryModels.catalogModelToModelInfo(input); + default -> throw new AssertionError("Unknown shape: " + shape); + }; + SpecVectors.assertEquivalent( + name, vector.get("expected"), actual.save(new SaveContext())); + })); + } + return tests; + } +} diff --git a/runtime/java/prompty-foundry/src/test/java/com/microsoft/prompty/foundry/FoundryArmTest.java b/runtime/java/prompty-foundry/src/test/java/com/microsoft/prompty/foundry/FoundryArmTest.java new file mode 100644 index 000000000..f89e34e83 --- /dev/null +++ b/runtime/java/prompty-foundry/src/test/java/com/microsoft/prompty/foundry/FoundryArmTest.java @@ -0,0 +1,490 @@ +package com.microsoft.prompty.foundry; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.microsoft.prompty.model.AiResourceInfo; +import com.microsoft.prompty.model.ProjectInfo; +import com.microsoft.prompty.model.SubscriptionInfo; +import com.microsoft.prompty.model.TypraJson; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +/** + * Covers the ARM discovery mappers: which resources are offerable, which endpoint a caller should + * use, and how a project's identity is derived from an ARM payload. + * + *

Two things are covered: the paging loop, driven through an injected transport so a multi-page + * response can be exercised without a control plane, and the parsing that turns ARM's payloads into + * the provider-neutral model — which is where the decisions that can silently produce an unusable + * picker entry actually live. + */ +@DisplayName("Foundry ARM discovery") +final class FoundryArmTest { + + @Nested + @DisplayName("subscriptions") + class Subscriptions { + + @Test + void anEnabledSubscriptionIsKept() { + SubscriptionInfo subscription = + FoundryArm.parseSubscription( + json("{\"subscriptionId\":\"sub-1\",\"displayName\":\"Prod\",\"state\":\"Enabled\"}")); + assertNotNull(subscription); + assertEquals("sub-1", subscription.subscriptionId); + assertEquals("Prod", subscription.displayName); + assertEquals("Enabled", subscription.state); + } + + @Test + void aDisabledSubscriptionIsDropped() { + // Offering a disabled subscription would produce a picker entry every later call rejects. + assertNull( + FoundryArm.parseSubscription( + json("{\"subscriptionId\":\"sub-2\",\"displayName\":\"Old\",\"state\":\"Disabled\"}"))); + } + + @Test + void aSubscriptionWithNoStateIsDropped() { + assertNull(FoundryArm.parseSubscription(json("{\"subscriptionId\":\"sub-3\"}"))); + } + } + + @Nested + @DisplayName("AI resources") + class AiResources { + + @Test + void theNamedInferenceEndpointWins() { + AiResourceInfo resource = + FoundryArm.parseAiResource( + json( + """ + { + "name": "myaccount", + "kind": "AIServices", + "location": "eastus", + "id": "/subscriptions/s/resourceGroups/my-rg/providers/x/accounts/myaccount", + "properties": { + "endpoint": "https://generic.example", + "endpoints": { + "OpenAI Language Model Instance API": "https://preferred.example" + } + } + } + """)); + assertNotNull(resource); + // An AI Services account publishes several endpoints and only this one speaks the OpenAI API. + assertEquals("https://preferred.example", resource.endpoint); + assertEquals("my-rg", resource.resourceGroup); + assertEquals("eastus", resource.location); + assertEquals("https://myaccount.services.ai.azure.com", resource.serviceUrl); + } + + @Test + void theGenericEndpointIsTheFallback() { + AiResourceInfo resource = + FoundryArm.parseAiResource( + json( + "{\"name\":\"acct\",\"kind\":\"OpenAI\",\"id\":\"\"," + + "\"properties\":{\"endpoint\":\"https://generic.example\"}}")); + assertNotNull(resource); + assertEquals("https://generic.example", resource.endpoint); + } + + @Test + void anEmptyPreferredEndpointFallsThroughRatherThanWinning() { + // Present-but-empty is the case that would otherwise produce a resource with no endpoint. + AiResourceInfo resource = + FoundryArm.parseAiResource( + json( + "{\"name\":\"acct\",\"kind\":\"OpenAI\",\"id\":\"\",\"properties\":{" + + "\"endpoint\":\"https://generic.example\"," + + "\"endpoints\":{\"OpenAI Language Model Instance API\":\"\"}}}")); + assertNotNull(resource); + assertEquals("https://generic.example", resource.endpoint); + } + + @Test + void aResourceWithNoEndpointAtAllIsDropped() { + assertNull( + FoundryArm.parseAiResource( + json("{\"name\":\"acct\",\"kind\":\"OpenAI\",\"id\":\"\",\"properties\":{}}"))); + } + + @Test + void anUnrelatedKindIsDropped() { + // Cognitive Services hosts speech, vision, and more; none of them accept a chat completion. + assertNull( + FoundryArm.parseAiResource( + json( + "{\"name\":\"speech\",\"kind\":\"SpeechServices\",\"id\":\"\"," + + "\"properties\":{\"endpoint\":\"https://speech.example\"}}"))); + } + + @Test + void onlyAiServicesAccountsGetAProjectStyleHost() { + AiResourceInfo openAi = + FoundryArm.parseAiResource( + json( + "{\"name\":\"acct\",\"kind\":\"OpenAI\",\"id\":\"\"," + + "\"properties\":{\"endpoint\":\"https://e.example\"}}")); + assertNotNull(openAi); + // A classic Azure OpenAI account has no services.ai alias, so inventing one would 404. + assertNull(openAi.serviceUrl); + } + + @Test + void theResourceGroupIsReadCaseInsensitively() { + // ARM writes this segment both ways and they name the same thing. + assertEquals( + "my-rg", + FoundryArm.extractResourceGroup( + "/subscriptions/s/resourcegroups/my-rg/providers/x/accounts/a")); + assertEquals( + "my-rg", + FoundryArm.extractResourceGroup( + "/subscriptions/s/resourceGroups/my-rg/providers/x/accounts/a")); + } + + @Test + void anIdWithNoResourceGroupYieldsEmpty() { + assertEquals("", FoundryArm.extractResourceGroup("/subscriptions/s")); + } + + @Test + void aTrailingResourceGroupsSegmentWithNoNameYieldsEmpty() { + // Guards the index-plus-one read against running off the end of the id. + assertEquals("", FoundryArm.extractResourceGroup("/subscriptions/s/resourceGroups")); + } + } + + @Nested + @DisplayName("projects") + class Projects { + + @Test + void anAbsentDisplayNameFallsBackButAnEmptyOneIsKept() { + // These two cases look alike after the usual "missing or blank" flattening, and they are not + // alike: only one of them is the author saying something. Rust distinguishes them, so a + // project rendered from the same ARM payload must read the same in both runtimes. + ProjectInfo absent = + FoundryArm.parseModernProject(json("{\"name\":\"acct/proj\",\"properties\":{}}"), "acct"); + assertEquals("proj", absent.displayName); + + ProjectInfo empty = + FoundryArm.parseModernProject( + json("{\"name\":\"acct/proj\",\"properties\":{\"displayName\":\"\"}}"), "acct"); + assertEquals("", empty.displayName); + } + + @Test + void anAbsentFriendlyNameFallsBackButAnEmptyOneIsKept() { + ProjectInfo absent = + FoundryArm.parseClassicWorkspace( + json("{\"kind\":\"Project\",\"name\":\"ws\",\"properties\":{}}"), "acct"); + assertEquals("ws", absent.displayName); + + ProjectInfo empty = + FoundryArm.parseClassicWorkspace( + json("{\"kind\":\"Project\",\"name\":\"ws\",\"properties\":{\"friendlyName\":\"\"}}"), + "acct"); + assertEquals("", empty.displayName); + } + + @Test + void aModernProjectKeepsOnlyTheChildSegmentOfItsName() { ProjectInfo project = + FoundryArm.parseModernProject( + json("{\"name\":\"myaccount/myproject\",\"properties\":{\"displayName\":\"My Project\"}}"), + "myaccount"); + // ARM names a child resource "parent/child"; a picker showing the pair would be nonsense, and + // the endpoint built from it would be wrong. + assertEquals("myproject", project.name); + assertEquals("My Project", project.displayName); + assertEquals( + "https://myaccount.services.ai.azure.com/api/projects/myproject", project.endpoint); + } + + @Test + void aModernProjectWithoutADisplayNameFallsBackToItsName() { + ProjectInfo project = + FoundryArm.parseModernProject(json("{\"name\":\"acct/proj\"}"), "acct"); + assertEquals("proj", project.displayName); + } + + @Test + void anUnqualifiedModernProjectNameIsUsedAsIs() { + ProjectInfo project = FoundryArm.parseModernProject(json("{\"name\":\"proj\"}"), "acct"); + assertEquals("proj", project.name); + } + + @Test + void aClassicWorkspaceOfKindProjectBecomesAProject() { + ProjectInfo project = + FoundryArm.parseClassicWorkspace( + json("{\"name\":\"ws\",\"kind\":\"Project\",\"properties\":{\"friendlyName\":\"Friendly\"}}"), + "acct"); + assertNotNull(project); + assertEquals("ws", project.name); + assertEquals("Friendly", project.displayName); + assertEquals("https://acct.services.ai.azure.com/api/projects/ws", project.endpoint); + } + + @Test + void aClassicWorkspaceOfAnotherKindIsDropped() { + // The workspaces endpoint also returns hubs and plain ML workspaces, which are not projects. + assertNull( + FoundryArm.parseClassicWorkspace(json("{\"name\":\"hub\",\"kind\":\"Hub\"}"), "acct")); + } + + @Test + void aClassicWorkspaceWithoutAFriendlyNameFallsBackToItsName() { + ProjectInfo project = + FoundryArm.parseClassicWorkspace(json("{\"name\":\"ws\",\"kind\":\"Project\"}"), "acct"); + assertNotNull(project); + assertEquals("ws", project.displayName); + } + + @Test + void bothProjectShapesBuildTheSameEndpointForm() { + // The two ARM shapes are an implementation detail of how a project was created; a caller must + // not be able to tell them apart from the endpoint it is handed. + ProjectInfo modern = FoundryArm.parseModernProject(json("{\"name\":\"acct/p\"}"), "acct"); + ProjectInfo classic = + FoundryArm.parseClassicWorkspace(json("{\"name\":\"p\",\"kind\":\"Project\"}"), "acct"); + assertNotNull(classic); + assertEquals(modern.endpoint, classic.endpoint); + } + } + + @Nested + @DisplayName("model shape") + class ModelShape { + + @Test + void resultsRoundTripThroughTheGeneratedModel() { + // These values cross a process boundary as the generated model, so a field the mappers set but + // the model does not persist would silently vanish on the way to a host. + AiResourceInfo resource = + FoundryArm.parseAiResource( + json( + "{\"name\":\"acct\",\"kind\":\"AIServices\",\"location\":\"westus\"," + + "\"id\":\"/subscriptions/s/resourceGroups/rg/x\"," + + "\"properties\":{\"endpoint\":\"https://e.example\"}}")); + assertNotNull(resource); + Map saved = + resource.save(new com.microsoft.prompty.model.SaveContext()); + assertEquals("acct", saved.get("name")); + assertEquals("rg", saved.get("resourceGroup")); + assertTrue( + String.valueOf(saved.get("serviceUrl")).contains("services.ai.azure.com"), + "the project-style host should survive the round trip: " + saved); + } + } + + @Nested + @DisplayName("paging") + class Paging { + + /** Records the URLs asked for and replays a scripted page per call. */ + private static final class ScriptedPages implements FoundryArm.PageEndpoint { + private final List requested = new ArrayList<>(); + private final Deque pages = new ArrayDeque<>(); + + ScriptedPages(String... bodies) { + for (String body : bodies) { + pages.add(body); + } + } + + @Override + public Object get(String url) { + requested.add(url); + if (pages.isEmpty()) { + throw new IllegalStateException("asked for an unscripted page: " + url); + } + return TypraJson.parse(pages.removeFirst()); + } + } + + @Test + void aSinglePageIsReturnedWhole() { + ScriptedPages pages = new ScriptedPages("{\"value\":[{\"name\":\"a\"},{\"name\":\"b\"}]}"); + List> items = FoundryArm.fetchAll(pages, "https://arm/first"); + + assertEquals(2, items.size()); + assertEquals("a", items.get(0).get("name")); + assertEquals(List.of("https://arm/first"), pages.requested); + } + + @Test + void nextLinkIsFollowedAndResultsAccumulateInOrder() { + // The whole point of the loop: a caller must not have to know a response was split. + ScriptedPages pages = + new ScriptedPages( + "{\"value\":[{\"name\":\"a\"}],\"nextLink\":\"https://arm/p2\"}", + "{\"value\":[{\"name\":\"b\"}],\"nextLink\":\"https://arm/p3\"}", + "{\"value\":[{\"name\":\"c\"}]}"); + + List> items = FoundryArm.fetchAll(pages, "https://arm/p1"); + + assertEquals( + List.of("a", "b", "c"), items.stream().map(item -> item.get("name")).toList()); + // ARM hands back absolute URLs, so each page must dictate the next request verbatim. + assertEquals( + List.of("https://arm/p1", "https://arm/p2", "https://arm/p3"), pages.requested); + } + + @Test + void anEmptyNextLinkTerminatesRatherThanRequestingIt() { + // ARM writes "" as often as it omits the key; treating it as a URL would fetch the wrong host. + ScriptedPages pages = new ScriptedPages("{\"value\":[{\"name\":\"a\"}],\"nextLink\":\"\"}"); + + List> items = FoundryArm.fetchAll(pages, "https://arm/first"); + + assertEquals(1, items.size()); + assertEquals(1, pages.requested.size()); + } + + @Test + void aNonStringNextLinkTerminates() { + ScriptedPages pages = new ScriptedPages("{\"value\":[{\"name\":\"a\"}],\"nextLink\":42}"); + + assertEquals(1, FoundryArm.fetchAll(pages, "https://arm/first").size()); + assertEquals(1, pages.requested.size()); + } + + @Test + void aPageWithNoValueContributesNothingButStillPages() { + ScriptedPages pages = + new ScriptedPages( + "{\"nextLink\":\"https://arm/p2\"}", "{\"value\":[{\"name\":\"a\"}]}"); + + List> items = FoundryArm.fetchAll(pages, "https://arm/p1"); + + assertEquals(1, items.size()); + assertEquals(2, pages.requested.size()); + } + + @Test + void aNonObjectBodyStopsTheLoopAndKeepsWhatWasRead() { + ScriptedPages pages = + new ScriptedPages("{\"value\":[{\"name\":\"a\"}],\"nextLink\":\"https://arm/p2\"}", "\"nonsense\""); + + List> items = FoundryArm.fetchAll(pages, "https://arm/p1"); + + // A page that cannot be read is the end of the road, but it does not discard earlier pages. + assertEquals(1, items.size()); + assertEquals(2, pages.requested.size()); + } + + @Test + void nonObjectEntriesAreSkipped() { + // Deliberate, documented divergence from the Rust reference, not an accident of typing. + // + // Rust keeps every `value` entry and maps S1 projects infallibly, so a stray null becomes a + // project with empty fields — which then makes the project list non-empty and suppresses the + // classic-hub fallback that would have found the real projects. Dropping the entry here keeps + // that fallback reachable. ARM does not emit such payloads, so this is malformed-input only; + // it is filed as a cross-runtime follow-up against the Rust side rather than replicated. + ScriptedPages pages = new ScriptedPages("{\"value\":[{\"name\":\"a\"},\"stray\",7]}"); + + assertEquals(1, FoundryArm.fetchAll(pages, "https://arm/first").size()); + } + + @Test + void aTransportFailurePropagatesOutOfTheStrictFetch() { + // The strict path backs subscription and resource listing, where an empty list and a failed + // call mean very different things: swallowing the failure would show a picker with no + // subscriptions rather than telling the caller the lookup did not happen. + FoundryArm.PageEndpoint failing = + url -> { + throw new IllegalStateException("403 Forbidden"); + }; + + assertThrows( + IllegalStateException.class, () -> FoundryArm.fetchAll(failing, "https://arm/first")); + } + + @Test + void aFailureOnALaterPagePropagatesRatherThanReturningAPartialList() { + // A truncated list is the dangerous case: it looks authoritative while missing entries. + FoundryArm.PageEndpoint failsOnSecondPage = + new FoundryArm.PageEndpoint() { + private int calls; + + @Override + public Object get(String url) { + if (calls++ == 0) { + return TypraJson.parse("{\"value\":[{\"name\":\"a\"}],\"nextLink\":\"https://arm/p2\"}"); + } + throw new IllegalStateException("500 Internal Server Error"); + } + }; + + assertThrows( + IllegalStateException.class, () -> FoundryArm.fetchAll(failsOnSecondPage, "https://arm/p1")); + } + + @Test + void aFirstUrlThatIsEmptyNeverCallsTheTransport() { + ScriptedPages pages = new ScriptedPages(); + + assertTrue(FoundryArm.fetchAll(pages, "").isEmpty()); + assertTrue(pages.requested.isEmpty()); + } + + @Test + void theSoftFailingProbeSwallowsATransportFailure() { + // A tenant that denies one project provider is ordinary; it must not fail the whole lookup. + FoundryArm.PageEndpoint failing = + url -> { + throw new RuntimeException("403 Forbidden"); + }; + + assertTrue(FoundryArm.fetchAllOrEmpty(failing, "https://arm/projects").isEmpty()); + } + + @Test + void theSoftFailingProbeDiscardsPagesReadBeforeTheFailure() { + // Half a list is worse than none: it would look authoritative while silently missing entries. + FoundryArm.PageEndpoint failsOnSecondPage = + new FoundryArm.PageEndpoint() { + private int calls; + + @Override + public Object get(String url) { + if (calls++ == 0) { + return TypraJson.parse( + "{\"value\":[{\"name\":\"a\"}],\"nextLink\":\"https://arm/p2\"}"); + } + throw new RuntimeException("500 Internal Server Error"); + } + }; + + assertTrue(FoundryArm.fetchAllOrEmpty(failsOnSecondPage, "https://arm/p1").isEmpty()); + } + + @Test + void theSoftFailingProbeReturnsPagesWhenNothingFails() { + ScriptedPages pages = new ScriptedPages("{\"value\":[{\"name\":\"a\"}]}"); + + assertEquals(1, FoundryArm.fetchAllOrEmpty(pages, "https://arm/projects").size()); + } + } + + @SuppressWarnings("unchecked") + private static Map json(String text) { + return (Map) TypraJson.parse(text); + } +} diff --git a/runtime/java/prompty-foundry/src/test/java/com/microsoft/prompty/foundry/FoundryAuthTest.java b/runtime/java/prompty-foundry/src/test/java/com/microsoft/prompty/foundry/FoundryAuthTest.java new file mode 100644 index 000000000..abc04dcc0 --- /dev/null +++ b/runtime/java/prompty-foundry/src/test/java/com/microsoft/prompty/foundry/FoundryAuthTest.java @@ -0,0 +1,65 @@ +package com.microsoft.prompty.foundry; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.microsoft.prompty.model.ApiKeyConnection; +import com.microsoft.prompty.model.Connection; +import com.microsoft.prompty.model.FoundryConnection; +import java.util.Optional; +import org.junit.jupiter.api.Test; + +/** Credential precedence and blank handling for Foundry connections. */ +class FoundryAuthTest { + + private static ApiKeyConnection key(String apiKey) { + ApiKeyConnection connection = new ApiKeyConnection(); + connection.kind = "key"; + connection.endpoint = "https://example.openai.azure.com"; + connection.apiKey = apiKey; + return connection; + } + + @Test + void anApiKeyIsReadFromTheConnection() { + assertEquals(Optional.of("secret"), FoundryAuth.apiKey(key("secret"))); + } + + @Test + void surroundingWhitespaceIsTrimmed() { + // A key pasted out of a portal commonly carries a trailing newline. + assertEquals(Optional.of("secret"), FoundryAuth.apiKey(key(" secret\n"))); + } + + @Test + void aBlankCredentialCountsAsAbsent() { + // Otherwise the request would authenticate with nothing instead of falling through. + assertTrue(FoundryAuth.apiKey(key(" ")).isEmpty()); + assertTrue(FoundryAuth.apiKey(key("")).isEmpty()); + assertTrue(FoundryAuth.bearerToken(key(" ")).isEmpty()); + } + + @Test + void aBearerTokenAcceptsTheApiKeyFieldForCompatibility() { + // Hosts commonly carry an OAuth token in apiKey. + assertEquals(Optional.of("token"), FoundryAuth.bearerToken(key("token"))); + } + + @Test + void aFoundryConnectionCarriesNoInlineCredential() { + // The generated Foundry connection declares only an endpoint, name, and type, so the token has + // to come from the environment. + FoundryConnection connection = new FoundryConnection(); + connection.kind = "foundry"; + connection.endpoint = "https://example.services.ai.azure.com"; + assertTrue(FoundryAuth.bearerToken(connection).isEmpty()); + assertTrue(FoundryAuth.apiKey(connection).isEmpty()); + } + + @Test + void anAbsentConnectionIsNotAnError() { + Connection none = null; + assertTrue(FoundryAuth.apiKey(none).isEmpty()); + assertTrue(FoundryAuth.bearerToken(none).isEmpty()); + } +} diff --git a/runtime/java/prompty-foundry/src/test/java/com/microsoft/prompty/foundry/FoundryExecutorTest.java b/runtime/java/prompty-foundry/src/test/java/com/microsoft/prompty/foundry/FoundryExecutorTest.java new file mode 100644 index 000000000..68b29acba --- /dev/null +++ b/runtime/java/prompty-foundry/src/test/java/com/microsoft/prompty/foundry/FoundryExecutorTest.java @@ -0,0 +1,280 @@ +package com.microsoft.prompty.foundry; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.microsoft.prompty.Environment; +import com.microsoft.prompty.InvokerException; +import com.microsoft.prompty.model.LoadContext; +import com.microsoft.prompty.model.Prompty; +import java.util.LinkedHashMap; +import java.util.Map; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +/** + * Routing and authentication for the Foundry provider. + * + *

The wire format is covered by the shared vectors through the OpenAI module, so what is left to + * check here is everything Azure does differently: which URL a request goes to, which header + * authenticates it, and which fallbacks apply when the prompt leaves something out. + */ +class FoundryExecutorTest { + + private final FoundryExecutor executor = new FoundryExecutor(); + + @AfterEach + void clearEnvironment() { + Environment.clearAll(); + } + + // ------------------------------------------------------------------ setup + + private static Prompty agent(Map model) { + Map data = new LinkedHashMap<>(); + data.put("name", "foundry-test"); + data.put("kind", "prompt"); + data.put("instructions", "test"); + data.put("model", model); + return Prompty.load(data, new LoadContext()); + } + + private static Map keyModel() { + return new LinkedHashMap<>( + Map.of( + "id", + "gpt-4o-mini", + "provider", + "foundry", + "connection", + Map.of( + "kind", "key", + "endpoint", "https://myresource.openai.azure.com", + "apiKey", "secret"))); + } + + private static Map foundryModel() { + return new LinkedHashMap<>( + Map.of( + "id", + "gpt-4o-mini", + "provider", + "foundry", + "connection", + Map.of( + "kind", "foundry", + "endpoint", "https://myresource.services.ai.azure.com/api/projects/proj"))); + } + + // -------------------------------------------------------------------- url + + @Test + void aKeyConnectionAddressesANamedDeployment() { + assertEquals( + "https://myresource.openai.azure.com/openai/deployments/gpt-4o-mini/chat/completions" + + "?api-version=" + + FoundryExecutor.DEFAULT_API_VERSION, + executor.buildUrl(agent(keyModel()), "/v1/chat/completions")); + } + + @Test + void embeddingAndImageMapToTheirAzureOperations() { + assertTrue( + executor.buildUrl(agent(keyModel()), "/v1/embeddings").contains("/embeddings?api-version=")); + assertTrue( + executor + .buildUrl(agent(keyModel()), "/v1/images/generations") + .contains("/images/generations?api-version=")); + } + + @Test + void aFoundryConnectionAddressesTheOpenAiSurfaceDirectly() { + // No deployment segment and no api-version: the OpenAI/v1 surface routes by the model in the + // body. + assertEquals( + "https://myresource.openai.azure.com/openai/v1/chat/completions", + executor.buildUrl(agent(foundryModel()), "/v1/chat/completions")); + } + + @Test + void aTrailingSlashOnTheEndpointDoesNotDoubleUp() { + Map model = keyModel(); + model.put( + "connection", + Map.of( + "kind", "key", + "endpoint", "https://myresource.openai.azure.com///", + "apiKey", "secret")); + assertTrue( + executor.buildUrl(agent(model), "/v1/chat/completions").startsWith("https://myresource.openai.azure.com/openai/deployments/")); + } + + @Test + void azureHasNoResponsesSurface() { + InvokerException error = + assertThrows( + InvokerException.class, () -> executor.buildUrl(agent(keyModel()), "/v1/responses")); + assertTrue(error.getMessage().contains("responses"), error.getMessage()); + } + + @Test + void theApiVersionCanBeOverriddenPerPrompt() { + Map model = keyModel(); + model.put("options", Map.of("additionalProperties", Map.of("apiVersion", "2024-02-01"))); + assertTrue( + executor.buildUrl(agent(model), "/v1/chat/completions").endsWith("?api-version=2024-02-01")); + } + + // ------------------------------------------------------------- fallbacks + + @Test + void theEndpointFallsBackToTheEnvironment() { + Environment.set("AZURE_OPENAI_ENDPOINT", "https://fromenv.openai.azure.com"); + Map model = new LinkedHashMap<>(Map.of("id", "gpt-4o-mini")); + assertTrue( + executor.buildUrl(agent(model), "/v1/chat/completions").startsWith("https://fromenv.openai.azure.com/")); + } + + @Test + void theDeploymentFallsBackToTheEnvironment() { + Environment.set("AZURE_OPENAI_DEPLOYMENT", "env-deployment"); + Map model = new LinkedHashMap<>(keyModel()); + model.remove("id"); + assertTrue( + executor.buildUrl(agent(model), "/v1/chat/completions").contains("/deployments/env-deployment/")); + } + + @Test + void aMissingEndpointIsReportedRatherThanGuessed() { + Environment.mask("AZURE_OPENAI_ENDPOINT"); + Map model = new LinkedHashMap<>(Map.of("id", "gpt-4o-mini")); + InvokerException error = + assertThrows( + InvokerException.class, () -> executor.buildUrl(agent(model), "/v1/chat/completions")); + assertTrue(error.getMessage().contains("AZURE_OPENAI_ENDPOINT"), error.getMessage()); + } + + @Test + void aMissingDeploymentIsReportedRatherThanGuessed() { + Environment.mask("AZURE_OPENAI_DEPLOYMENT"); + Map model = new LinkedHashMap<>(keyModel()); + model.remove("id"); + InvokerException error = + assertThrows( + InvokerException.class, () -> executor.buildUrl(agent(model), "/v1/chat/completions")); + assertTrue(error.getMessage().contains("AZURE_OPENAI_DEPLOYMENT"), error.getMessage()); + } + + // ------------------------------------------------------------------- auth + + @Test + void aKeyConnectionUsesTheAzureApiKeyHeader() { + // Azure authenticates with api-key, not an Authorization bearer. + assertEquals(Map.of("api-key", "secret"), executor.authHeaders(agent(keyModel()))); + } + + @Test + void theApiKeyFallsBackToTheEnvironment() { + Environment.set("AZURE_OPENAI_API_KEY", "env-key"); + Map model = keyModel(); + model.put( + "connection", Map.of("kind", "key", "endpoint", "https://myresource.openai.azure.com")); + assertEquals(Map.of("api-key", "env-key"), executor.authHeaders(agent(model))); + } + + @Test + void aMissingApiKeyIsReportedRatherThanSentEmpty() { + Environment.mask("AZURE_OPENAI_API_KEY"); + Map model = keyModel(); + model.put( + "connection", Map.of("kind", "key", "endpoint", "https://myresource.openai.azure.com")); + InvokerException error = + assertThrows(InvokerException.class, () -> executor.authHeaders(agent(model))); + assertTrue(error.getMessage().contains("AZURE_OPENAI_API_KEY"), error.getMessage()); + } + + @Test + void aFoundryConnectionUsesABearerToken() { + Environment.set("AZURE_INFERENCE_CREDENTIAL", "token-abc"); + assertEquals( + Map.of("Authorization", "Bearer token-abc"), executor.authHeaders(agent(foundryModel()))); + } + + @Test + void aMissingFoundryTokenIsReportedRatherThanFallingBackToAKey() { + // An api-key would be silently rejected by the Foundry surface, so the absence is reported. + // The token is masked rather than merely cleared: a machine that exports + // AZURE_INFERENCE_CREDENTIAL for live runs would otherwise satisfy the lookup and there would + // be no absence left to assert on. + Environment.mask("AZURE_INFERENCE_CREDENTIAL"); + Environment.set("AZURE_OPENAI_API_KEY", "not-a-token"); + InvokerException error = + assertThrows(InvokerException.class, () -> executor.authHeaders(agent(foundryModel()))); + assertTrue(error.getMessage().contains("AZURE_INFERENCE_CREDENTIAL"), error.getMessage()); + } + + // ------------------------------------------------------- endpoint rewrite + + @Test + void aProjectEndpointBecomesTheInferenceSurface() { + assertEquals( + "https://myresource.openai.azure.com/openai/v1", + FoundryExecutor.stripProjectPath( + "https://myresource.services.ai.azure.com/api/projects/my-project")); + } + + @Test + void aProjectEndpointWithoutAProjectPathStillResolves() { + assertEquals( + "https://myresource.openai.azure.com/openai/v1", + FoundryExecutor.stripProjectPath("https://myresource.services.ai.azure.com")); + } + + @Test + void aHostThatIsNotAServicesHostKeepsItsName() { + assertEquals( + "https://custom.example.com/openai/v1", + FoundryExecutor.stripProjectPath("https://custom.example.com/api/projects/p")); + } + + @Test + void aPortIsCarriedThrough() { + assertEquals( + "https://localhost:8443/openai/v1", FoundryExecutor.stripProjectPath("https://localhost:8443")); + } + + @Test + void somethingThatIsNotAUrlIsLeftAlone() { + // A colon that is not a port must not be mistaken for one either. + assertEquals("not-a-url", FoundryExecutor.stripProjectPath("not-a-url")); + assertEquals( + "https://host:notaport/openai/v1", FoundryExecutor.stripProjectPath("https://host:notaport")); + } + + @Test + void aColonThatIsNotAPortDoesNotHideTheServicesHost() { + // Splitting userinfo off as if it were a port would leave the host looking like "https", and + // the endpoint would never be rewritten to the inference surface. + assertEquals( + "https://user:pass@myresource.openai.azure.com/openai/v1", + FoundryExecutor.stripProjectPath("https://user:pass@myresource.services.ai.azure.com")); + } + + @Test + void aRealPortOnAServicesHostSurvivesTheRewrite() { + assertEquals( + "https://myresource.openai.azure.com:8443/openai/v1", + FoundryExecutor.stripProjectPath("https://myresource.services.ai.azure.com:8443/api/projects/p")); + } + + // ------------------------------------------------------------- processor + + @Test + void theProcessorDoesNotOfferAContinuationAzureCannotHonour() { + FoundryProcessor processor = new FoundryProcessor(); + assertEquals("foundry", processor.providerName()); + assertFalse(processor.supportsResponsesContinuation()); + } +} diff --git a/runtime/java/prompty-foundry/src/test/java/com/microsoft/prompty/foundry/FoundryLiveTest.java b/runtime/java/prompty-foundry/src/test/java/com/microsoft/prompty/foundry/FoundryLiveTest.java new file mode 100644 index 000000000..3ba02ce69 --- /dev/null +++ b/runtime/java/prompty-foundry/src/test/java/com/microsoft/prompty/foundry/FoundryLiveTest.java @@ -0,0 +1,144 @@ +package com.microsoft.prompty.foundry; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.microsoft.prompty.LiveEnv; +import com.microsoft.prompty.Pipeline; +import com.microsoft.prompty.model.Prompty; +import java.util.LinkedHashMap; +import java.util.Map; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +/** + * End-to-end coverage against a real Azure AI Foundry deployment. + * + *

Foundry needs both an endpoint and a credential, and the two arrive by different routes: a + * classic Azure OpenAI resource uses {@code AZURE_OPENAI_ENDPOINT} plus {@code AZURE_OPENAI_API_KEY}, + * while a Foundry project uses {@code FOUNDRY_PROJECT_ENDPOINT} plus a bearer token in {@code + * AZURE_INFERENCE_CREDENTIAL} — typically {@code az account get-access-token}. Each test asks only + * for the pair it needs so a machine holding one style of credential still exercises that path. + * + *

Method order is pinned because one test deliberately removes a credential. JUnit's default + * order is an unspecified hash order, so without pinning, whether a later test ran or skipped could + * change between JVMs — and a skip that looks like "no credential" would really be "a previous test + * took it away". + * + *

Excluded from the normal build by the {@code live} tag. Run with {@code -PliveTests}. + */ +@Tag("live") +@DisplayName("live: Foundry") +@TestMethodOrder(MethodOrderer.MethodName.class) +final class FoundryLiveTest { + + @BeforeAll + static void setUp() { + LiveEnv.load(); + } + + /** A prompt against a classic Azure OpenAI deployment, addressed by deployment name. */ + private static Prompty azureAgent(String question, Map options) { + Map connection = new LinkedHashMap<>(); + connection.put("kind", "key"); + connection.put("endpoint", LiveEnv.get("AZURE_OPENAI_ENDPOINT", "")); + connection.put("apiKey", LiveEnv.get("AZURE_OPENAI_API_KEY", "")); + + Map model = new LinkedHashMap<>(); + model.put("id", LiveEnv.get("AZURE_OPENAI_DEPLOYMENT", "gpt-4o-mini")); + model.put("provider", "foundry"); + model.put("apiType", "chat"); + model.put("connection", connection); + model.put("options", options); + + Map data = new LinkedHashMap<>(); + data.put("name", "live-foundry-azure"); + data.put("kind", "prompt"); + data.put("model", model); + data.put("instructions", "system:\nYou are a helpful assistant. Be very brief.\nuser:\n" + question); + return Prompty.load(data, new com.microsoft.prompty.model.LoadContext()); + } + + /** A prompt against a Foundry project's inference surface, addressed by model id. */ + private static Prompty foundryAgent(String question, Map options) { + Map connection = new LinkedHashMap<>(); + connection.put("kind", "foundry"); + connection.put("endpoint", LiveEnv.get("FOUNDRY_PROJECT_ENDPOINT", "")); + + Map model = new LinkedHashMap<>(); + model.put("id", LiveEnv.get("FOUNDRY_MODEL", "gpt-4o-mini")); + model.put("provider", "foundry"); + model.put("apiType", "chat"); + model.put("connection", connection); + model.put("options", options); + + Map data = new LinkedHashMap<>(); + data.put("name", "live-foundry-project"); + data.put("kind", "prompt"); + data.put("model", model); + data.put("instructions", "system:\nYou are a helpful assistant. Be very brief.\nuser:\n" + question); + return Prompty.load(data, new com.microsoft.prompty.model.LoadContext()); + } + + @Test + void azureDeploymentChatCompletionReturnsText() { + LiveEnv.require("AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_API_KEY", "AZURE_OPENAI_DEPLOYMENT"); + + Object result = + Pipeline.invoke( + azureAgent("Say hello in exactly 3 words.", Map.of("temperature", 0, "maxOutputTokens", 100)), + Map.of()); + + String text = Pipeline.textOf(result); + assertNotNull(text); + assertFalse(text.isBlank(), "chat completion returned no text"); + System.out.println("[foundry/azure] chat -> " + text); + } + + @Test + void foundryProjectChatCompletionReturnsText() { + LiveEnv.require("FOUNDRY_PROJECT_ENDPOINT", "AZURE_INFERENCE_CREDENTIAL"); + + Object result = + Pipeline.invoke( + foundryAgent("Say hello in exactly 3 words.", Map.of("temperature", 0, "maxOutputTokens", 100)), + Map.of()); + + String text = Pipeline.textOf(result); + assertNotNull(text); + assertFalse(text.isBlank(), "chat completion returned no text"); + System.out.println("[foundry/project] chat -> " + text); + } + + @Test + void aMissingFoundryCredentialFailsBeforeAnyRequestIsSent() { + LiveEnv.require("FOUNDRY_PROJECT_ENDPOINT"); + + // Masking is what makes this test mean something: the credential is genuinely present on this + // machine, and a JVM cannot remove it from its own environment. Without the mask the request + // would authenticate and there would be no absence to assert on. + RuntimeException failure = null; + try { + com.microsoft.prompty.Environment.mask("AZURE_INFERENCE_CREDENTIAL"); + try { + Pipeline.invoke(foundryAgent("Hello", Map.of("maxOutputTokens", 5)), Map.of()); + } catch (RuntimeException e) { + failure = e; + } + } finally { + com.microsoft.prompty.Environment.clear("AZURE_INFERENCE_CREDENTIAL"); + } + + assertNotNull(failure, "a missing credential should fail rather than send an anonymous request"); + String message = String.valueOf(failure.getMessage()); + assertTrue( + message.contains("AZURE_INFERENCE_CREDENTIAL"), + "the error should name the variable that was missing but said: " + message); + System.out.println("[foundry/project] missing credential -> " + message); + } +} diff --git a/runtime/java/prompty-foundry/src/test/java/com/microsoft/prompty/foundry/FoundryModelListerTest.java b/runtime/java/prompty-foundry/src/test/java/com/microsoft/prompty/foundry/FoundryModelListerTest.java new file mode 100644 index 000000000..7ac057e5f --- /dev/null +++ b/runtime/java/prompty-foundry/src/test/java/com/microsoft/prompty/foundry/FoundryModelListerTest.java @@ -0,0 +1,244 @@ +package com.microsoft.prompty.foundry; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.microsoft.prompty.Environment; +import com.microsoft.prompty.InvokerException; +import com.microsoft.prompty.model.ModelInfo; +import com.sun.net.httpserver.HttpServer; +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +/** + * Model listing for Foundry and Azure OpenAI connections. + * + *

The mapping from wire shape to {@link ModelInfo} is covered by the shared discovery vectors, + * so this suite is about the parts the vectors cannot reach: which service a connection kind is + * routed to, how the URL and credential are assembled, and what happens on the failure paths. + */ +class FoundryModelListerTest { + + private HttpServer server; + private String baseUrl; + private final List requests = new ArrayList<>(); + + @BeforeEach + void startServer() throws IOException { + server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); + server.start(); + baseUrl = "http://127.0.0.1:" + server.getAddress().getPort(); + requests.clear(); + } + + @AfterEach + void stopServer() { + server.stop(0); + Environment.clearAll(); + } + + /** Record the request line and auth headers, then answer with a fixed body. */ + private void respond(String path, String body) { + server.createContext( + path, + exchange -> { + requests.add( + exchange.getRequestURI().toString() + + "|Authorization=" + + exchange.getRequestHeaders().getFirst("Authorization") + + "|api-key=" + + exchange.getRequestHeaders().getFirst("api-key")); + byte[] bytes = body.getBytes(StandardCharsets.UTF_8); + exchange.sendResponseHeaders(200, bytes.length); + try (OutputStream out = exchange.getResponseBody()) { + out.write(bytes); + } + }); + } + + @Nested + @DisplayName("connection kind routing") + class Routing { + + @Test + void aFoundryConnectionListsDeployments() { + respond( + "/deployments", + "{\"value\":[{\"name\":\"gpt-4o-prod\",\"modelName\":\"gpt-4o\"," + + "\"modelPublisher\":\"OpenAI\"}]}"); + + List models = + new FoundryModelLister() + .listModels( + Map.of("kind", "foundry", "endpoint", baseUrl, "apiKey", "token-abc")); + + // The deployment name is the invokable identifier, so it is what lands in id. + assertEquals(List.of("gpt-4o-prod"), models.stream().map(m -> m.id).toList()); + assertEquals("gpt-4o", models.get(0).displayName); + assertTrue( + requests.get(0).startsWith("/deployments?api-version=v1|Authorization=Bearer token-abc"), + "request was: " + requests.get(0)); + } + + @Test + void aKeyConnectionListsTheModelCatalog() { + respond("/openai/models", "{\"data\":[{\"id\":\"gpt-4o\",\"owned_by\":\"openai\"}]}"); + + List models = + new FoundryModelLister() + .listModels(Map.of("kind", "key", "endpoint", baseUrl, "apiKey", "secret")); + + assertEquals(List.of("gpt-4o"), models.stream().map(m -> m.id).toList()); + // The catalog authenticates with the Azure api-key header, not a bearer token. + assertTrue(requests.get(0).contains("|api-key=secret"), "request was: " + requests.get(0)); + assertTrue( + requests.get(0).contains("Authorization=null"), "request was: " + requests.get(0)); + } + + @Test + void anUnsupportedKindIsRejectedBeforeAnyRequest() { + // Falling through to one of the two services would produce a confusing transport error for + // what is really a configuration mistake. + InvokerException error = + assertThrows( + InvokerException.class, + () -> new FoundryModelLister().listModels(Map.of("kind", "reference"))); + + assertTrue(error.getMessage().contains("reference"), error.getMessage()); + assertTrue(error.getMessage().contains("foundry"), error.getMessage()); + assertEquals(0, requests.size()); + } + + @Test + void aNonMapConnectionIsRejectedRatherThanCrashing() { + assertThrows( + InvokerException.class, () -> new FoundryModelLister().listModels("not-a-connection")); + } + } + + @Nested + @DisplayName("url assembly") + class Urls { + + @Test + void aTrailingSlashDoesNotDoubleUp() { + assertEquals( + "https://p.example/deployments?api-version=v1", + FoundryModelLister.deploymentsUrl(Map.of("endpoint", "https://p.example///"))); + } + + @Test + void anAbsentDeploymentEndpointIsRejected() { + // There is no environment fallback for a project endpoint, so guessing would be wrong. + assertThrows( + InvokerException.class, () -> FoundryModelLister.deploymentsUrl(Map.of())); + } + + @Test + void theCatalogEndpointFallsBackToTheEnvironment() { + Environment.set("AZURE_OPENAI_ENDPOINT", "https://acct.openai.azure.com/"); + + assertEquals( + "https://acct.openai.azure.com/openai/models?api-version=" + + FoundryModelLister.DEFAULT_API_VERSION, + FoundryModelLister.catalogUrl(Map.of())); + } + + @Test + void aConnectionApiVersionOverridesTheDefault() { + assertEquals( + "https://a.example/openai/models?api-version=2024-06-01", + FoundryModelLister.catalogUrl( + Map.of("endpoint", "https://a.example", "apiVersion", "2024-06-01"))); + } + + @Test + void anAbsentCatalogEndpointIsRejected() { + // The catalog URL does fall back to AZURE_OPENAI_ENDPOINT, so the absence has to be + // asserted against a masked name rather than a merely unset one. + Environment.mask("AZURE_OPENAI_ENDPOINT"); + assertThrows(InvokerException.class, () -> FoundryModelLister.catalogUrl(Map.of())); + } + } + + @Nested + @DisplayName("credential resolution") + class Credentials { + + @Test + void anUndeclaredSnakeCaseAliasIsStillAccepted() { + // Listing takes its connection as raw JSON, so unlike the typed executor path it can see the + // aliases a host may have written. + assertEquals("t", FoundryModelLister.deploymentToken(Map.of("bearer_token", "t"))); + assertEquals("k", FoundryModelLister.catalogKey(Map.of("api_key", "k"))); + } + + @Test + void aBlankCredentialIsTreatedAsAbsent() { + // Sending an empty bearer would produce a 401 that reads like a permissions problem. + assertThrows( + InvokerException.class, () -> FoundryModelLister.deploymentToken(Map.of("apiKey", " "))); + } + + @Test + void theCatalogKeyFallsBackToTheEnvironment() { + Environment.set("AZURE_OPENAI_API_KEY", "env-key"); + + assertEquals("env-key", FoundryModelLister.catalogKey(Map.of())); + } + + @Test + void aBlankEnvironmentKeyDoesNotSatisfyTheRequirement() { + Environment.set("AZURE_OPENAI_API_KEY", " "); + + assertThrows(InvokerException.class, () -> FoundryModelLister.catalogKey(Map.of())); + } + + @Test + void aMissingDeploymentTokenNamesTheWayToGetOne() { + com.microsoft.prompty.Environment.mask("AZURE_INFERENCE_CREDENTIAL"); + InvokerException error = + assertThrows( + InvokerException.class, () -> FoundryModelLister.deploymentToken(Map.of())); + + assertTrue(error.getMessage().contains("device code"), error.getMessage()); + } + } + + @Nested + @DisplayName("response handling") + class Responses { + + @Test + void aProjectWithNoDeploymentsIsEmptyRatherThanAFailure() { + respond("/deployments", "{\"value\":[]}"); + + assertEquals( + List.of(), + new FoundryModelLister() + .listModels(Map.of("kind", "foundry", "endpoint", baseUrl, "apiKey", "t"))); + } + + @Test + void aResponseMissingTheCollectionIsAlsoEmpty() { + // Some Azure surfaces omit the key entirely instead of sending an empty array. + respond("/openai/models", "{}"); + + assertEquals( + List.of(), + new FoundryModelLister() + .listModels(Map.of("kind", "key", "endpoint", baseUrl, "apiKey", "k"))); + } + } +} diff --git a/runtime/java/prompty-foundry/src/test/java/com/microsoft/prompty/foundry/FoundryModelsTest.java b/runtime/java/prompty-foundry/src/test/java/com/microsoft/prompty/foundry/FoundryModelsTest.java new file mode 100644 index 000000000..158736012 --- /dev/null +++ b/runtime/java/prompty-foundry/src/test/java/com/microsoft/prompty/foundry/FoundryModelsTest.java @@ -0,0 +1,138 @@ +package com.microsoft.prompty.foundry; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import com.microsoft.prompty.model.ModelInfo; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +/** + * Covers the payload edges the shared discovery vectors leave alone. + * + *

The vectors describe well-formed answers from both Foundry endpoints. Real deployments + * occasionally omit a field or send it empty, and the reference runtime draws distinctions there + * that are easy to lose in translation, so those distinctions are pinned here. + */ +class FoundryModelsTest { + + private static Map deployment(Map extra) { + Map raw = new LinkedHashMap<>(); + raw.put("name", "my-deployment"); + raw.putAll(extra); + return raw; + } + + @Nested + @DisplayName("publisher attribution") + class Publisher { + + @Test + @DisplayName("a deployment that names no publisher is still attributed to Azure") + void absentPublisherFallsBack() { + ModelInfo info = FoundryModels.deploymentToModelInfo(deployment(Map.of())); + assertEquals("azure", info.ownedBy); + } + + @Test + @DisplayName("a publisher the endpoint sent as empty is kept, not replaced") + void emptyPublisherIsKept() { + ModelInfo info = + FoundryModels.deploymentToModelInfo(deployment(Map.of("modelPublisher", ""))); + assertEquals("", info.ownedBy); + } + + @Test + @DisplayName("a nested publisher is found when the flat one is absent") + void nestedPublisherIsUsed() { + Map raw = + deployment(Map.of("properties", Map.of("model", Map.of("publisher", "contoso")))); + assertEquals("contoso", FoundryModels.deploymentToModelInfo(raw).ownedBy); + } + } + + @Nested + @DisplayName("identity") + class Identity { + + @Test + @DisplayName("a deployment with no name still round trips with an id") + void missingNameBecomesEmptyId() { + ModelInfo info = FoundryModels.deploymentToModelInfo(Map.of("modelName", "gpt-4o")); + assertEquals("", info.id); + } + + @Test + @DisplayName("a catalog entry with no id still round trips with an id") + void missingCatalogIdBecomesEmpty() { + ModelInfo info = FoundryModels.catalogModelToModelInfo(Map.of("owned_by", "")); + assertEquals("", info.id); + } + + @Test + @DisplayName("a payload that is not an object yields a blank record rather than throwing") + void nonObjectIsTolerated() { + assertEquals("", FoundryModels.deploymentToModelInfo("not-an-object").id); + assertNull(FoundryModels.deploymentToModelInfo(null).contextWindow); + assertEquals("", FoundryModels.catalogModelToModelInfo(List.of()).id); + } + } + + @Nested + @DisplayName("capability blocks") + class CapabilityBlocks { + + @Test + @DisplayName("a capability block the endpoint sent empty is honoured, not skipped") + void presentButEmptyBlockWins() { + Map raw = + deployment( + Map.of( + "properties", Map.of("capabilities", Map.of()), + "capabilities", Map.of("inputModalities", List.of("text", "image")))); + ModelInfo info = FoundryModels.deploymentToModelInfo(raw); + assertNull(info.inputModalities, "the sibling block must not stand in for an empty one"); + } + + @Test + @DisplayName("modalities sent as a comma separated string are split") + void commaSeparatedModalities() { + Map raw = + deployment(Map.of("capabilities", Map.of("supportedInputModalities", "text, image"))); + assertEquals(List.of("text", "image"), FoundryModels.deploymentToModelInfo(raw).inputModalities); + } + + @Test + @DisplayName("a context length sent as a string is still a number") + void stringEncodedContextLength() { + Map raw = + deployment(Map.of("capabilities", Map.of("maxContextLength", "128000"))); + assertEquals(128000, FoundryModels.deploymentToModelInfo(raw).contextWindow); + } + + @Test + @DisplayName("a context length that is not a number is dropped rather than fatal") + void unparseableContextLength() { + Map raw = + deployment(Map.of("capabilities", Map.of("maxContextLength", "very large"))); + assertNull(FoundryModels.deploymentToModelInfo(raw).contextWindow); + } + } + + @Test + @DisplayName("the shared dataset carries no Foundry entries, so what the endpoint sent stands") + void datasetLeavesFoundryAlone() { + // Deployments name a deployment, not a model family, so prefix matching against a model table + // would be guesswork. The dataset therefore declares no Foundry entries and enrichment is a + // no-op here — a fact worth pinning, because a future entry would silently start rewriting + // fields the endpoint already answered for. + ModelInfo info = FoundryModels.deploymentToModelInfo(Map.of("name", "gpt-4o")); + assertNull(info.contextWindow); + assertNull(info.inputModalities); + assertNull(info.outputModalities); + } +} diff --git a/runtime/java/prompty-foundry/src/test/java/com/microsoft/prompty/foundry/FoundryOAuthTest.java b/runtime/java/prompty-foundry/src/test/java/com/microsoft/prompty/foundry/FoundryOAuthTest.java new file mode 100644 index 000000000..fa28b4822 --- /dev/null +++ b/runtime/java/prompty-foundry/src/test/java/com/microsoft/prompty/foundry/FoundryOAuthTest.java @@ -0,0 +1,552 @@ +package com.microsoft.prompty.foundry; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.microsoft.prompty.Http; +import com.microsoft.prompty.model.AuthorizationCodeFlow; +import com.microsoft.prompty.model.DeviceAuthorization; +import com.microsoft.prompty.model.OAuthToken; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Deque; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +/** + * Covers the interactive sign-in protocol: PKCE, authorize-URL construction, response validation, + * and the device-code poll state machine. + * + *

The poll loop is driven through its injected transport and clock rather than over the network, + * so the branches that only appear during a real sign-in — a user who has not answered yet, a + * provider asking to be polled less often, an expired code — are exercised deterministically and in + * microseconds. + */ +@DisplayName("Foundry OAuth") +final class FoundryOAuthTest { + + @Nested + @DisplayName("PKCE") + class Pkce { + + @Test + void theVerifierUsesOnlyCharactersTheSpecAllows() { + FoundryOAuth.Pkce pkce = FoundryOAuth.generatePkce(); + assertEquals(FoundryOAuth.PKCE_VERIFIER_LENGTH, pkce.verifier().length()); + for (char c : pkce.verifier().toCharArray()) { + boolean unreserved = + Character.isLetterOrDigit(c) && c < 128 || c == '-' || c == '.' || c == '_' || c == '~'; + assertTrue(unreserved, "verifier must use RFC 7636 unreserved characters but had: " + c); + } + } + + @Test + void theChallengeIsTheBase64UrlSha256OfTheVerifier() throws Exception { + FoundryOAuth.Pkce pkce = FoundryOAuth.generatePkce(); + byte[] digest = + MessageDigest.getInstance("SHA-256") + .digest(pkce.verifier().getBytes(StandardCharsets.US_ASCII)); + String expected = Base64.getUrlEncoder().withoutPadding().encodeToString(digest); + assertEquals(expected, pkce.challenge()); + } + + @Test + void theChallengeIsUrlSafeAndUnpadded() { + String challenge = FoundryOAuth.generatePkce().challenge(); + // SHA-256 is 32 bytes, which is 43 base64 characters once padding is dropped. + assertEquals(43, challenge.length()); + assertFalse(challenge.contains("="), "padding would be rejected in a URL"); + assertFalse(challenge.contains("+"), "'+' means a space in a query string"); + assertFalse(challenge.contains("/"), "'/' would be read as a path separator"); + } + + @Test + void everyPairIsFresh() { + FoundryOAuth.Pkce first = FoundryOAuth.generatePkce(); + FoundryOAuth.Pkce second = FoundryOAuth.generatePkce(); + assertNotEquals(first.verifier(), second.verifier()); + assertNotEquals(first.challenge(), second.challenge()); + } + + @Test + void aKnownVerifierProducesTheChallengeFromTheSpec() { + // RFC 7636 appendix B's worked example, which pins the digest and the encoding together. + assertEquals( + "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM", + FoundryOAuth.challengeFor("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk")); + } + } + + @Nested + @DisplayName("authorize URL") + class AuthorizeUrl { + + @Test + void carriesEveryParameterTheCodeFlowNeeds() { + AuthorizationCodeFlow flow = + FoundryOAuth.buildAuthCodeUrl("my-tenant", null, null, "http://127.0.0.1:5000"); + URI url = URI.create(flow.authUrl); + + assertEquals("login.microsoftonline.com", url.getHost()); + assertEquals("/my-tenant/oauth2/v2.0/authorize", url.getPath()); + + Map query = queryOf(flow.authUrl); + assertEquals(FoundryOAuth.DEFAULT_CLIENT_ID, query.get("client_id")); + assertEquals("code", query.get("response_type")); + assertEquals("http://127.0.0.1:5000", query.get("redirect_uri")); + assertEquals("query", query.get("response_mode")); + assertEquals(FoundryOAuth.AZURE_OPENAI_SCOPE, query.get("scope")); + assertEquals("S256", query.get("code_challenge_method")); + } + + @Test + void theChallengeInTheUrlMatchesTheVerifierHandedBack() { + AuthorizationCodeFlow flow = + FoundryOAuth.buildAuthCodeUrl("t", null, null, "http://127.0.0.1:1"); + // If these ever diverged the provider would reject the exchange, and only at the very last + // step of a sign-in the user already sat through. + assertEquals( + FoundryOAuth.challengeFor(flow.codeVerifier), queryOf(flow.authUrl).get("code_challenge")); + } + + @Test + void anEmptyTenantFallsBackToTheOrganizationsEndpoint() { + AuthorizationCodeFlow flow = + FoundryOAuth.buildAuthCodeUrl("", "custom-client", "custom-scope", "http://127.0.0.1:1"); + assertEquals("/organizations/oauth2/v2.0/authorize", URI.create(flow.authUrl).getPath()); + + Map query = queryOf(flow.authUrl); + assertEquals("custom-client", query.get("client_id")); + assertEquals("custom-scope", query.get("scope")); + } + + @Test + void theScopeSeparatorSurvivesEncoding() { + // The default scope contains a space; if it were dropped or mangled the request would ask for + // a single nonsensical scope and silently come back without a refresh token. + AuthorizationCodeFlow flow = + FoundryOAuth.buildAuthCodeUrl("t", null, null, "http://127.0.0.1:1"); + assertTrue( + flow.authUrl.contains("scope=https%3A%2F%2Fai.azure.com%2F.default+offline_access"), + "the space must survive as '+' but the URL was: " + flow.authUrl); + assertEquals(FoundryOAuth.AZURE_OPENAI_SCOPE, queryOf(flow.authUrl).get("scope")); + } + } + + @Nested + @DisplayName("endpoint URLs") + class EndpointUrls { + + @Test + void applyTheTenantDefault() { + assertEquals( + "https://login.microsoftonline.com/organizations/oauth2/v2.0/devicecode", + FoundryOAuth.deviceCodeUrl("")); + assertEquals( + "https://login.microsoftonline.com/contoso/oauth2/v2.0/token", + FoundryOAuth.tokenUrl("contoso")); + assertEquals( + "https://login.microsoftonline.com/organizations/oauth2/v2.0/authorize", + FoundryOAuth.authorizeUrl(null)); + } + } + + @Nested + @DisplayName("response parsing") + class ResponseParsing { + + @Test + void aTokenLoadsWithOnlyTheRequiredFields() { + OAuthToken token = + FoundryOAuth.parseToken( + "{\"access_token\":\"abc\",\"token_type\":\"Bearer\",\"expires_in\":3600}"); + assertEquals("abc", token.accessToken); + assertEquals("Bearer", token.tokenType); + assertEquals(3600L, token.expiresIn); + assertNull(token.refreshToken); + assertNull(token.scope); + } + + @Test + void aTokenCarriesItsRefreshAndScopeWhenPresent() { + OAuthToken token = + FoundryOAuth.parseToken( + "{\"access_token\":\"a\",\"token_type\":\"Bearer\",\"expires_in\":1," + + "\"refresh_token\":\"r\",\"scope\":\"s\"}"); + assertEquals("r", token.refreshToken); + assertEquals("s", token.scope); + } + + @Test + void aTokenMissingItsAccessTokenIsRejected() { + RuntimeException error = + assertThrows( + RuntimeException.class, + () -> FoundryOAuth.parseToken("{\"token_type\":\"Bearer\",\"expires_in\":3600}")); + assertTrue( + String.valueOf(error.getMessage()).contains("access_token"), + "the error should name the field that was missing: " + error.getMessage()); + } + + @Test + void aTokenWithAnEmptyAccessTokenIsRejected() { + // An empty string is not a usable credential, and accepting one would turn an auth failure + // into a confusing 401 several calls later. + assertThrows( + RuntimeException.class, + () -> + FoundryOAuth.parseToken( + "{\"access_token\":\"\",\"token_type\":\"Bearer\",\"expires_in\":1}")); + } + + @Test + void aDeviceAuthorizationLoads() { + DeviceAuthorization device = + FoundryOAuth.parseDeviceAuthorization( + "{\"device_code\":\"dc\",\"user_code\":\"UC\"," + + "\"verification_uri\":\"https://aka.ms/devicelogin\"," + + "\"expires_in\":900,\"interval\":5,\"message\":\"go here\"}"); + assertEquals("dc", device.deviceCode); + assertEquals("UC", device.userCode); + assertEquals("https://aka.ms/devicelogin", device.verificationUri); + assertEquals(5L, device.interval); + assertEquals("go here", device.message); + } + + @Test + void aDeviceAuthorizationWithoutAMessageDefaultsToEmpty() { + DeviceAuthorization device = + FoundryOAuth.parseDeviceAuthorization( + "{\"device_code\":\"dc\",\"user_code\":\"UC\",\"verification_uri\":\"u\"," + + "\"expires_in\":900,\"interval\":5}"); + assertEquals("", device.message); + } + + @Test + void aNegativePollIntervalIsRejected() { + // A negative interval would become a negative sleep and spin the poll loop as fast as the + // network allows, which is the behaviour the interval exists to prevent. + RuntimeException error = + assertThrows( + RuntimeException.class, + () -> + FoundryOAuth.parseDeviceAuthorization( + "{\"device_code\":\"dc\",\"user_code\":\"UC\",\"verification_uri\":\"u\"," + + "\"expires_in\":900,\"interval\":-1}")); + assertTrue( + String.valueOf(error.getMessage()).contains("interval"), + "the error should name the offending field: " + error.getMessage()); + } + + @Test + void aNonObjectResponseIsRejected() { + assertThrows(RuntimeException.class, () -> FoundryOAuth.parseToken("\"not an object\"")); + } + } + + @Nested + @DisplayName("device code poll loop") + class PollLoop { + + @Test + void returnsTheTokenOnTheFirstSuccessfulPoll() { + FakeEndpoint endpoint = + new FakeEndpoint( + ok("{\"access_token\":\"tok\",\"token_type\":\"Bearer\",\"expires_in\":10}")); + RecordingSleeper sleeper = new RecordingSleeper(); + + OAuthToken token = + FoundryOAuth.pollForToken( + "t", "dc", 5, 600, null, endpoint, sleeper, new SteadyClock()); + + assertEquals("tok", token.accessToken); + assertEquals(1, endpoint.calls.size()); + } + + @Test + void keepsWaitingWhileTheUserHasNotAnswered() { + FakeEndpoint endpoint = + new FakeEndpoint( + error(400, "authorization_pending"), + error(400, "authorization_pending"), + ok("{\"access_token\":\"tok\",\"token_type\":\"Bearer\",\"expires_in\":10}")); + RecordingSleeper sleeper = new RecordingSleeper(); + + OAuthToken token = + FoundryOAuth.pollForToken( + "t", "dc", 5, 600, null, endpoint, sleeper, new SteadyClock()); + + assertEquals("tok", token.accessToken); + assertEquals(3, endpoint.calls.size()); + // A pending answer must not change the cadence. + assertEquals(List.of(5L, 5L, 5L), sleeper.waits); + } + + @Test + void backsOffByFiveSecondsWhenAskedToSlowDown() { + FakeEndpoint endpoint = + new FakeEndpoint( + error(400, "slow_down"), + error(400, "slow_down"), + ok("{\"access_token\":\"tok\",\"token_type\":\"Bearer\",\"expires_in\":10}")); + RecordingSleeper sleeper = new RecordingSleeper(); + + FoundryOAuth.pollForToken("t", "dc", 5, 600, null, endpoint, sleeper, new SteadyClock()); + + // Each slow_down adds five seconds, and the increase persists into later polls. + assertEquals(List.of(5L, 10L, 15L), sleeper.waits); + } + + @Test + void neverPollsFasterThanTheProtocolFloor() { + FakeEndpoint endpoint = + new FakeEndpoint( + ok("{\"access_token\":\"tok\",\"token_type\":\"Bearer\",\"expires_in\":10}")); + RecordingSleeper sleeper = new RecordingSleeper(); + + // A provider that asks for a one-second cadence still gets the RFC 8628 minimum. + FoundryOAuth.pollForToken("t", "dc", 1, 600, null, endpoint, sleeper, new SteadyClock()); + + assertEquals(List.of(FoundryOAuth.MIN_POLL_INTERVAL_SECONDS), sleeper.waits); + } + + @Test + void reportsAnExpiredCodeInTermsTheUserCanActastOn() { + FakeEndpoint endpoint = new FakeEndpoint(error(400, "expired_token")); + + RuntimeException error = + assertThrows( + RuntimeException.class, + () -> + FoundryOAuth.pollForToken( + "t", "dc", 5, 600, null, endpoint, new RecordingSleeper(), new SteadyClock())); + + assertTrue( + String.valueOf(error.getMessage()).contains("expired"), + "the message should say the code expired: " + error.getMessage()); + } + + @Test + void surfacesAnUnrecognisedErrorRatherThanLoopingOnIt() { + FakeEndpoint endpoint = new FakeEndpoint(error(400, "access_denied")); + + RuntimeException error = + assertThrows( + RuntimeException.class, + () -> + FoundryOAuth.pollForToken( + "t", "dc", 5, 600, null, endpoint, new RecordingSleeper(), new SteadyClock())); + + assertTrue( + String.valueOf(error.getMessage()).contains("access_denied"), + "the provider's error code should reach the caller: " + error.getMessage()); + } + + @Test + void givesUpOnceTheDeadlinePasses() { + FakeEndpoint endpoint = + new FakeEndpoint(error(400, "authorization_pending"), error(400, "authorization_pending")); + // Each poll advances the clock by a minute, so a two-minute budget cannot survive three. + AdvancingClock clock = new AdvancingClock(60); + + RuntimeException error = + assertThrows( + RuntimeException.class, + () -> + FoundryOAuth.pollForToken( + "t", "dc", 5, 120, null, endpoint, new RecordingSleeper(), clock)); + + assertTrue( + String.valueOf(error.getMessage()).contains("timed out"), + "the message should say the flow timed out: " + error.getMessage()); + } + + @Test + void checksTheDeadlineBeforeTheFirstSleep() { + // A caller that passes an already-elapsed budget should not be made to wait first. + FakeEndpoint endpoint = new FakeEndpoint(); + RecordingSleeper sleeper = new RecordingSleeper(); + + assertThrows( + RuntimeException.class, + () -> + FoundryOAuth.pollForToken( + "t", "dc", 5, 0, null, endpoint, sleeper, new SteadyClock())); + + assertTrue(sleeper.waits.isEmpty(), "no sleep should happen when the budget is already spent"); + assertTrue(endpoint.calls.isEmpty(), "no request should be sent when the budget is spent"); + } + + @Test + void sendsTheDeviceGrantWithoutResendingTheScope() { + FakeEndpoint endpoint = + new FakeEndpoint( + ok("{\"access_token\":\"tok\",\"token_type\":\"Bearer\",\"expires_in\":10}")); + + FoundryOAuth.pollForToken( + "t", "the-code", 5, 600, "cid", endpoint, new RecordingSleeper(), new SteadyClock()); + + Map form = endpoint.calls.get(0); + assertEquals("cid", form.get("client_id")); + assertEquals("urn:ietf:params:oauth:grant-type:device_code", form.get("grant_type")); + assertEquals("the-code", form.get("device_code")); + // The scope was fixed when the device code was issued; resending it is at best redundant. + assertFalse(form.containsKey("scope"), "the poll must not resend the scope"); + } + + @Test + void anUnparseableErrorBodyIsReportedRatherThanTreatedAsPending() { + // Silently continuing here would turn a hard failure into a poll loop that never ends. + FakeEndpoint endpoint = new FakeEndpoint(new Http.FormResult(500, "gateway error")); + + RuntimeException error = + assertThrows( + RuntimeException.class, + () -> + FoundryOAuth.pollForToken( + "t", "dc", 5, 600, null, endpoint, new RecordingSleeper(), new SteadyClock())); + + assertTrue( + String.valueOf(error.getMessage()).contains("500"), + "the status should reach the caller: " + error.getMessage()); + } + + @Test + void aWellFormedBodyWithNoErrorCodeIsAlsoReported() { + // Distinct from the unparseable case: this body is valid JSON, so it gets past the parser and + // reaches the branch that decides what the failure means. Treating a missing code as "pending" + // would poll forever against a service that has already given its final answer. + FakeEndpoint endpoint = new FakeEndpoint(new Http.FormResult(400, "{\"unexpected\":\"shape\"}")); + + RuntimeException error = + assertThrows( + RuntimeException.class, + () -> + FoundryOAuth.pollForToken( + "t", "dc", 5, 600, null, endpoint, new RecordingSleeper(), new SteadyClock())); + + assertTrue( + String.valueOf(error.getMessage()).contains("400"), + "the status should reach the caller: " + error.getMessage()); + } + + @Test + void aFailureReportDoesNotEchoTheResponseBody() { + // This runs on a path that handles tokens. A body is reported by way of the parser's + // complaint about it, never copied out wholesale, so no future response shape can turn this + // message into a place credentials end up. + FakeEndpoint endpoint = + new FakeEndpoint(new Http.FormResult(400, "{\"access_token\":\"do-not-echo-me\"}")); + + RuntimeException error = + assertThrows( + RuntimeException.class, + () -> + FoundryOAuth.pollForToken( + "t", "dc", 5, 600, null, endpoint, new RecordingSleeper(), new SteadyClock())); + + assertFalse( + String.valueOf(error.getMessage()).contains("do-not-echo-me"), + "the body leaked into the message: " + error.getMessage()); + } + } + + // ------------------------------------------------------------------------------------------- + // Helpers + // ------------------------------------------------------------------------------------------- + + private static Http.FormResult ok(String body) { + return new Http.FormResult(200, body); + } + + private static Http.FormResult error(int status, String code) { + return new Http.FormResult(status, "{\"error\":\"" + code + "\"}"); + } + + private static Map queryOf(String url) { + Map pairs = new HashMap<>(); + String query = URI.create(url).getRawQuery(); + if (query == null) { + return pairs; + } + for (String part : query.split("&")) { + int equals = part.indexOf('='); + String key = equals < 0 ? part : part.substring(0, equals); + String value = equals < 0 ? "" : part.substring(equals + 1); + pairs.put(decode(key), decode(value)); + } + return pairs; + } + + private static String decode(String value) { + return java.net.URLDecoder.decode(value, StandardCharsets.UTF_8); + } + + /** A token endpoint that replays a fixed script and records what it was sent. */ + private static final class FakeEndpoint implements FoundryOAuth.TokenEndpoint { + + private final Deque responses = new ArrayDeque<>(); + final List> calls = new ArrayList<>(); + + FakeEndpoint(Http.FormResult... scripted) { + this.responses.addAll(List.of(scripted)); + } + + @Override + public Http.FormResult post(String url, Map form) { + calls.add(Map.copyOf(form)); + if (responses.isEmpty()) { + throw new AssertionError("the poll loop asked for more responses than were scripted"); + } + return responses.removeFirst(); + } + } + + /** Records the requested waits instead of performing them. */ + private static final class RecordingSleeper implements FoundryOAuth.Sleeper { + + final List waits = new ArrayList<>(); + + @Override + public void sleep(long seconds) { + waits.add(seconds); + } + } + + /** A clock that never advances, so only an explicit deadline check can fire. */ + private static final class SteadyClock implements FoundryOAuth.Clock { + + @Override + public long nanoTime() { + return 0; + } + } + + /** A clock that jumps forward a fixed number of seconds on every reading. */ + private static final class AdvancingClock implements FoundryOAuth.Clock { + + private final long stepSeconds; + private long readings; + + AdvancingClock(long stepSeconds) { + this.stepSeconds = stepSeconds; + } + + @Override + public long nanoTime() { + return readings++ * stepSeconds * 1_000_000_000L; + } + } +} diff --git a/runtime/java/prompty-openai/build.gradle.kts b/runtime/java/prompty-openai/build.gradle.kts new file mode 100644 index 000000000..fbd3a0d9d --- /dev/null +++ b/runtime/java/prompty-openai/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + id("java-library") +} + +dependencies { + api(project(":prompty")) + + testImplementation(platform("org.junit:junit-bom:5.11.4")) + testImplementation("org.junit.jupiter:junit-jupiter") + testImplementation(testFixtures(project(":prompty"))) + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} diff --git a/runtime/java/prompty-openai/src/main/java/com/microsoft/prompty/openai/OpenAIExecutor.java b/runtime/java/prompty-openai/src/main/java/com/microsoft/prompty/openai/OpenAIExecutor.java new file mode 100644 index 000000000..6d88668c0 --- /dev/null +++ b/runtime/java/prompty-openai/src/main/java/com/microsoft/prompty/openai/OpenAIExecutor.java @@ -0,0 +1,378 @@ +package com.microsoft.prompty.openai; + +import com.microsoft.prompty.CancellationToken; +import com.microsoft.prompty.Connections; +import com.microsoft.prompty.Environment; +import com.microsoft.prompty.Executor; +import com.microsoft.prompty.Http; +import com.microsoft.prompty.InvokerException; +import com.microsoft.prompty.Streams; +import com.microsoft.prompty.model.AnonymousConnection; +import com.microsoft.prompty.model.ApiKeyConnection; +import com.microsoft.prompty.model.Connection; +import com.microsoft.prompty.model.FoundryConnection; +import com.microsoft.prompty.model.InvocationContextPortability; +import com.microsoft.prompty.model.Message; +import com.microsoft.prompty.model.ModelInvocationRequest; +import com.microsoft.prompty.model.OAuthConnection; +import com.microsoft.prompty.model.Prompty; +import com.microsoft.prompty.model.RemoteConnection; +import com.microsoft.prompty.model.SaveContext; +import com.microsoft.prompty.model.ToolCall; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** Sends requests to the OpenAI chat, responses, embedding, and image endpoints. */ +public class OpenAIExecutor implements Executor { + + private static final String DEFAULT_ENDPOINT = "https://api.openai.com"; + + /** The provider name used in error messages and delegated-state lookups. */ + protected String providerName() { + return "openai"; + } + + @Override + public Object execute(Prompty agent, List messages) { + return executeRequest(agent, messages, null); + } + + @Override + public Object executeWithContext( + Prompty agent, ModelInvocationRequest request, CancellationToken cancellation) { + cancellation.throwIfCancelled( + "execution cancelled before " + providerName() + " provider invocation"); + Object result = executeRequest(agent, messagesOf(request), request); + // Checked again after the call because a cancellation that arrives mid-flight still means the + // caller no longer wants the result, even though the provider has already acted on it. + cancellation.throwIfCancelled( + "execution cancelled during " + providerName() + " provider invocation"); + return result; + } + + @Override + public Iterator executeStream(Prompty agent, List messages) { + return executeStreamRequest(agent, messages, null); + } + + @Override + public Iterator executeStreamWithContext( + Prompty agent, ModelInvocationRequest request, CancellationToken cancellation) { + cancellation.throwIfCancelled( + "streaming execution cancelled before " + providerName() + " provider invocation"); + return Streams.cancellable(executeStreamRequest(agent, messagesOf(request), request), cancellation); + } + + @Override + public List formatToolMessages( + Object rawResponse, List toolCalls, List toolResults, String textContent) { + if (OpenAIProcessor.isResponsesPayload(rawResponse)) { + return formatResponsesToolMessages( + Streams.pointer(rawResponse, "output"), toolCalls, toolResults); + } + return Wire.formatToolMessages(toolCalls, toolResults); + } + + @Override + public List formatStreamToolMessages( + List rawChunks, List toolCalls, List toolResults, String textContent) { + // A Responses stream is recognised by its event names; a chat stream carries none of them and + // replays through the ordinary chat shape. + for (Object chunk : rawChunks) { + if (Streams.pointer(chunk, "type") instanceof String type && type.startsWith("response.")) { + return formatResponsesToolMessages(streamedFunctionCalls(rawChunks), toolCalls, toolResults); + } + } + return Wire.formatToolMessages(toolCalls, toolResults); + } + + /** + * Rebuild the function-call items a Responses stream delivered piecewise. + * + *

The terminal {@code response.completed} event carries the authoritative list; the per-item + * events are used only when the stream ended before it arrived. + */ + private static Object streamedFunctionCalls(List rawChunks) { + Map byCallId = new LinkedHashMap<>(); + for (Object chunk : rawChunks) { + Object type = Streams.pointer(chunk, "type"); + if ("response.completed".equals(type)) { + Object output = Streams.pointer(chunk, "response", "output"); + if (output instanceof List items) { + for (Object item : items) { + if ("function_call".equals(Streams.pointer(item, "type"))) { + byCallId.put(String.valueOf(Streams.pointer(item, "call_id")), item); + } + } + } + } else if ("response.output_item.done".equals(type) || "response.output_item.added".equals(type)) { + Object item = Streams.pointer(chunk, "item"); + if ("function_call".equals(Streams.pointer(item, "type"))) { + byCallId.putIfAbsent(String.valueOf(Streams.pointer(item, "call_id")), item); + } + } + } + return new ArrayList(byCallId.values()); + } + + /** + * Replay a Responses tool round as conversation messages. + * + *

The provider's own function-call items are carried through untouched so a continuation can + * still match them, with one output message per result following. + */ + private static List formatResponsesToolMessages( + Object output, List toolCalls, List toolResults) { + List messages = new ArrayList<>(); + Map callsById = new LinkedHashMap<>(); + if (output instanceof List items) { + for (Object item : items) { + if ("function_call".equals(Streams.pointer(item, "type"))) { + callsById.put(String.valueOf(Streams.pointer(item, "call_id")), item); + } + } + } + + for (ToolCall call : toolCalls) { + Object item = callsById.get(call.id); + if (item != null) { + Message assistant = com.microsoft.prompty.Messages.withText( + com.microsoft.prompty.model.Role.ASSISTANT, ""); + com.microsoft.prompty.Messages.metadata(assistant).put("responses_function_call", item); + messages.add(assistant); + } + } + + for (int i = 0; i < toolCalls.size(); i++) { + messages.add( + com.microsoft.prompty.Messages.toolResult( + toolCalls.get(i).id, i < toolResults.size() ? toolResults.get(i) : "")); + } + return messages; + } + + // --------------------------------------------------------------- request + + private Object executeRequest( + Prompty agent, List messages, ModelInvocationRequest request) { + String apiType = apiType(agent); + Map body = buildRequestArgs(agent, messages, request); + String url = endpointFor(agent, apiType, false); + return Http.postJson(providerName(), url, authHeaders(agent), body); + } + + private Iterator executeStreamRequest( + Prompty agent, List messages, ModelInvocationRequest request) { + String apiType = apiType(agent); + Map body = buildRequestArgs(agent, messages, request); + String url = endpointFor(agent, apiType, true); + Wire.enableStreaming(body, apiType); + return Http.postSse(providerName(), url, authHeaders(agent), body); + } + + /** Build the request body without sending it. */ + public Map buildArgs(Prompty agent, List messages) { + return buildRequestArgs(agent, messages, null); + } + + private Map buildRequestArgs( + Prompty agent, List messages, ModelInvocationRequest request) { + String apiType = apiType(agent); + return switch (apiType) { + case "chat", "agent" -> Wire.buildChatArgs(agent, messages); + case "responses" -> buildResponsesRequestArgs(agent, messages, request); + case "embedding" -> Wire.buildEmbeddingArgs(agent, messages); + case "image" -> Wire.buildImageArgs(agent, messages); + default -> throw InvokerException.execute("Unsupported apiType: " + apiType); + }; + } + + /** + * Build a Responses request, continuing from provider-held state when that is still valid. + * + *

Continuing lets the provider keep the conversation prefix, so only the new turns are sent. + */ + private Map buildResponsesRequestArgs( + Prompty agent, List messages, ModelInvocationRequest request) { + Continuation continuation = responsesContinuation(request); + List input = messages; + + if (continuation != null) { + input = new ArrayList<>(messages.subList(continuation.messageCount(), messages.size())); + // The provider already holds its own function-call items; resending them would duplicate + // what the continuation is standing in for. + input.removeIf(Wire::isResponsesFunctionCall); + } + + Map args = Wire.buildResponsesArgs(agent, input); + if (continuation != null) { + args.put("previous_response_id", continuation.responseId()); + } + return args; + } + + private record Continuation(String responseId, int messageCount) {} + + /** + * The provider-held state this request may continue from, if any. + * + *

A response ID only stands for the exact message prefix that produced it. When the current + * conversation no longer starts with that prefix — a policy trimmed it, a hook rewrote it, + * compaction replaced it — continuing would silently mix two different contexts, so the request + * falls back to replaying the whole conversation. + */ + private Continuation responsesContinuation(ModelInvocationRequest request) { + if (request == null || request.context == null || request.context.contextState == null) { + return null; + } + var state = request.context.contextState; + if (state.portability != InvocationContextPortability.DELEGATED || state.delegatedState == null) { + return null; + } + + for (var delegated : state.delegatedState) { + if (!providerName().equals(delegated.provider) + || !"response".equals(delegated.kind) + || delegated.id == null + || delegated.id.isEmpty() + || delegated.metadata == null) { + continue; + } + Object boundary = + Streams.pointer( + delegated.metadata, OpenAIProcessor.RESPONSES_CONTINUATION_BOUNDARY, "inputMessages"); + if (!(boundary instanceof List recorded)) { + continue; + } + + List current = request.context.messages; + if (current == null || current.size() < recorded.size()) { + continue; + } + if (!prefixMatches(current, recorded)) { + continue; + } + return new Continuation(delegated.id, recorded.size()); + } + return null; + } + + /** + * Whether the conversation still begins with the recorded prefix. + * + *

Messages are compared through their saved form so that an equivalent message reconstructed + * from storage still matches, while an edited one does not. + */ + private static boolean prefixMatches(List current, List recorded) { + SaveContext context = new SaveContext(); + for (int i = 0; i < recorded.size(); i++) { + Object expected = recorded.get(i); + Object expectedSaved = expected instanceof Message message ? message.save(context) : expected; + if (!current.get(i).save(context).equals(expectedSaved)) { + return false; + } + } + return true; + } + + private static String apiType(Prompty agent) { + String apiType = agent == null || agent.model == null ? null : agent.model.apiType; + return apiType == null || apiType.isEmpty() ? "chat" : apiType; + } + + // -------------------------------------------------------------- endpoint + + private String endpointFor(Prompty agent, String apiType, boolean streaming) { + String path = + switch (apiType) { + case "chat", "agent" -> "/v1/chat/completions"; + case "responses" -> "/v1/responses"; + case "embedding" -> streaming ? null : "/v1/embeddings"; + case "image" -> streaming ? null : "/v1/images/generations"; + default -> null; + }; + if (path == null) { + throw InvokerException.execute( + (streaming ? "Streaming not supported for apiType: " : "Unsupported apiType: ") + apiType); + } + return buildUrl(agent, path); + } + + /** Resolve the base URL: the prompt's connection, then the environment, then the public API. */ + protected String buildUrl(Prompty agent, String path) { + String endpoint = endpointOf(connection(agent)); + if (endpoint == null || endpoint.isEmpty()) { + endpoint = Environment.lookup("OPENAI_BASE_URL").filter(v -> !v.isEmpty()).orElse(DEFAULT_ENDPOINT); + } + + String base = Connections.trimTrailingSlashes(endpoint); + // A proxy base is commonly written with the version already on it; appending another would + // produce /v1/v1/chat/completions. + if (base.endsWith("/v1") && path.startsWith("/v1")) { + return base + path.substring("/v1".length()); + } + return base + path; + } + + /** The headers that authenticate the request. */ + protected Map authHeaders(Prompty agent) { + return Map.of("Authorization", "Bearer " + apiKey(agent)); + } + + /** Resolve the API key from the prompt's connection, falling back to the environment. */ + protected String apiKey(Prompty agent) { + Connection connection = connection(agent); + if (connection instanceof ApiKeyConnection apiKey + && apiKey.apiKey != null + && !apiKey.apiKey.isEmpty()) { + return apiKey.apiKey; + } + return Environment.lookup("OPENAI_API_KEY") + .filter(key -> !key.isEmpty()) + .orElseThrow( + () -> + InvokerException.execute( + "No API key found. Set OPENAI_API_KEY or configure model.connection.apiKey")); + } + + /** The prompt's connection with any reference followed to the concrete one. */ + protected static Connection connection(Prompty agent) { + Connection connection = agent == null || agent.model == null ? null : agent.model.connection; + return connection == null ? null : Connections.resolve(connection); + } + + /** + * The endpoint a connection carries. + * + *

{@code endpoint} is declared per connection kind rather than on the base type, so each kind + * that has one is asked directly. + */ + protected static String endpointOf(Connection connection) { + if (connection instanceof ApiKeyConnection apiKey) { + return apiKey.endpoint; + } + if (connection instanceof AnonymousConnection anonymous) { + return anonymous.endpoint; + } + if (connection instanceof RemoteConnection remote) { + return remote.endpoint; + } + if (connection instanceof OAuthConnection oauth) { + return oauth.endpoint; + } + if (connection instanceof FoundryConnection foundry) { + return foundry.endpoint; + } + return null; + } + + private static List messagesOf(ModelInvocationRequest request) { + if (request == null || request.context == null || request.context.messages == null) { + return List.of(); + } + return request.context.messages; + } +} diff --git a/runtime/java/prompty-openai/src/main/java/com/microsoft/prompty/openai/OpenAIExtension.java b/runtime/java/prompty-openai/src/main/java/com/microsoft/prompty/openai/OpenAIExtension.java new file mode 100644 index 000000000..83a45c046 --- /dev/null +++ b/runtime/java/prompty-openai/src/main/java/com/microsoft/prompty/openai/OpenAIExtension.java @@ -0,0 +1,17 @@ +package com.microsoft.prompty.openai; + +import com.microsoft.prompty.PromptyExtension; + +/** + * Registers the OpenAI provider. + * + *

Discovered through {@code ServiceLoader}, so putting this module on the classpath is all it + * takes for {@code provider: openai} prompts to run — no registration call in application code. + */ +public final class OpenAIExtension implements PromptyExtension { + + @Override + public void register(Registrar registrar) { + registrar.executor("openai", new OpenAIExecutor()).processor("openai", new OpenAIProcessor()); + } +} diff --git a/runtime/java/prompty-openai/src/main/java/com/microsoft/prompty/openai/OpenAIModelLister.java b/runtime/java/prompty-openai/src/main/java/com/microsoft/prompty/openai/OpenAIModelLister.java new file mode 100644 index 000000000..d42b785ed --- /dev/null +++ b/runtime/java/prompty-openai/src/main/java/com/microsoft/prompty/openai/OpenAIModelLister.java @@ -0,0 +1,113 @@ +package com.microsoft.prompty.openai; + +import com.microsoft.prompty.Discovery; +import com.microsoft.prompty.Environment; +import com.microsoft.prompty.Http; +import com.microsoft.prompty.InvokerException; +import com.microsoft.prompty.model.ModelInfo; +import com.microsoft.prompty.model.ModelLister; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Lists the models an OpenAI connection can reach. */ +public final class OpenAIModelLister implements ModelLister { + + private static final String PROVIDER = "openai"; + + /** + * Map one raw {@code /v1/models} entry onto the provider-neutral contract. + * + *

This is the only place the OpenAI listing wire format is interpreted, and the shared + * discovery vectors exercise it directly so every runtime agrees on the result. + * + *

OpenAI reports an id and an owner and nothing else, so capabilities come from the shared + * dataset. That fill is provider-optional under the {@code ModelInfo} contract, which is why the + * discovery vectors deliberately use ids the dataset does not know: it leaves the wire mapping + * visible on its own. + */ + public static ModelInfo modelInfoFromWire(Object raw) { + ModelInfo info = new ModelInfo(); + if (!(raw instanceof Map map)) { + return info; + } + info.id = map.get("id") instanceof String id ? id : ""; + info.ownedBy = map.get("owned_by") instanceof String owner ? owner : null; + info.additionalProperties = copy(map); + Discovery.enrich(PROVIDER, info); + return info; + } + + /** Call {@code GET /v1/models} and map every entry it returns. */ + @Override + public List listModels(Object connection) { + Map config = connection instanceof Map map ? map : Map.of(); + requireKeyConnection(config); + + Object body = + Http.getJson( + PROVIDER, + modelsUrl(config), + Map.of("Authorization", "Bearer " + apiKey(config))); + + List models = new ArrayList<>(); + if (body instanceof Map map && map.get("data") instanceof Iterable data) { + for (Object entry : data) { + models.add(modelInfoFromWire(entry)); + } + } + return models; + } + + static String modelsUrl(Map connection) { + String endpoint = text(connection.get("endpoint")); + if (endpoint.isEmpty()) { + endpoint = Environment.lookup("OPENAI_BASE_URL").orElse(""); + } + if (endpoint.isEmpty()) { + endpoint = "https://api.openai.com"; + } + + String base = endpoint; + while (base.endsWith("/")) { + base = base.substring(0, base.length() - 1); + } + // An endpoint that already names the API version would otherwise produce /v1/v1/models. + return base.endsWith("/v1") ? base + "/models" : base + "/v1/models"; + } + + static String apiKey(Map connection) { + String key = text(connection.get("apiKey")); + if (key.isEmpty()) { + key = text(connection.get("api_key")); + } + if (key.isEmpty()) { + key = Environment.lookup("OPENAI_API_KEY").orElse(""); + } + if (key.isEmpty()) { + throw InvokerException.execute( + "No API key found. Set OPENAI_API_KEY or configure connection.apiKey"); + } + return key; + } + + private static void requireKeyConnection(Map connection) { + String kind = text(connection.get("kind")); + if (!"key".equals(kind)) { + throw InvokerException.execute( + "Connection kind '" + kind + "' is not supported for OpenAI model listing. Use 'key'."); + } + } + + private static String text(Object value) { + return value instanceof String s ? s : ""; + } + + private static Map copy(Map source) { + Map result = new java.util.LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } +} diff --git a/runtime/java/prompty-openai/src/main/java/com/microsoft/prompty/openai/OpenAIProcessor.java b/runtime/java/prompty-openai/src/main/java/com/microsoft/prompty/openai/OpenAIProcessor.java new file mode 100644 index 000000000..fc9258d82 --- /dev/null +++ b/runtime/java/prompty-openai/src/main/java/com/microsoft/prompty/openai/OpenAIProcessor.java @@ -0,0 +1,632 @@ +package com.microsoft.prompty.openai; + +import com.microsoft.prompty.InvokerException; +import com.microsoft.prompty.Messages; +import com.microsoft.prompty.Processor; +import com.microsoft.prompty.StreamFailure; +import com.microsoft.prompty.Streams; +import com.microsoft.prompty.model.DelegatedStateReference; +import com.microsoft.prompty.model.ErrorChunk; +import com.microsoft.prompty.model.InvocationContextPortability; +import com.microsoft.prompty.model.InvocationContextState; +import com.microsoft.prompty.model.InvocationUsage; +import com.microsoft.prompty.model.Message; +import com.microsoft.prompty.model.ModelInvocationRequest; +import com.microsoft.prompty.model.ModelInvocationResponse; +import com.microsoft.prompty.model.ModelToolRequest; +import com.microsoft.prompty.model.Prompty; +import com.microsoft.prompty.model.Role; +import com.microsoft.prompty.model.StreamChunk; +import com.microsoft.prompty.model.TextChunk; +import com.microsoft.prompty.model.ToolCall; +import com.microsoft.prompty.model.ToolChunk; +import com.microsoft.prompty.model.TypraJson; +import com.microsoft.prompty.model.UsageChunk; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; + +/** Extracts usable results from OpenAI chat, responses, embedding, and image payloads. */ +public class OpenAIProcessor implements Processor { + + /** + * Metadata key recording which message prefix a Responses continuation stands for. + * + *

A {@code previous_response_id} is only reusable against the exact conversation it was issued + * for. Recording that prefix from the immutable request snapshot is what lets a later turn tell + * whether the continuation still applies, instead of guessing from message history that may since + * have been edited or compacted. + */ + public static final String RESPONSES_CONTINUATION_BOUNDARY = "prompty.openai.responses.boundary"; + + /** The provider key this processor is registered under. */ + protected String providerName() { + return "openai"; + } + + /** + * Whether this provider's Responses API can restore server-side context. + * + *

Chat completions cannot: their response IDs identify a completion, not resumable state. + */ + protected boolean supportsResponsesContinuation() { + return true; + } + + @Override + public Object process(Prompty agent, Object response) { + return processResponse(agent, response); + } + + @Override + public ModelInvocationResponse processWithContext( + Prompty agent, Object response, ModelInvocationRequest request) { + return mapInvocationResponse(agent, response, request); + } + + @Override + public ModelInvocationResponse processRawWithContext( + Prompty agent, Object response, ModelInvocationRequest request) { + ModelInvocationResponse mapped = mapInvocationResponse(agent, response, request); + // Raw execution promised the provider's own words, so the interpreted output and the tool + // requests derived from it are replaced by the payload as received. + mapped.output = response; + mapped.toolRequests = new ArrayList<>(); + return mapped; + } + + @Override + public Iterator processStream(Prompty agent, Iterator response) { + return new StreamProcessor(response); + } + + // -------------------------------------------------------- invocation contract + + private ModelInvocationResponse mapInvocationResponse( + Prompty agent, Object response, ModelInvocationRequest request) { + Object output = processResponse(agent, response); + + List toolRequests = new ArrayList<>(); + for (ToolCall call : extractToolCalls(output)) { + ModelToolRequest toolRequest = new ModelToolRequest(); + toolRequest.id = call.id; + toolRequest.name = call.name; + // Arguments arrive as a JSON string. Decoding them here means downstream tool dispatch works + // with data; the raw string is kept when it is not valid JSON so nothing is silently lost. + Object parsed = tryParseJson(call.arguments); + toolRequest.arguments = parsed == null ? call.arguments : parsed; + toolRequests.add(toolRequest); + } + + InvocationContextState contextState = contextState(response, request); + ModelInvocationResponse mapped = new ModelInvocationResponse(); + mapped.output = toolRequests.isEmpty() ? output : null; + mapped.usage = invocationUsage(response); + mapped.assistantMessages = + contextState.portability == InvocationContextPortability.PORTABLE + ? portableAssistantMessages(response) + : new ArrayList<>(); + mapped.toolRequests = toolRequests; + mapped.nextContextState = contextState; + return mapped; + } + + private InvocationContextState contextState(Object response, ModelInvocationRequest request) { + InvocationContextState state = new InvocationContextState(); + state.delegatedState = new ArrayList<>(); + + String responseId = supportsResponsesContinuation() ? responsesId(response) : null; + if (responseId == null) { + state.portability = InvocationContextPortability.PORTABLE; + return state; + } + + DelegatedStateReference reference = new DelegatedStateReference(); + reference.provider = providerName(); + reference.kind = "response"; + reference.id = responseId; + + List messages = + request == null || request.context == null ? List.of() : request.context.messages; + Map boundary = new LinkedHashMap<>(); + boundary.put("inputMessages", messages == null ? List.of() : messages); + reference.metadata = Map.of(RESPONSES_CONTINUATION_BOUNDARY, boundary); + + state.portability = InvocationContextPortability.DELEGATED; + state.delegatedState.add(reference); + return state; + } + + private static String responsesId(Object response) { + if (!isResponsesPayload(response)) { + return null; + } + Object id = Streams.pointer(response, "id"); + return id instanceof String text && !text.isEmpty() ? text : null; + } + + private List portableAssistantMessages(Object response) { + List messages = new ArrayList<>(); + + if (isResponsesPayload(response)) { + for (Object item : asList(Streams.pointer(response, "output"))) { + if ("function_call".equals(Streams.pointer(item, "type"))) { + Message call = Messages.withText(Role.ASSISTANT, ""); + // The provider's own function-call item is preserved verbatim: replaying a synthesised + // equivalent would lose the identifiers it matches its own state against. + Messages.metadata(call).put("responses_function_call", item); + messages.add(call); + } + } + if (!messages.isEmpty()) { + return messages; + } + messages.add(Messages.assistant(stringOrEmpty(Streams.pointer(response, "output_text")))); + return messages; + } + + Object message = Streams.pointer(response, "choices", 0, "message"); + if (message == null) { + return messages; + } + Message assistant = Messages.assistant(stringOrEmpty(Streams.pointer(message, "content"))); + Object toolCalls = Streams.pointer(message, "tool_calls"); + if (toolCalls instanceof List calls) { + Messages.metadata(assistant).put("tool_calls", new ArrayList(calls)); + } + messages.add(assistant); + return messages; + } + + private static InvocationUsage invocationUsage(Object response) { + return usageFrom(Streams.pointer(response, "usage")); + } + + private static InvocationUsage usageFrom(Object value) { + Long input = asLong(firstNonNull(Streams.pointer(value, "input_tokens"), Streams.pointer(value, "prompt_tokens"))); + Long output = + asLong(firstNonNull(Streams.pointer(value, "output_tokens"), Streams.pointer(value, "completion_tokens"))); + if (input == null || output == null) { + return null; + } + Long total = asLong(Streams.pointer(value, "total_tokens")); + InvocationUsage usage = new InvocationUsage(); + usage.inputTokens = input; + usage.outputTokens = output; + usage.totalTokens = total == null ? input + output : total; + return usage; + } + + // ------------------------------------------------------------- dispatch + + /** Interpret a raw OpenAI payload, dispatching on its shape. */ + public static Object processResponse(Prompty agent, Object response) { + if (isResponsesPayload(response)) { + return processResponsesApi(agent, response); + } + + Object choices = Streams.pointer(response, "choices"); + if (choices instanceof List list) { + return processChatCompletion(agent, list); + } + + Object data = Streams.pointer(response, "data"); + if ("list".equals(Streams.pointer(response, "object")) && data instanceof List list) { + return processEmbedding(list); + } + + if (data instanceof List list) { + for (Object item : list) { + if (Streams.pointer(item, "url") != null || Streams.pointer(item, "b64_json") != null) { + return processImage(list); + } + } + } + + // An unrecognised shape is handed back untouched rather than forced into a guess; the caller + // can still see everything the provider sent. + return response; + } + + private static Object processChatCompletion(Prompty agent, List choices) { + if (choices.isEmpty()) { + throw InvokerException.process("Empty choices array"); + } + Object message = Streams.pointer(choices.get(0), "message"); + if (message == null) { + throw InvokerException.process("Missing message in choice"); + } + + // A tool call outranks any content the model also produced: it is a request for work, and + // treating it as prose would strand the turn. + Object toolCalls = Streams.pointer(message, "tool_calls"); + if (toolCalls instanceof List calls && !calls.isEmpty()) { + List normalized = new ArrayList<>(); + for (Object call : calls) { + Object function = firstNonNull(Streams.pointer(call, "function"), call); + normalized.add( + Map.of( + "id", stringOrEmpty(Streams.pointer(call, "id")), + "name", stringOrEmpty(Streams.pointer(function, "name")), + "arguments", stringOr(Streams.pointer(function, "arguments"), "{}"))); + } + return normalized; + } + + Object content = Streams.pointer(message, "content"); + if (content == null) { + Object refusal = Streams.pointer(message, "refusal"); + if (refusal instanceof String text) { + return text; + } + } + return structuredOrText(agent, stringOrEmpty(content)); + } + + private static Object processResponsesApi(Prompty agent, Object response) { + List toolCalls = new ArrayList<>(); + for (Object item : asList(Streams.pointer(response, "output"))) { + if ("function_call".equals(Streams.pointer(item, "type"))) { + toolCalls.add( + Map.of( + "id", stringOrEmpty(Streams.pointer(item, "call_id")), + "name", stringOrEmpty(Streams.pointer(item, "name")), + "arguments", stringOr(Streams.pointer(item, "arguments"), "{}"))); + } + } + if (!toolCalls.isEmpty()) { + return toolCalls; + } + return structuredOrText(agent, stringOrEmpty(Streams.pointer(response, "output_text"))); + } + + /** + * Decode declared structured output, falling back to the raw text. + * + *

Falling back rather than failing keeps a malformed reply visible to the caller, who is better + * placed to decide whether to retry than a processor throwing on the model's behalf. + */ + private static Object structuredOrText(Prompty agent, String text) { + if (agent != null && agent.outputs != null && !agent.outputs.isEmpty()) { + Object parsed = tryParseJson(text); + if (parsed != null) { + return parsed; + } + } + return text; + } + + private static Object processEmbedding(List data) { + List vectors = new ArrayList<>(); + for (Object item : data) { + Object embedding = Streams.pointer(item, "embedding"); + if (embedding != null) { + vectors.add(embedding); + } + } + return vectors.size() == 1 ? vectors.get(0) : vectors; + } + + private static Object processImage(List data) { + List images = new ArrayList<>(); + for (Object item : data) { + // A URL is preferred when both are present; base64 is the fallback for providers or settings + // that never issue one. + images.add(firstNonNull(Streams.pointer(item, "url"), Streams.pointer(item, "b64_json"))); + } + return images.size() == 1 ? images.get(0) : images; + } + + /** Recognise tool calls in an already-processed output. */ + public static List extractToolCalls(Object output) { + List calls = new ArrayList<>(); + if (!(output instanceof List list)) { + return calls; + } + for (Object item : list) { + if (!(Streams.pointer(item, "id") instanceof String id) + || !(Streams.pointer(item, "name") instanceof String name) + || !(Streams.pointer(item, "arguments") instanceof String arguments)) { + return new ArrayList<>(); + } + ToolCall call = new ToolCall(); + call.id = id; + call.name = name; + call.arguments = arguments; + calls.add(call); + } + return calls; + } + + // ------------------------------------------------------------ streaming + + /** + * Turns OpenAI's server-sent events into typed chunks. + * + *

Text is forwarded as it arrives; tool calls are accumulated across deltas and emitted once + * complete, because a half-assembled call is not something a caller can act on. Usage comes last, + * so a consumer that stops at the first tool call has still seen every chunk that matters. + */ + private static final class StreamProcessor implements Iterator, java.io.Closeable { + + private final Iterator source; + /** Partial tool calls keyed by their wire index, which is how deltas identify themselves. */ + private final Map partialCalls = new java.util.TreeMap<>(); + private final Map itemSlots = new LinkedHashMap<>(); + + private final List pending = new ArrayList<>(); + private InvocationUsage usage; + private boolean drained; + private boolean finished; + + StreamProcessor(Iterator source) { + this.source = source; + } + + @Override + public boolean hasNext() { + advance(); + return !pending.isEmpty(); + } + + @Override + public StreamChunk next() { + advance(); + if (pending.isEmpty()) { + throw new NoSuchElementException(); + } + return pending.remove(0); + } + + private void advance() { + while (pending.isEmpty() && !finished) { + if (source.hasNext()) { + consume(source.next()); + } else if (!drained) { + drain(); + } else { + finished = true; + } + } + } + + private void consume(Object chunk) { + Object error = Streams.pointer(chunk, "error"); + if (error != null) { + String message = stringOr(Streams.pointer(error, "message"), "OpenAI stream failed"); + // A transport failure leaves the request's fate unknown — the provider may have completed + // it after the connection dropped — so it is reported separately from a decided error. + pending.add( + "sse_transport_error".equals(Streams.pointer(error, "type")) + ? StreamFailure.indeterminate(message) + : StreamFailure.determinate(message)); + finish(); + return; + } + + if (consumeResponsesEvent(chunk)) { + return; + } + + Object usageValue = Streams.pointer(chunk, "usage"); + if (usageValue != null) { + InvocationUsage parsed = usageFrom(usageValue); + if (parsed != null) { + usage = parsed; + } + } + + Object delta = Streams.pointer(chunk, "choices", 0, "delta"); + if (delta == null) { + return; + } + + if (Streams.pointer(delta, "content") instanceof String content && !content.isEmpty()) { + pending.add(textChunk(content)); + } + + for (Object toolDelta : asList(Streams.pointer(delta, "tool_calls"))) { + Long index = asLong(Streams.pointer(toolDelta, "index")); + ToolCall call = partialCalls.computeIfAbsent(index == null ? 0 : index.intValue(), key -> new ToolCall()); + if (Streams.pointer(toolDelta, "id") instanceof String id) { + call.id = id; + } + if (Streams.pointer(toolDelta, "function", "name") instanceof String name) { + call.name = name; + } + if (Streams.pointer(toolDelta, "function", "arguments") instanceof String arguments) { + call.arguments = call.arguments + arguments; + } + } + + // Spec §10.3: a refusal ends the stream. Continuing would let a partial answer look complete. + if (Streams.pointer(delta, "refusal") instanceof String refusal && !refusal.isEmpty()) { + pending.add(errorChunk("Model refused: " + refusal)); + finish(); + } + } + + /** Handle a Responses API event, reporting whether the chunk was one. */ + private boolean consumeResponsesEvent(Object chunk) { + if (!(Streams.pointer(chunk, "type") instanceof String type)) { + return false; + } + switch (type) { + case "response.output_text.delta" -> { + if (Streams.pointer(chunk, "delta") instanceof String text && !text.isEmpty()) { + pending.add(textChunk(text)); + } + } + case "response.output_item.added", "response.output_item.done" -> { + Object item = Streams.pointer(chunk, "item"); + if ("function_call".equals(Streams.pointer(item, "type"))) { + Long index = asLong(Streams.pointer(chunk, "output_index")); + int slot = index == null ? partialCalls.size() : index.intValue(); + partialCalls.put(slot, toolCallFrom(item)); + // Argument events identify their target by item id, which is distinct from the call id + // the tool result must later be correlated with, so both have to be remembered. + if (Streams.pointer(item, "id") instanceof String itemId && !itemId.isEmpty()) { + itemSlots.put(itemId, slot); + } + } + } + case "response.function_call_arguments.delta" -> { + applyArguments(chunk, Streams.pointer(chunk, "delta"), false); + } + case "response.function_call_arguments.done" -> { + applyArguments(chunk, Streams.pointer(chunk, "arguments"), true); + } + case "response.completed" -> { + InvocationUsage parsed = usageFrom(Streams.pointer(chunk, "response", "usage")); + if (parsed != null) { + usage = parsed; + } + // The terminal event carries the authoritative output, which supersedes anything + // accumulated from deltas that may have been truncated. + List output = asList(Streams.pointer(chunk, "response", "output")); + for (int i = 0; i < output.size(); i++) { + Object item = output.get(i); + if ("function_call".equals(Streams.pointer(item, "type"))) { + partialCalls.put(i, toolCallFrom(item)); + } + } + } + case "response.refusal.delta" -> { + if (Streams.pointer(chunk, "delta") instanceof String refusal && !refusal.isEmpty()) { + pending.add(errorChunk("Model refused: " + refusal)); + finish(); + } + } + default -> { + return false; + } + } + return true; + } + + /** + * Route an argument fragment to the call it belongs to. + * + *

The event identifies its target inconsistently: {@code call_id} where the provider echoes + * it, otherwise {@code item_id}, otherwise only the output index. Trying all three keeps + * arguments from being silently dropped, which would leave the model's tool call unusable. + */ + private void applyArguments(Object chunk, Object arguments, boolean replace) { + if (!(arguments instanceof String text)) { + return; + } + + ToolCall target = null; + if (Streams.pointer(chunk, "call_id") instanceof String callId && !callId.isEmpty()) { + for (ToolCall call : partialCalls.values()) { + if (callId.equals(call.id)) { + target = call; + break; + } + } + } + if (target == null && Streams.pointer(chunk, "item_id") instanceof String itemId) { + Integer slot = itemSlots.get(itemId); + target = slot == null ? null : partialCalls.get(slot); + } + if (target == null) { + Long index = asLong(Streams.pointer(chunk, "output_index")); + target = index == null ? null : partialCalls.get(index.intValue()); + } + if (target == null) { + return; + } + + target.arguments = replace ? text : target.arguments + text; + } + + private void drain() { + drained = true; + for (ToolCall call : partialCalls.values()) { + ToolChunk chunk = new ToolChunk(); + chunk.toolCall = call; + pending.add(chunk); + } + if (usage != null) { + UsageChunk chunk = new UsageChunk(); + chunk.usage = usage; + pending.add(chunk); + } + } + + /** + * Stop after a terminal chunk, discarding anything the provider may still be sending. + * + *

The provider is mid-send, so the connection is released rather than read to completion: + * whatever follows a refusal or a fatal error is not something a caller may act on. + */ + private void finish() { + drained = true; + finished = true; + close(); + } + + @Override + public void close() { + Streams.close(source); + } + } + + private static ToolCall toolCallFrom(Object item) { + ToolCall call = new ToolCall(); + call.id = stringOrEmpty(Streams.pointer(item, "call_id")); + call.name = stringOrEmpty(Streams.pointer(item, "name")); + call.arguments = stringOrEmpty(Streams.pointer(item, "arguments")); + return call; + } + + private static TextChunk textChunk(String value) { + TextChunk chunk = new TextChunk(); + chunk.value = value; + return chunk; + } + + private static ErrorChunk errorChunk(String message) { + return StreamFailure.determinate(message); + } + + // ------------------------------------------------------------- helpers + + static boolean isResponsesPayload(Object response) { + return "response".equals(Streams.pointer(response, "object")); + } + + private static Object tryParseJson(String text) { + if (text == null || text.isEmpty()) { + return null; + } + try { + return TypraJson.parse(text); + } catch (RuntimeException e) { + return null; + } + } + + private static List asList(Object value) { + return value instanceof List list ? new ArrayList(list) : List.of(); + } + + private static Object firstNonNull(Object first, Object second) { + return first != null ? first : second; + } + + private static Long asLong(Object value) { + return value instanceof Number number ? number.longValue() : null; + } + + private static String stringOrEmpty(Object value) { + return stringOr(value, ""); + } + + private static String stringOr(Object value, String fallback) { + return value instanceof String text ? text : fallback; + } +} diff --git a/runtime/java/prompty-openai/src/main/java/com/microsoft/prompty/openai/SchemaException.java b/runtime/java/prompty-openai/src/main/java/com/microsoft/prompty/openai/SchemaException.java new file mode 100644 index 000000000..7444e7a09 --- /dev/null +++ b/runtime/java/prompty-openai/src/main/java/com/microsoft/prompty/openai/SchemaException.java @@ -0,0 +1,30 @@ +package com.microsoft.prompty.openai; + +/** + * Raised when a portable {@code Property} schema cannot be expressed in the JSON Schema subset + * OpenAI accepts. + * + *

Failing here rather than at the API boundary keeps the error attributable: the prompt author + * wrote a schema the provider will not take, and the message says which construct was at fault. + */ +public class SchemaException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public SchemaException(String message) { + super(message); + } + + /** A union that declares neither branch list, or declares both. */ + public static SchemaException invalidUnion() { + return new SchemaException( + "UnionProperty must contain exactly one non-empty `oneOf` or `anyOf` array"); + } + + /** A union expressed as {@code oneOf}, which OpenAI does not support. */ + public static SchemaException unsupportedOneOf() { + return new SchemaException( + "OpenAI schemas do not support UnionProperty.oneOf; use the provider-supported anyOf" + + " composition"); + } +} diff --git a/runtime/java/prompty-openai/src/main/java/com/microsoft/prompty/openai/Wire.java b/runtime/java/prompty-openai/src/main/java/com/microsoft/prompty/openai/Wire.java new file mode 100644 index 000000000..b868f129c --- /dev/null +++ b/runtime/java/prompty-openai/src/main/java/com/microsoft/prompty/openai/Wire.java @@ -0,0 +1,640 @@ +package com.microsoft.prompty.openai; + +import com.microsoft.prompty.Messages; +import com.microsoft.prompty.model.ArrayProperty; +import com.microsoft.prompty.model.AudioPart; +import com.microsoft.prompty.model.Binding; +import com.microsoft.prompty.model.ContentPart; +import com.microsoft.prompty.model.FilePart; +import com.microsoft.prompty.model.FunctionTool; +import com.microsoft.prompty.model.ImagePart; +import com.microsoft.prompty.model.Message; +import com.microsoft.prompty.model.ModelOptions; +import com.microsoft.prompty.model.ObjectProperty; +import com.microsoft.prompty.model.Property; +import com.microsoft.prompty.model.Prompty; +import com.microsoft.prompty.model.TextPart; +import com.microsoft.prompty.model.Tool; +import com.microsoft.prompty.model.ToolCall; +import com.microsoft.prompty.model.TypraJson; +import com.microsoft.prompty.model.UnionProperty; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Conversion between Prompty's portable types and the JSON bodies the OpenAI APIs expect. + * + *

Everything here is a pure function of the agent and its messages, with no network or client + * state, which is what lets the shared {@code spec/vectors/wire} suite grade it directly. + * + *

Option names are not mapped here. {@link ModelOptions#toWire(String)} is generated from the + * same TypeSpec every runtime shares, so {@code maxOutputTokens → max_completion_tokens} is decided + * once in the model rather than re-decided per language. + */ +public final class Wire { + + private Wire() {} + + // ------------------------------------------------------------ messages + + /** Convert a message to the OpenAI chat wire shape. */ + public static Map messageToWire(Message message) { + Map wire = new LinkedHashMap<>(); + wire.put("role", roleName(message)); + + // Metadata carries the fields that live beside content on the wire — tool_call_id, tool_calls, + // name. Role and content are excluded because they are produced from the message proper. + for (Map.Entry entry : Messages.metadata(message).entrySet()) { + if (!"role".equals(entry.getKey()) && !"content".equals(entry.getKey())) { + wire.put(entry.getKey(), entry.getValue()); + } + } + + Object content = Messages.toTextContent(message); + if (content instanceof String) { + wire.put("content", content); + } else { + List parts = new ArrayList<>(); + if (message.parts != null) { + for (ContentPart part : message.parts) { + parts.add(partToWire(part)); + } + } + wire.put("content", parts); + } + return wire; + } + + private static String roleName(Message message) { + return message.role == null ? "user" : message.role.value; + } + + private static Map partToWire(ContentPart part) { + if (part instanceof TextPart text) { + return Map.of("type", "text", "text", text.value == null ? "" : text.value); + } + if (part instanceof ImagePart image) { + Map url = new LinkedHashMap<>(); + url.put("url", image.source); + if (image.detail != null) { + url.put("detail", image.detail); + } + return Map.of("type", "image_url", "image_url", url); + } + if (part instanceof AudioPart audio) { + Map input = new LinkedHashMap<>(); + input.put("data", audio.source); + input.put("format", mimeToAudioFormat(audio.mediaType)); + return Map.of("type", "input_audio", "input_audio", input); + } + if (part instanceof FilePart file) { + return Map.of("type", "file", "file", Map.of("url", file.source)); + } + return Map.of("type", "text", "text", ""); + } + + /** Map an audio MIME type onto the short format name OpenAI expects. */ + static String mimeToAudioFormat(String mime) { + if (mime == null) { + return "wav"; + } + return switch (mime) { + case "audio/wav", "audio/x-wav" -> "wav"; + case "audio/mpeg", "audio/mp3" -> "mp3"; + case "audio/mp4" -> "mp4"; + case "audio/ogg" -> "ogg"; + case "audio/flac" -> "flac"; + case "audio/webm" -> "webm"; + case "audio/pcm" -> "pcm"; + // Spec §7.1.2: an unmapped audio type falls back to its subtype. + default -> mime.startsWith("audio/") ? mime.substring("audio/".length()) : "wav"; + }; + } + + // ------------------------------------------------------- request bodies + + /** Build the request body for a chat completions call. */ + public static Map buildChatArgs(Prompty agent, List messages) { + Map args = new LinkedHashMap<>(); + args.put("model", modelId(agent, "")); + + List wireMessages = new ArrayList<>(); + for (Message message : messages) { + wireMessages.add(messageToWire(message)); + } + args.put("messages", wireMessages); + + applyOptions(args, options(agent), "openai"); + + List tools = toolsToWire(agent); + if (!tools.isEmpty()) { + args.put("tools", tools); + } + + Map responseFormat = outputSchemaToWire(agent); + if (responseFormat != null) { + args.put("response_format", responseFormat); + } + return args; + } + + /** + * Turn a chat or agent request body into a streaming one. + * + *

{@code include_usage} is requested so the terminal event carries token counts; without it a + * streamed call reports no usage at all and cost tracking silently loses those turns. Every + * provider that speaks the OpenAI wire format shares this helper so their usage reporting cannot + * drift apart. + */ + public static void enableStreaming(Map body, String apiType) { + body.put("stream", true); + if ("chat".equals(apiType) || "agent".equals(apiType)) { + body.put("stream_options", Map.of("include_usage", true)); + } + } + + /** Build the request body for an embedding call. */ + public static Map buildEmbeddingArgs(Prompty agent, List messages) { + Map args = new LinkedHashMap<>(); + args.put("model", modelId(agent, "text-embedding-ada-002")); + args.put("input", extractTextInput(messages)); + applyAdditionalProperties(args, options(agent), true); + return args; + } + + /** Build the request body for an image generation call. */ + public static Map buildImageArgs(Prompty agent, List messages) { + Object input = extractTextInput(messages); + String prompt; + if (input instanceof List items) { + List texts = new ArrayList<>(); + for (Object item : items) { + if (item instanceof String text) { + texts.add(text); + } + } + prompt = String.join(" ", texts); + } else { + prompt = input instanceof String text ? text : ""; + } + + Map args = new LinkedHashMap<>(); + args.put("model", modelId(agent, "dall-e-3")); + args.put("prompt", prompt); + applyAdditionalProperties(args, options(agent), true); + return args; + } + + private static Object extractTextInput(List messages) { + List texts = new ArrayList<>(); + for (Message message : messages) { + String text = Messages.text(message); + if (text != null && !text.isEmpty()) { + texts.add(text); + } + } + return texts.size() == 1 ? texts.get(0) : new ArrayList(texts); + } + + private static String modelId(Prompty agent, String fallback) { + String id = agent == null || agent.model == null ? null : agent.model.id; + return id == null || id.isEmpty() ? fallback : id; + } + + private static ModelOptions options(Prompty agent) { + return agent == null || agent.model == null ? null : agent.model.options; + } + + // ------------------------------------------------------------- options + + private static void applyOptions( + Map args, ModelOptions options, String provider) { + if (options == null) { + return; + } + for (Map.Entry entry : options.toWire(provider).entrySet()) { + if (entry.getValue() != null) { + args.put(entry.getKey(), narrowFloat(entry.getValue())); + } + } + applyAdditionalProperties(args, options, false); + } + + /** + * Merge provider-specific passthrough options. + * + * @param overwrite whether a passthrough key may replace one the mapped options already set. + * Chat requests keep the mapped value, because a declared option is more specific than a + * passthrough; embedding and image requests have no mapped options to defend. + */ + private static void applyAdditionalProperties( + Map args, ModelOptions options, boolean overwrite) { + if (options == null || options.additionalProperties == null) { + return; + } + for (Map.Entry entry : options.additionalProperties.entrySet()) { + if (overwrite || !args.containsKey(entry.getKey())) { + args.put(entry.getKey(), entry.getValue()); + } + } + } + + /** + * Render a 32-bit option value as the decimal the author wrote. + * + *

{@code temperature} is a float in the model, so widening 0.7 to a double exposes the binary + * approximation as 0.699999988079071. Formatting through {@code Float} and re-parsing recovers + * the shortest decimal that round-trips, which is what every other runtime puts on the wire. + */ + private static Object narrowFloat(Object value) { + if (value instanceof Float floatValue) { + return Double.parseDouble(Float.toString(floatValue)); + } + return value; + } + + // --------------------------------------------------------------- tools + + /** Convert the agent's function tools to OpenAI's nested wire shape. */ + public static List toolsToWire(Prompty agent) { + List wire = new ArrayList<>(); + for (FunctionTool tool : functionTools(agent)) { + Map definition = functionDefinition(tool, false); + wire.add(Map.of("type", "function", "function", definition)); + } + return wire; + } + + private static List functionTools(Prompty agent) { + List functions = new ArrayList<>(); + List tools = agent == null ? null : agent.tools; + if (tools == null) { + return functions; + } + for (Tool tool : tools) { + if (tool instanceof FunctionTool function) { + functions.add(function); + } + } + return functions; + } + + /** + * Build a function definition body. + * + * @param flat whether to emit the Responses API's flat shape, which carries {@code type} beside + * the name instead of nesting the definition under a {@code function} key + */ + private static Map functionDefinition(FunctionTool tool, boolean flat) { + Map definition = new LinkedHashMap<>(); + if (flat) { + definition.put("type", "function"); + } + definition.put("name", tool.name); + if (tool.description != null) { + definition.put("description", tool.description); + } + + boolean strict = Boolean.TRUE.equals(tool.strict); + Map schema = parametersToJsonSchema(unboundParameters(tool), strict); + definition.put("parameters", schema); + + if (strict) { + definition.put("strict", true); + schema.put("additionalProperties", false); + } + return definition; + } + + /** + * The parameters the model is allowed to fill in. + * + *

Spec §7.1.3: a bound parameter is supplied by the caller, so exposing it would invite the + * model to argue with the binding. + */ + private static List unboundParameters(FunctionTool tool) { + Set bound = new HashSet<>(); + if (tool.bindings != null) { + for (Binding binding : tool.bindings) { + bound.add(binding.name); + } + } + List parameters = new ArrayList<>(); + if (tool.parameters != null) { + for (Property property : tool.parameters) { + if (!bound.contains(property.name)) { + parameters.add(property); + } + } + } + return parameters; + } + + // -------------------------------------------------------- JSON Schema + + private static Map parametersToJsonSchema( + List parameters, boolean strict) { + Map properties = new LinkedHashMap<>(); + List required = new ArrayList<>(); + + for (Property parameter : parameters) { + boolean isRequired = Boolean.TRUE.equals(parameter.required); + properties.put(parameter.name, propertySchema(parameter, !isRequired, strict)); + // Strict mode requires every key to be listed; optionality is expressed by nullability + // instead, which is the shape OpenAI documents for structured outputs. + if (strict || isRequired) { + required.add(parameter.name); + } + } + + Map schema = new LinkedHashMap<>(); + schema.put("type", "object"); + schema.put("properties", properties); + if (!required.isEmpty()) { + schema.put("required", required); + } + return schema; + } + + private static Map propertySchema( + Property property, boolean optional, boolean strict) { + Map schema = propertySchema(property, strict); + if (strict && optional && !Boolean.TRUE.equals(property.nullable)) { + addNullability(schema); + } + return schema; + } + + private static Map propertySchema(Property property, boolean strict) { + Map schema = new LinkedHashMap<>(); + String jsonType = kindToJsonType(property.kind); + if (jsonType != null) { + schema.put("type", jsonType); + } + if (property.description != null) { + schema.put("description", property.description); + } + if (property.enumValues != null) { + schema.put("enum", new ArrayList(property.enumValues)); + } + + if (property instanceof ArrayProperty array) { + if (array.items != null) { + schema.put("items", propertySchema(array.items, strict)); + } + } else if (property instanceof ObjectProperty object) { + if (object.properties != null && !object.properties.isEmpty()) { + Map nested = new LinkedHashMap<>(); + List required = new ArrayList<>(); + for (Property child : object.properties) { + if (child.name == null || child.name.isEmpty()) { + continue; + } + boolean isRequired = Boolean.TRUE.equals(child.required); + nested.put(child.name, propertySchema(child, !isRequired, strict)); + // Strict mode applies at every depth, not just the top level: OpenAI rejects a schema + // whose nested object omits a key from `required`. Optionality survives as nullability, + // added by the sibling overload above, so `border` becomes ["string", "null"] rather + // than disappearing from the list. + if (strict || isRequired) { + required.add(child.name); + } + } + schema.put("properties", nested); + if (!required.isEmpty()) { + schema.put("required", required); + } + schema.put("additionalProperties", false); + } + } else if (property instanceof UnionProperty union) { + boolean hasOneOf = union.oneOf != null && !union.oneOf.isEmpty(); + boolean hasAnyOf = union.anyOf != null && !union.anyOf.isEmpty(); + if (hasOneOf && !hasAnyOf) { + throw SchemaException.unsupportedOneOf(); + } + if (!hasAnyOf || hasOneOf) { + throw SchemaException.invalidUnion(); + } + List branches = new ArrayList<>(); + for (Property branch : union.anyOf) { + branches.add(propertySchema(branch, strict)); + } + schema.put("anyOf", branches); + } + + if (Boolean.TRUE.equals(property.nullable)) { + addNullability(schema); + } + return schema; + } + + /** + * Widen a schema to admit null. + * + *

Which form that takes depends on what the schema already says: a plain type becomes a type + * union, an existing {@code anyOf} gains a null branch, and anything else is wrapped. An empty + * schema is left alone — it already admits null, and {@code {"anyOf": [{}, {"type": "null"}]}} + * would only be noise. + */ + private static void addNullability(Map schema) { + Object type = schema.get("type"); + if (type instanceof String typeName) { + schema.remove("type"); + Map reordered = new LinkedHashMap<>(schema); + schema.clear(); + schema.put("type", List.of(typeName, "null")); + schema.putAll(reordered); + } else if (schema.get("anyOf") instanceof List branches) { + List widened = new ArrayList<>(branches); + widened.add(Map.of("type", "null")); + schema.put("anyOf", widened); + } else if (!schema.isEmpty()) { + Map inner = new LinkedHashMap<>(schema); + schema.clear(); + schema.put("anyOf", List.of(inner, Map.of("type", "null"))); + } + + if (schema.get("enum") instanceof List values && !values.contains(null)) { + List widened = new ArrayList<>(values); + widened.add(null); + schema.put("enum", widened); + } + } + + private static String kindToJsonType(String kind) { + if (kind == null) { + return null; + } + return switch (kind) { + case "string" -> "string"; + case "integer" -> "integer"; + case "float", "number" -> "number"; + case "boolean" -> "boolean"; + case "array" -> "array"; + case "object" -> "object"; + default -> null; + }; + } + + // -------------------------------------------------- structured output + + private static Map outputSchemaToWire(Prompty agent) { + Map schema = outputObjectSchema(agent); + if (schema == null) { + return null; + } + return Map.of( + "type", + "json_schema", + "json_schema", + Map.of("name", "structured_output", "strict", true, "schema", schema)); + } + + private static Map outputObjectSchema(Prompty agent) { + List outputs = agent == null ? null : agent.outputs; + if (outputs == null || outputs.isEmpty()) { + return null; + } + + Map properties = new LinkedHashMap<>(); + List required = new ArrayList<>(); + for (Property output : outputs) { + properties.put( + output.name, propertySchema(output, !Boolean.TRUE.equals(output.required), true)); + required.add(output.name); + } + + Map schema = new LinkedHashMap<>(); + schema.put("type", "object"); + schema.put("properties", properties); + if (!required.isEmpty()) { + schema.put("required", required); + } + schema.put("additionalProperties", false); + return schema; + } + + // ------------------------------------------------------- Responses API + + /** + * Build the request body for the Responses API. + * + *

System and developer turns are lifted out of the conversation into {@code instructions}, + * which is where that API expects them; everything else becomes an input item. + */ + public static Map buildResponsesArgs(Prompty agent, List messages) { + List instructions = new ArrayList<>(); + List input = new ArrayList<>(); + + for (Message message : messages) { + String role = roleName(message); + if ("system".equals(role) || "developer".equals(role)) { + instructions.add(Messages.text(message)); + } else { + input.add(messageToResponsesInput(message)); + } + } + + Map args = new LinkedHashMap<>(); + args.put("model", modelId(agent, "gpt-4o")); + args.put("input", input); + if (!instructions.isEmpty()) { + args.put("instructions", String.join("\n\n", instructions)); + } + + applyOptions(args, options(agent), "responses"); + + List tools = new ArrayList<>(); + for (FunctionTool tool : functionTools(agent)) { + tools.add(functionDefinition(tool, true)); + } + if (!tools.isEmpty()) { + args.put("tools", tools); + } + + Map schema = outputObjectSchema(agent); + if (schema != null) { + args.put( + "text", + Map.of( + "format", + Map.of( + "type", "json_schema", + "name", "structured_output", + "schema", schema, + "strict", true))); + } + return args; + } + + private static Object messageToResponsesInput(Message message) { + Map metadata = Messages.metadata(message); + + // A function call the provider already owns is replayed verbatim; re-deriving it would lose + // the identifiers the provider matches its own output against. + Object functionCall = metadata.get("responses_function_call"); + if (functionCall != null) { + return functionCall; + } + + Object content = Messages.toTextContent(message); + Object callId = metadata.get(Messages.TOOL_CALL_ID); + if (callId != null) { + String output = content instanceof String text ? text : TypraJson.stringify(content); + return Map.of("type", "function_call_output", "call_id", callId, "output", output); + } + + String role = roleName(message); + Map item = new LinkedHashMap<>(); + // The Responses API has no tool role; a tool turn that is not a function_call_output is + // ordinary caller-supplied text. + item.put("role", "tool".equals(role) ? "user" : role); + item.put("content", content); + return item; + } + + /** Whether a durable message holds a provider-owned Responses function-call item. */ + public static boolean isResponsesFunctionCall(Message message) { + return Messages.metadata(message).get("responses_function_call") != null; + } + + // -------------------------------------------------------- agent loop + + /** + * Render a completed round of tool calls back into conversation messages. + * + *

One assistant message recording what was called, then one tool message per result — the + * order OpenAI requires, and the order a replayed conversation has to reproduce. + */ + public static List formatToolMessages(List toolCalls, List results) { + List messages = new ArrayList<>(); + + List wireCalls = new ArrayList<>(); + for (ToolCall call : toolCalls) { + wireCalls.add( + Map.of( + "id", call.id, + "type", "function", + "function", Map.of("name", call.name, "arguments", call.arguments))); + } + + Message assistant = Messages.assistant(""); + Messages.metadata(assistant).put("tool_calls", wireCalls); + messages.add(assistant); + + for (int i = 0; i < toolCalls.size(); i++) { + ToolCall call = toolCalls.get(i); + // A missing result still needs its message: OpenAI rejects a conversation where a requested + // tool call has no answer, so an empty answer beats an absent one. + String result = i < results.size() ? results.get(i) : ""; + Message message = Messages.toolResult(call.id, result); + Messages.metadata(message).put("name", call.name); + messages.add(message); + } + return messages; + } +} diff --git a/runtime/java/prompty-openai/src/main/resources/META-INF/services/com.microsoft.prompty.PromptyExtension b/runtime/java/prompty-openai/src/main/resources/META-INF/services/com.microsoft.prompty.PromptyExtension new file mode 100644 index 000000000..be9c6fe23 --- /dev/null +++ b/runtime/java/prompty-openai/src/main/resources/META-INF/services/com.microsoft.prompty.PromptyExtension @@ -0,0 +1 @@ +com.microsoft.prompty.openai.OpenAIExtension \ No newline at end of file diff --git a/runtime/java/prompty-openai/src/test/java/com/microsoft/prompty/openai/DiscoveryVectorsTest.java b/runtime/java/prompty-openai/src/test/java/com/microsoft/prompty/openai/DiscoveryVectorsTest.java new file mode 100644 index 000000000..c684b8b69 --- /dev/null +++ b/runtime/java/prompty-openai/src/test/java/com/microsoft/prompty/openai/DiscoveryVectorsTest.java @@ -0,0 +1,62 @@ +package com.microsoft.prompty.openai; + +import com.microsoft.prompty.SpecVectors; +import com.microsoft.prompty.model.ModelInfo; +import com.microsoft.prompty.model.SaveContext; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; + +/** + * Grades the OpenAI half of the shared discovery and enrichment suites. + * + *

The discovery vectors pin the wire mapping; the enrichment vectors pin the shared capability + * dataset and the fill-only-missing rule that applies it. Both are the same fixtures every other + * runtime is measured by. + */ +class DiscoveryVectorsTest { + + @TestFactory + Iterable discoveryVectors() { + List tests = new ArrayList<>(); + for (Map vector : SpecVectors.readCases("discovery/discovery_vectors.json", "vectors")) { + if (!"openai".equals(vector.get("provider"))) { + continue; + } + String name = SpecVectors.string(vector, "name"); + tests.add( + DynamicTest.dynamicTest( + name, + () -> { + ModelInfo actual = + OpenAIModelLister.modelInfoFromWire(SpecVectors.map(vector, "input")); + SpecVectors.assertEquivalent( + name, vector.get("expected"), actual.save(new SaveContext())); + })); + } + return tests; + } + + @TestFactory + Iterable enrichmentVectors() { + List tests = new ArrayList<>(); + for (Map vector : SpecVectors.readCases("discovery/enrichment_vectors.json", "vectors")) { + if (!"openai".equals(vector.get("provider"))) { + continue; + } + String name = SpecVectors.string(vector, "name"); + tests.add( + DynamicTest.dynamicTest( + name, + () -> { + ModelInfo info = ModelInfo.load(SpecVectors.map(vector, "input"), null); + com.microsoft.prompty.Discovery.enrich("openai", info); + SpecVectors.assertEquivalent( + name, vector.get("expected"), info.save(new SaveContext())); + })); + } + return tests; + } +} diff --git a/runtime/java/prompty-openai/src/test/java/com/microsoft/prompty/openai/OpenAILiveTest.java b/runtime/java/prompty-openai/src/test/java/com/microsoft/prompty/openai/OpenAILiveTest.java new file mode 100644 index 000000000..7f44420a6 --- /dev/null +++ b/runtime/java/prompty-openai/src/test/java/com/microsoft/prompty/openai/OpenAILiveTest.java @@ -0,0 +1,364 @@ +package com.microsoft.prompty.openai; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.microsoft.prompty.Environment; +import com.microsoft.prompty.LiveEnv; +import com.microsoft.prompty.Pipeline; +import com.microsoft.prompty.Registry; +import com.microsoft.prompty.TurnOptions; +import com.microsoft.prompty.model.Message; +import com.microsoft.prompty.model.ModelInfo; +import com.microsoft.prompty.model.Prompty; +import com.microsoft.prompty.model.StreamChunk; +import com.microsoft.prompty.model.TextChunk; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +/** + * End-to-end coverage against the real OpenAI API. + * + *

Every other suite grades the runtime against recorded fixtures, which proves the runtime agrees + * with the shared spec but not that the spec matches what OpenAI actually accepts. These tests close + * that gap: they send real requests and assert on real responses, so a wire shape that the fixtures + * bless but the service rejects fails here. + * + *

Excluded from the normal build by the {@code live} tag. Run with {@code -PliveTests} and an + * {@code OPENAI_API_KEY} in {@code runtime/java/.env}. + */ +@Tag("live") +@DisplayName("live: OpenAI") +final class OpenAILiveTest { + + @BeforeAll + static void setUp() { + LiveEnv.load(); + } + + private static String modelId() { + return LiveEnv.get("OPENAI_MODEL", "gpt-4o-mini"); + } + + private static Prompty chatAgent(String question, Map options) { + return LiveEnv.agent( + new LiveEnv.Spec("openai", modelId()) + .chat("You are a helpful assistant. Be very brief.", question) + .options(options)); + } + + // ------------------------------------------------------------------ chat + + @Test + void chatCompletionReturnsText() { + LiveEnv.require("OPENAI_API_KEY"); + + Object result = + Pipeline.invoke( + chatAgent("Say hello in exactly 3 words.", Map.of("temperature", 0, "maxOutputTokens", 100)), + Map.of()); + + String text = Pipeline.textOf(result); + assertNotNull(text); + assertFalse(text.isBlank(), "chat completion returned no text"); + System.out.println("[openai] chat -> " + text); + } + + @Test + void chatHonoursDeterministicTemperature() { + LiveEnv.require("OPENAI_API_KEY"); + + Object result = + Pipeline.invoke( + chatAgent( + "What is 2+2? Reply with just the number.", + Map.of("temperature", 0, "maxOutputTokens", 10)), + Map.of()); + + String text = Pipeline.textOf(result); + assertTrue(text.contains("4"), "expected the answer to contain 4 but got: " + text); + System.out.println("[openai] temperature -> " + text); + } + + @Test + void chatStreamingAccumulatesIntoTheSameAnswerShape() { + LiveEnv.require("OPENAI_API_KEY"); + + Map options = new LinkedHashMap<>(); + options.put("temperature", 0); + options.put("maxOutputTokens", 60); + // `stream` is not a declared model option; it rides in the passthrough bag, which is also where + // the executor looks for it. + options.put("additionalProperties", Map.of("stream", true)); + Prompty agent = chatAgent("Count from 1 to 5, separated by spaces.", options); + + // `invoke` deliberately collapses a stream into the finished answer, so a caller who only wants + // the result does not have to know whether the transport streamed. + Object result = Pipeline.invoke(agent, Map.of()); + + String text = assertInstanceOf(String.class, result, "invoke should collapse a stream to text"); + assertFalse(text.isBlank(), "stream produced no text"); + assertTrue(text.contains("5"), "expected the counted answer to reach 5 but got: " + text); + System.out.println("[openai] stream via invoke -> " + text); + } + + @Test + void chatStreamingYieldsIncrementalChunksOverSse() { + LiveEnv.require("OPENAI_API_KEY"); + + Map options = new LinkedHashMap<>(); + options.put("temperature", 0); + options.put("maxOutputTokens", 60); + options.put("additionalProperties", Map.of("stream", true)); + Prompty agent = chatAgent("Count from 1 to 10, separated by spaces.", options); + + // Driving the executor and processor directly is what proves the SSE parser copes with real + // chunk boundaries; going through `invoke` would hide that behind the accumulated string. + List messages = Pipeline.prepare(agent, Map.of()); + Iterator raw = Registry.executor("openai").executeStream(agent, messages); + Iterator chunks = Registry.processor("openai").processStream(agent, raw); + + StringBuilder text = new StringBuilder(); + int textChunks = 0; + while (chunks.hasNext()) { + StreamChunk chunk = chunks.next(); + if (chunk instanceof TextChunk t) { + textChunks++; + text.append(t.value); + } + } + + assertTrue(textChunks > 1, "expected more than one text chunk, got " + textChunks); + assertTrue(text.toString().contains("10"), "expected the counted answer to reach 10: " + text); + System.out.println("[openai] stream over sse -> " + textChunks + " chunks: " + text); + } + + // ------------------------------------------------------------------ other api types + + @Test + void embeddingReturnsAVector() { + LiveEnv.require("OPENAI_API_KEY"); + + Prompty agent = + LiveEnv.agent( + new LiveEnv.Spec("openai", LiveEnv.get("OPENAI_EMBEDDING_MODEL", "text-embedding-3-small")) + .apiType("embedding") + .instructions("user:\nThe quick brown fox.")); + + Object result = Pipeline.invoke(agent, Map.of()); + + List vector = assertInstanceOf(List.class, result, "embedding should yield a list"); + assertFalse(vector.isEmpty(), "embedding vector was empty"); + assertInstanceOf(Number.class, vector.get(0), "embedding values should be numeric"); + System.out.println("[openai] embedding -> " + vector.size() + " dimensions"); + } + + @Test + void imageGenerationReturnsAReference() { + LiveEnv.require("OPENAI_API_KEY"); + + Prompty agent = + LiveEnv.agent( + new LiveEnv.Spec("openai", LiveEnv.get("OPENAI_IMAGE_MODEL", "gpt-image-1-mini")) + .apiType("image") + .instructions("user:\nA small red circle on a white background.") + .options(Map.of("additionalProperties", Map.of("size", "1024x1024")))); + + Object result; + try { + result = Pipeline.invoke(agent, Map.of()); + } catch (RuntimeException e) { + // Image models are a separate entitlement. A key without one says nothing about the runtime, + // so that specific rejection skips; every other rejection is a real failure and propagates. + String message = String.valueOf(e.getMessage()); + Assumptions.assumeFalse( + message.contains("does not exist") || message.contains("must be verified"), + () -> "live test skipped: this key has no image model entitlement -- " + message); + throw e; + } + + assertNotNull(result, "image generation returned nothing"); + String rendered = String.valueOf(result); + assertFalse(rendered.isBlank(), "image generation returned an empty reference"); + System.out.println( + "[openai] image -> " + rendered.substring(0, Math.min(80, rendered.length())) + "..."); + } + + @Test + void listModelsReturnsTheConfiguredModel() { + LiveEnv.require("OPENAI_API_KEY"); + + Map connection = new LinkedHashMap<>(); + connection.put("kind", "key"); + List models = new OpenAIModelLister().listModels(connection); + + assertFalse(models.isEmpty(), "expected at least one model"); + assertTrue( + models.stream().anyMatch(m -> m.id != null && !m.id.isBlank()), + "every listed model should carry an id"); + System.out.println("[openai] models -> " + models.size() + " available"); + } + + // ------------------------------------------------------------------ structured output + + @Test + void structuredOutputParsesIntoDeclaredFields() { + LiveEnv.require("OPENAI_API_KEY"); + + Prompty agent = + LiveEnv.agent( + new LiveEnv.Spec("openai", modelId()) + .chat( + "Extract structured data from the user's message.", + "My name is Ada Lovelace and I am 36 years old.") + .options(Map.of("temperature", 0)) + .outputs( + List.of( + Map.of("name", "name", "kind", "string", "required", true), + Map.of("name", "age", "kind", "integer", "required", true)))); + + Object result = Pipeline.invoke(agent, Map.of()); + + Map fields = assertInstanceOf(Map.class, result, "structured output should yield a map"); + assertTrue(fields.containsKey("name"), "missing declared field 'name' in " + fields); + assertTrue(fields.containsKey("age"), "missing declared field 'age' in " + fields); + assertTrue( + String.valueOf(fields.get("name")).contains("Ada"), + "expected the extracted name to mention Ada but got: " + fields.get("name")); + System.out.println("[openai] structured -> " + fields); + } + + /** + * A nested optional field is the shape that motivated recursive strict widening: OpenAI rejects a + * strict schema whose nested object leaves a key out of {@code required}, so this asserts against + * the service what {@code WireSchemaTest} asserts against the wire builder. The optional key is + * genuinely absent from the prompt, so a faithful schema must let the model return null for it + * rather than force a fabricated value. + */ + @Test + void structuredOutputHandlesNestedOptionalFields() { + LiveEnv.require("OPENAI_API_KEY"); + + Prompty agent = + LiveEnv.agent( + new LiveEnv.Spec("openai", modelId()) + .chat( + "Extract structured data from the user's message.", + "Ada Lovelace lives in London. Her postcode is not mentioned.") + .options(Map.of("temperature", 0)) + .outputs( + List.of( + Map.of("name", "name", "kind", "string", "required", true), + Map.of( + "name", + "address", + "kind", + "object", + "required", + true, + "properties", + List.of( + Map.of("name", "city", "kind", "string", "required", true), + Map.of( + "name", "postcode", "kind", "string", "required", false)))))); + + Object result = Pipeline.invoke(agent, Map.of()); + + Map fields = assertInstanceOf(Map.class, result, "structured output should yield a map"); + Map address = + assertInstanceOf(Map.class, fields.get("address"), "missing nested object in " + fields); + assertTrue( + String.valueOf(address.get("city")).toLowerCase().contains("london"), + "expected the nested required field to be extracted but got: " + address); + assertTrue( + address.containsKey("postcode"), + "strict mode names every nested key, so the optional one must still come back: " + address); + System.out.println("[openai] nested structured -> " + fields); + } + + // ------------------------------------------------------------------ tool calling + + @Test + void agentTurnCallsAToolAndUsesTheResult() { + LiveEnv.require("OPENAI_API_KEY"); + + Prompty agent = + LiveEnv.agent( + new LiveEnv.Spec("openai", modelId()) + .apiType("agent") + .chat( + "You are a helpful assistant. Use the provided tools when they apply.", + "What is the weather in Paris? Use the get_weather tool.") + .options(Map.of("temperature", 0)) + .tools(List.of(weatherTool()))); + + boolean[] called = {false}; + TurnOptions options = + TurnOptions.builder() + .tool( + "get_weather", + arguments -> { + called[0] = true; + System.out.println("[openai] tool invoked with " + arguments); + return "{\"temperature\":\"18C\",\"conditions\":\"cloudy\"}"; + }) + .build(); + + Object result = Pipeline.turn(agent, Map.of(), options); + + assertTrue(called[0], "the model never called the tool"); + String text = Pipeline.textOf(result); + assertFalse(text.isBlank(), "the turn produced no final text"); + assertTrue( + text.contains("18") || text.toLowerCase().contains("cloud"), + "expected the answer to reflect the tool result but got: " + text); + System.out.println("[openai] agent -> " + text); + } + + /** A minimal function tool, shaped the way the TypeSpec model declares parameters. */ + static Map weatherTool() { + Map location = new LinkedHashMap<>(); + location.put("name", "location"); + location.put("kind", "string"); + location.put("description", "The city to report on"); + location.put("required", true); + + Map tool = new LinkedHashMap<>(); + tool.put("name", "get_weather"); + tool.put("kind", "function"); + tool.put("description", "Get the current weather for a city"); + tool.put("parameters", List.of(location)); + return tool; + } + + @Test + void anInvalidKeyIsReportedRatherThanSwallowed() { + LiveEnv.require("OPENAI_API_KEY"); + + String saved = Environment.lookup("OPENAI_API_KEY").orElse(""); + Environment.set("OPENAI_API_KEY", "sk-definitely-not-a-real-key"); + try { + Object result = + Pipeline.invoke(chatAgent("Hello", Map.of("temperature", 0, "maxOutputTokens", 5)), Map.of()); + // A rejected request must surface as a failure, not as a plausible-looking empty answer. + throw new AssertionError("expected an authentication failure but got: " + result); + } catch (AssertionError e) { + throw e; + } catch (RuntimeException e) { + String message = String.valueOf(e.getMessage()); + assertFalse(message.contains("sk-definitely-not-a-real-key"), "the key must not appear in the error"); + System.out.println("[openai] auth failure -> " + message); + } finally { + Environment.set("OPENAI_API_KEY", saved); + } + } +} diff --git a/runtime/java/prompty-openai/src/test/java/com/microsoft/prompty/openai/OpenAIStreamingTest.java b/runtime/java/prompty-openai/src/test/java/com/microsoft/prompty/openai/OpenAIStreamingTest.java new file mode 100644 index 000000000..d7e3a0784 --- /dev/null +++ b/runtime/java/prompty-openai/src/test/java/com/microsoft/prompty/openai/OpenAIStreamingTest.java @@ -0,0 +1,312 @@ +package com.microsoft.prompty.openai; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.microsoft.prompty.Messages; +import com.microsoft.prompty.StreamFailure; +import com.microsoft.prompty.model.ErrorChunk; +import com.microsoft.prompty.model.LoadContext; +import com.microsoft.prompty.model.Message; +import com.microsoft.prompty.model.Prompty; +import com.microsoft.prompty.model.Role; +import com.microsoft.prompty.model.StreamChunk; +import com.microsoft.prompty.model.TextChunk; +import com.microsoft.prompty.model.ToolCall; +import com.microsoft.prompty.model.ToolChunk; +import com.microsoft.prompty.model.UsageChunk; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +/** + * Behaviour the shared vectors do not reach: streaming assembly and tool-turn replay. + * + *

Both are stateful across many wire events, so they cannot be expressed as a single + * request/response fixture, yet both decide whether a multi-turn conversation works at all. + */ +class OpenAIStreamingTest { + + private static Prompty agent() { + return Prompty.load( + Map.of( + "name", "test", + "kind", "prompt", + "instructions", "test", + "model", Map.of("id", "gpt-4", "provider", "openai")), + new LoadContext()); + } + + private static List drain(List chunks) { + Iterator processed = + new OpenAIProcessor().processStream(agent(), chunks.iterator()); + List result = new ArrayList<>(); + processed.forEachRemaining(result::add); + return result; + } + + private static Object chatDelta(Object delta) { + return Map.of("choices", List.of(Map.of("delta", delta))); + } + + @Test + void textDeltasSurfaceInOrder() { + List chunks = + drain( + List.of( + chatDelta(Map.of("content", "Hello")), + chatDelta(Map.of("content", " ")), + chatDelta(Map.of("content", "world")))); + + assertEquals(3, chunks.size()); + assertEquals( + "Hello world", + chunks.stream() + .map(c -> assertInstanceOf(TextChunk.class, c).value) + .reduce("", String::concat)); + } + + @Test + void toolCallDeltasAreAssembledAndEmittedInWireOrder() { + List chunks = + drain( + List.of( + // Deliberately interleaved and out of index order, as real streams arrive. + chatDelta( + Map.of( + "tool_calls", + List.of( + Map.of( + "index", 1, + "id", "call_b", + "function", Map.of("name", "second", "arguments", "{\"x\""))))), + chatDelta( + Map.of( + "tool_calls", + List.of( + Map.of( + "index", 0, + "id", "call_a", + "function", Map.of("name", "first", "arguments", "{}"))))), + chatDelta( + Map.of( + "tool_calls", + List.of(Map.of("index", 1, "function", Map.of("arguments", ":1}"))))), + Map.of("usage", Map.of("prompt_tokens", 10, "completion_tokens", 5)))); + + List calls = + chunks.stream() + .filter(ToolChunk.class::isInstance) + .map(c -> ((ToolChunk) c).toolCall) + .toList(); + + assertEquals(2, calls.size()); + assertEquals("call_a", calls.get(0).id); + assertEquals("first", calls.get(0).name); + assertEquals("call_b", calls.get(1).id); + // Fragments arriving across chunks must concatenate into valid JSON. + assertEquals("{\"x\":1}", calls.get(1).arguments); + + UsageChunk usage = + (UsageChunk) chunks.stream().filter(UsageChunk.class::isInstance).findFirst().orElseThrow(); + assertEquals(Long.valueOf(10), usage.usage.inputTokens); + assertEquals(Long.valueOf(15), usage.usage.totalTokens); + } + + @Test + void refusalEndsTheStreamRatherThanContinuing() { + List chunks = + drain( + List.of( + chatDelta(Map.of("content", "thinking")), + chatDelta(Map.of("refusal", "I cannot help with that")), + // Anything after a refusal must not be surfaced. + chatDelta(Map.of("content", "leaked")))); + + ErrorChunk error = + (ErrorChunk) chunks.stream().filter(ErrorChunk.class::isInstance).findFirst().orElseThrow(); + assertTrue(error.message.contains("I cannot help with that")); + assertFalse( + chunks.stream() + .anyMatch(c -> c instanceof TextChunk text && "leaked".equals(text.value))); + } + + @Test + void transportErrorsAreIndeterminateWhileProtocolErrorsAreNot() { + List transport = + drain(List.of(Map.of("error", Map.of("type", "sse_transport_error", "message", "reset")))); + List protocol = + drain(List.of(Map.of("error", Map.of("type", "invalid_request", "message", "bad")))); + + // A connection that dropped mid-stream may or may not have been acted on upstream, so a retry + // is unsafe; a rejected request definitively did nothing and can be retried. + assertTrue(StreamFailure.isIndeterminate(transport.get(0))); + assertFalse(StreamFailure.isIndeterminate(protocol.get(0))); + } + + @Test + void responsesEventsAssembleFunctionCalls() { + List chunks = + drain( + List.of( + Map.of( + "type", "response.output_item.added", + "output_index", 0, + "item", + Map.of("type", "function_call", "id", "fc_1", "call_id", "call_1", "name", + "get_weather")), + // The live API identifies argument events by item id, not call id. + Map.of( + "type", "response.function_call_arguments.delta", + "item_id", "fc_1", + "output_index", 0, + "delta", "{\"city\":"), + Map.of( + "type", "response.function_call_arguments.done", + "item_id", "fc_1", + "output_index", 0, + "arguments", "{\"city\":\"Seattle\"}"))); + + ToolCall call = + chunks.stream() + .filter(ToolChunk.class::isInstance) + .map(c -> ((ToolChunk) c).toolCall) + .findFirst() + .orElseThrow(); + + assertEquals("get_weather", call.name); + assertEquals("call_1", call.id); + // `.done` carries the authoritative arguments and replaces whatever the deltas accumulated. + assertEquals("{\"city\":\"Seattle\"}", call.arguments); + } + + @Test + void responsesArgumentEventsRouteByCallIdItemIdOrIndex() { + // Providers label these events inconsistently. Whichever identifier is present must work, or + // the tool call arrives with empty arguments and the turn fails for no visible reason. + List added = + List.of( + Map.of( + "type", "response.output_item.added", + "output_index", 0, + "item", + Map.of("type", "function_call", "id", "fc_1", "call_id", "call_1", "name", "f"))); + + for (Map identifier : + List.>of( + Map.of("call_id", "call_1"), + Map.of("item_id", "fc_1"), + Map.of("output_index", 0))) { + Map event = new java.util.LinkedHashMap<>(identifier); + event.put("type", "response.function_call_arguments.done"); + event.put("arguments", "{\"ok\":true}"); + + List stream = new ArrayList<>(added); + stream.add(event); + + ToolCall call = + drain(stream).stream() + .filter(ToolChunk.class::isInstance) + .map(c -> ((ToolChunk) c).toolCall) + .findFirst() + .orElseThrow(); + assertEquals("{\"ok\":true}", call.arguments, "routed by " + identifier.keySet()); + } + } + + @Test + void toolResultsReplayAsAnAssistantCallFollowedByToolMessages() { + ToolCall call = new ToolCall(); + call.id = "call_1"; + call.name = "get_weather"; + call.arguments = "{\"city\":\"Seattle\"}"; + + List replay = + new OpenAIExecutor() + .formatToolMessages( + Map.of("choices", List.of(Map.of("message", Map.of()))), + List.of(call), + List.of("72F"), + ""); + + assertEquals(2, replay.size()); + assertEquals(Role.ASSISTANT, replay.get(0).role); + assertNotNull(Messages.metadata(replay.get(0)).get("tool_calls")); + + // The result must carry the id the model asked with, or the next turn cannot correlate it. + assertEquals(Role.TOOL, replay.get(1).role); + assertEquals("call_1", Messages.metadata(replay.get(1)).get(Messages.TOOL_CALL_ID)); + assertEquals("get_weather", Messages.metadata(replay.get(1)).get("name")); + assertEquals("72F", Messages.text(replay.get(1))); + } + + @Test + void everyToolCallGetsAnAnswerEvenWhenOneIsMissing() { + ToolCall first = new ToolCall(); + first.id = "call_1"; + first.name = "a"; + ToolCall second = new ToolCall(); + second.id = "call_2"; + second.name = "b"; + + List replay = + new OpenAIExecutor() + .formatToolMessages( + Map.of("choices", List.of(Map.of("message", Map.of()))), + List.of(first, second), + List.of("only one result"), + ""); + + // OpenAI rejects a conversation where a requested call has no answer, so a short results list + // must still produce a message per call rather than silently dropping the tail. + assertEquals(3, replay.size()); + assertEquals("call_2", Messages.metadata(replay.get(2)).get(Messages.TOOL_CALL_ID)); + } + + @Test + void terminatingEarlyReleasesTheUnderlyingStream() { + // A refusal ends the exchange while the provider is still sending; the connection has to be + // released rather than left open waiting for content nobody may act on. + CloseTrackingIterator source = + new CloseTrackingIterator( + List.of( + chatDelta(Map.of("refusal", "no")), + chatDelta(Map.of("content", "never read"))) + .iterator()); + + Iterator processed = new OpenAIProcessor().processStream(agent(), source); + processed.forEachRemaining(chunk -> {}); + + assertTrue(source.closed, "a terminated stream must release its source"); + } + + /** An iterator that records whether it was closed, standing in for a live connection. */ + private static final class CloseTrackingIterator implements Iterator, java.io.Closeable { + private final Iterator delegate; + boolean closed; + + CloseTrackingIterator(Iterator delegate) { + this.delegate = delegate; + } + + @Override + public boolean hasNext() { + return delegate.hasNext(); + } + + @Override + public Object next() { + return delegate.next(); + } + + @Override + public void close() { + closed = true; + } + } +} diff --git a/runtime/java/prompty-openai/src/test/java/com/microsoft/prompty/openai/ProcessVectorsTest.java b/runtime/java/prompty-openai/src/test/java/com/microsoft/prompty/openai/ProcessVectorsTest.java new file mode 100644 index 000000000..31e321e23 --- /dev/null +++ b/runtime/java/prompty-openai/src/test/java/com/microsoft/prompty/openai/ProcessVectorsTest.java @@ -0,0 +1,44 @@ +package com.microsoft.prompty.openai; + +import com.microsoft.prompty.SpecVectors; +import com.microsoft.prompty.VectorAgents; +import com.microsoft.prompty.model.Prompty; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; + +/** Grades OpenAI response interpretation against the shared {@code spec/vectors/process} suite. */ +class ProcessVectorsTest { + + @TestFactory + Iterable processVectors() { + List tests = new ArrayList<>(); + for (Map vector : SpecVectors.readArray("process/process_vectors.json")) { + String name = SpecVectors.string(vector, "name"); + Map input = SpecVectors.map(vector, "input"); + + if (!"openai".equals(input.get("provider"))) { + continue; + } + + tests.add( + DynamicTest.dynamicTest( + name, + () -> { + Prompty agent = VectorAgents.buildProcessAgent(input, "gpt-4", "openai"); + Object actual = OpenAIProcessor.processResponse(agent, input.get("response")); + Object expected = SpecVectors.map(vector, "expected").get("result"); + + // A response with nothing to say and a response that said nothing are the same + // outcome to a caller; the fixtures spell one of them as an empty string. + if ("".equals(expected) && (actual == null || "".equals(actual))) { + return; + } + SpecVectors.assertEquivalent(name, expected, actual); + })); + } + return tests; + } +} diff --git a/runtime/java/prompty-openai/src/test/java/com/microsoft/prompty/openai/WireSchemaTest.java b/runtime/java/prompty-openai/src/test/java/com/microsoft/prompty/openai/WireSchemaTest.java new file mode 100644 index 000000000..45be18247 --- /dev/null +++ b/runtime/java/prompty-openai/src/test/java/com/microsoft/prompty/openai/WireSchemaTest.java @@ -0,0 +1,370 @@ +package com.microsoft.prompty.openai; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.microsoft.prompty.Streams; +import com.microsoft.prompty.model.LoadContext; +import com.microsoft.prompty.model.Prompty; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +/** + * Schema conversion tests that go beyond the shared vectors. + * + *

The vectors cover the shapes every runtime must agree on; these cover the awkward corners of + * OpenAI's strict-mode JSON Schema dialect, where getting it wrong produces a request the API + * rejects outright rather than a subtly different response. + */ +class WireSchemaTest { + + /** Build a strict function tool around the given parameters and return its emitted schema. */ + private static Map functionParametersSchema(List parameters) { + Map tool = new LinkedHashMap<>(); + tool.put("name", "set_row_visual"); + tool.put("kind", "function"); + tool.put("strict", true); + tool.put("parameters", parameters); + + Map data = new LinkedHashMap<>(); + data.put("name", "set-row-visual-test"); + data.put("kind", "prompt"); + data.put("instructions", "test"); + data.put("model", Map.of("id", "gpt-4", "provider", "openai")); + data.put("tools", List.of(tool)); + + Prompty agent = Prompty.load(data, new LoadContext()); + Map request = Wire.buildChatArgs(agent, List.of()); + Object schema = Streams.pointer(request, "tools", 0, "function", "parameters"); + return castMap(schema); + } + + /** + * Build a prompt whose declared outputs are the given properties, and return the schema handed to + * {@code response_format}. Structured output is always strict, so this is the second way into the + * same recursive property walk that {@link #functionParametersSchema} reaches through tools. + */ + private static Map outputsSchema(List outputs) { + return castMap(outputsResponseFormat(outputs).get("schema")); + } + + /** + * The {@code json_schema} envelope around the structured-output schema. Recursive widening is + * conditional on strict mode, so the envelope having to declare {@code strict} is part of the same + * contract: if it ever stopped doing so, every nested key would still be listed as required and + * the request would be wrong while the schema assertions carried on passing. + */ + private static Map outputsResponseFormat(List outputs) { + Map data = new LinkedHashMap<>(); + data.put("name", "structured-output-test"); + data.put("kind", "prompt"); + data.put("instructions", "test"); + data.put("model", Map.of("id", "gpt-4", "provider", "openai")); + data.put("outputs", outputs); + + Prompty agent = Prompty.load(data, new LoadContext()); + Map request = Wire.buildChatArgs(agent, List.of()); + return castMap(Streams.pointer(request, "response_format", "json_schema")); + } + + /** + * The nested shape the cross-runtime vector pins down: an object with one genuinely required + * member and one optional one. Strict mode has to name both in {@code required} and express the + * optional one as a nullable union instead of omitting it. + */ + private static List requiredColorOptionalBorder() { + return List.of( + Map.of( + "name", + "row", + "kind", + "object", + "required", + true, + "properties", + List.of( + Map.of("name", "color", "kind", "string", "required", true), + Map.of("name", "border", "kind", "string", "required", false)))); + } + + @SuppressWarnings("unchecked") + private static Map castMap(Object value) { + return (Map) assertInstanceOf(Map.class, value); + } + + /** An empty {@code type} is not valid JSON Schema, so it must never reach the wire. */ + private static void assertNoEmptyType(Object schema) { + if (schema instanceof Map map) { + assertNotEquals("", map.get("type"), "schemas must not emit an empty type: " + schema); + map.values().forEach(WireSchemaTest::assertNoEmptyType); + } else if (schema instanceof List list) { + list.forEach(WireSchemaTest::assertNoEmptyType); + } + } + + private static Object at(Object root, Object... path) { + return Streams.pointer(root, path); + } + + /** + * The exact case the cross-runtime vector pins: a nested object with a required {@code color} and + * an optional {@code border}. Strict mode must name both in {@code required}; {@code border} + * stays semantically optional by gaining a null branch. + */ + @Test + void strictNestedObjectRequiresEveryKeyAndNullsTheOptionalOnes() { + Map schema = functionParametersSchema(requiredColorOptionalBorder()); + + assertEquals( + List.of("color", "border"), + at(schema, "properties", "row", "required"), + "strict mode must name every nested key, not just the genuinely required ones"); + assertEquals("string", at(schema, "properties", "row", "properties", "color", "type")); + assertEquals( + List.of("string", "null"), + at(schema, "properties", "row", "properties", "border", "type"), + "the optional key keeps its optionality as a null branch"); + assertEquals(false, at(schema, "properties", "row", "additionalProperties")); + assertNoEmptyType(schema); + } + + /** + * Structured output reaches the same recursion through {@code response_format} rather than a + * tool, and is always strict, so the nested rule has to hold there too. + */ + @Test + void structuredOutputAppliesTheSameNestedRuleAsTools() { + Map responseFormat = outputsResponseFormat(requiredColorOptionalBorder()); + Map schema = castMap(responseFormat.get("schema")); + + assertEquals(List.of("row"), at(schema, "required")); + assertEquals( + List.of("color", "border"), + at(schema, "properties", "row", "required"), + "structured output reaches the nested walk through response_format, so it must widen too"); + assertEquals("string", at(schema, "properties", "row", "properties", "color", "type")); + assertEquals( + List.of("string", "null"), + at(schema, "properties", "row", "properties", "border", "type"), + "the optional key keeps its optionality as a null branch"); + // The tool path asserts this already; graded here as well because a nested object that omits it + // is rejected by the API, and the two paths could regress independently. + assertEquals(false, at(schema, "properties", "row", "additionalProperties")); + assertEquals(false, at(schema, "additionalProperties")); + assertNoEmptyType(schema); + + assertEquals( + true, + responseFormat.get("strict"), + "widening every nested key is only correct while the envelope declares strict mode"); + } + + /** Without strict, a nested object still lists only what the author actually marked required. */ + @Test + void withoutStrictNestedRequiredStaysExactlyAsDeclared() { + Map tool = new LinkedHashMap<>(); + tool.put("name", "set_row_visual"); + tool.put("kind", "function"); + tool.put("parameters", requiredColorOptionalBorder()); + + Map data = new LinkedHashMap<>(); + data.put("name", "non-strict-test"); + data.put("kind", "prompt"); + data.put("instructions", "test"); + data.put("model", Map.of("id", "gpt-4", "provider", "openai")); + data.put("tools", List.of(tool)); + + Prompty agent = Prompty.load(data, new LoadContext()); + Map schema = + castMap( + Streams.pointer( + Wire.buildChatArgs(agent, List.of()), "tools", 0, "function", "parameters")); + + assertEquals(List.of("color"), at(schema, "properties", "row", "required")); + assertEquals( + "string", + at(schema, "properties", "row", "properties", "border", "type"), + "outside strict mode an optional key is not widened to a nullable union"); + } + + @Test + void nestedUnionsAndOptionalityWiden() { + Map schema = + functionParametersSchema( + List.of( + Map.of( + "name", + "row", + "kind", + "object", + "required", + true, + "properties", + List.of( + Map.of("name", "color", "kind", "string", "nullable", true, "required", true), + Map.of( + "name", + "border", + "kind", + "union", + "nullable", + true, + "required", + false, + "anyOf", + List.of( + Map.of("kind", "string", "enumValues", List.of("thin")), + Map.of("kind", "string", "enumValues", List.of("thick")))), + Map.of( + "name", + "fill", + "kind", + "union", + "required", + true, + "anyOf", + List.of( + Map.of("kind", "string"), + Map.of( + "kind", + "object", + "properties", + List.of( + Map.of("name", "theme", "kind", "string", "required", true), + Map.of( + "name", + "tint", + "kind", + "float", + "required", + false))))))))); + + assertEquals("object", at(schema, "properties", "row", "type")); + // Strict mode lists every key at every depth — `border` is optional but still required-listed, + // with its optionality carried by the null branch asserted below. Dropping it produces a schema + // OpenAI rejects outright. + assertEquals(List.of("color", "border", "fill"), at(schema, "properties", "row", "required")); + assertEquals( + List.of("string", "null"), at(schema, "properties", "row", "properties", "color", "type")); + + assertEquals( + "string", at(schema, "properties", "row", "properties", "border", "anyOf", 0, "type")); + assertEquals( + "string", at(schema, "properties", "row", "properties", "border", "anyOf", 1, "type")); + assertEquals( + Map.of("type", "null"), + at(schema, "properties", "row", "properties", "border", "anyOf", 2)); + + assertEquals("string", at(schema, "properties", "row", "properties", "fill", "anyOf", 0, "type")); + assertEquals("object", at(schema, "properties", "row", "properties", "fill", "anyOf", 1, "type")); + // The same rule applies inside a union branch, which is its own recursion path. + assertEquals( + List.of("theme", "tint"), + at(schema, "properties", "row", "properties", "fill", "anyOf", 1, "required")); + + assertNoEmptyType(schema); + } + + @Test + void strictModeRequiresEveryParameterAndWidensOptionals() { + Map schema = + functionParametersSchema( + List.of( + Map.of( + "name", + "choice", + "kind", + "string", + "required", + false, + "nullable", + true, + "enumValues", + List.of("yes", "no")), + Map.of( + "name", "extension", "kind", "my-extension", "required", false, "nullable", true))); + + // Strict mode forbids optional keys, so optionality is expressed by allowing null instead. + assertEquals(List.of("choice", "extension"), schema.get("required")); + assertEquals(List.of("string", "null"), at(schema, "properties", "choice", "type")); + assertEquals(Arrays.asList("yes", "no", null), at(schema, "properties", "choice", "enum")); + + // An unrecognised kind has no JSON Schema equivalent; emitting `{}` accepts anything, which is + // strictly better than inventing a type the caller did not ask for. + assertEquals(Map.of(), at(schema, "properties", "extension")); + } + + @Test + void oneOfUnionsAreRejected() { + // OpenAI's strict dialect does not support oneOf; failing here beats a 400 from the API. + assertThrows( + SchemaException.class, + () -> + functionParametersSchema( + List.of( + Map.of( + "name", + "invalid", + "kind", + "union", + "oneOf", + List.of(Map.of("kind", "string"), Map.of("kind", "integer")))))); + } + + @Test + void malformedUnionsAreRejectedRatherThanCrashing() { + List> malformed = + List.of( + Map.of("name", "invalid", "kind", "union"), + Map.of( + "name", + "invalid", + "kind", + "union", + "oneOf", + List.of(Map.of("kind", "string")), + "anyOf", + List.of(Map.of("kind", "integer")))); + + for (Map union : malformed) { + assertThrows(SchemaException.class, () -> functionParametersSchema(List.of(union))); + } + } + + @Test + void floatOptionsAreNarrowedWithoutBinaryArtifacts() { + Map data = new LinkedHashMap<>(); + data.put("name", "test"); + data.put("kind", "prompt"); + data.put("instructions", "test"); + data.put( + "model", + Map.of("id", "gpt-4", "provider", "openai", "options", Map.of("temperature", 0.7))); + + Prompty agent = Prompty.load(data, new LoadContext()); + Map request = Wire.buildChatArgs(agent, List.of()); + + // A naive float-to-double widening turns 0.7 into 0.699999988079071 on the wire. + assertEquals("0.7", String.valueOf(request.get("temperature"))); + } + + @Test + void streamingIsEnabledWithUsageOnlyWhereItIsSupported() { + Map chat = new LinkedHashMap<>(); + Wire.enableStreaming(chat, "chat"); + assertEquals(true, chat.get("stream")); + assertEquals(Map.of("include_usage", true), chat.get("stream_options")); + + // The Responses API reports usage in its own events and rejects `stream_options`. + Map responses = new LinkedHashMap<>(); + Wire.enableStreaming(responses, "responses"); + assertEquals(true, responses.get("stream")); + assertTrue(responses.get("stream_options") == null); + } +} diff --git a/runtime/java/prompty-openai/src/test/java/com/microsoft/prompty/openai/WireVectorsTest.java b/runtime/java/prompty-openai/src/test/java/com/microsoft/prompty/openai/WireVectorsTest.java new file mode 100644 index 000000000..cb18d2514 --- /dev/null +++ b/runtime/java/prompty-openai/src/test/java/com/microsoft/prompty/openai/WireVectorsTest.java @@ -0,0 +1,55 @@ +package com.microsoft.prompty.openai; + +import com.microsoft.prompty.SpecVectors; +import com.microsoft.prompty.VectorAgents; +import com.microsoft.prompty.model.Message; +import com.microsoft.prompty.model.Prompty; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; + +/** + * Grades the OpenAI wire conversion against the shared {@code spec/vectors/wire} suite. + * + *

These are the same fixtures every other runtime is measured by, so a vector that passes here + * is evidence of cross-runtime agreement rather than merely of internal consistency. + */ +class WireVectorsTest { + + @TestFactory + Iterable wireVectors() { + List tests = new ArrayList<>(); + for (Map vector : SpecVectors.readArray("wire/wire_vectors.json")) { + String name = SpecVectors.string(vector, "name"); + Map input = SpecVectors.map(vector, "input"); + + // Vectors for other providers are graded by those providers' suites. + if (!"openai".equals(input.getOrDefault("provider", "openai"))) { + continue; + } + + tests.add(DynamicTest.dynamicTest(name, () -> runVector(name, vector, input))); + } + return tests; + } + + private static void runVector(String name, Map vector, Map input) { + Prompty agent = VectorAgents.buildAgent(input, "gpt-4", "openai"); + List messages = VectorAgents.buildMessages(input); + String apiType = String.valueOf(input.getOrDefault("apiType", "chat")); + + Map actual = + switch (apiType) { + case "chat", "agent" -> Wire.buildChatArgs(agent, messages); + case "responses" -> Wire.buildResponsesArgs(agent, messages); + case "embedding" -> Wire.buildEmbeddingArgs(agent, messages); + case "image" -> Wire.buildImageArgs(agent, messages); + default -> throw new AssertionError("Unknown apiType: " + apiType); + }; + + Object expected = SpecVectors.map(vector, "expected").get("request_body"); + SpecVectors.assertEquivalent(name, expected, actual); + } +} diff --git a/runtime/java/prompty/build.gradle.kts b/runtime/java/prompty/build.gradle.kts new file mode 100644 index 000000000..d64e1050c --- /dev/null +++ b/runtime/java/prompty/build.gradle.kts @@ -0,0 +1,25 @@ +plugins { + id("java-library") + // Publishes the shared spec-vector harness so every provider module grades itself against the + // same fixtures, rather than each one re-implementing vector loading and comparison. + id("java-test-fixtures") +} + +dependencies { + // YAML frontmatter parsing. The generated model carries a dependency-free YAML + // subset reader (TypraYaml); the loader uses SnakeYAML so real-world `.prompty` + // frontmatter (block scalars, comments, anchors) parses correctly. + api("org.yaml:snakeyaml:2.4") + + // Template engines: `jinjava` backs the `jinja2` format kind and `mustache.java` + // backs the `mustache` format kind. + implementation("com.hubspot.jinjava:jinjava:2.7.4") + implementation("com.github.spullara.mustache.java:compiler:0.9.14") + + testImplementation(platform("org.junit:junit-bom:5.11.4")) + testImplementation("org.junit.jupiter:junit-jupiter") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") + + testFixturesApi(platform("org.junit:junit-bom:5.11.4")) + testFixturesApi("org.junit.jupiter:junit-jupiter") +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/AgentEvent.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/AgentEvent.java new file mode 100644 index 000000000..dbe91be5b --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/AgentEvent.java @@ -0,0 +1,150 @@ +package com.microsoft.prompty; + +import com.microsoft.prompty.model.Message; +import java.util.List; + +/** + * Something observable that happened while a turn ran. + * + *

Events are a live, best-effort narration for a caller that wants to show progress — streamed + * tokens, tool activity, retries. They are deliberately not the durable record: the engine's + * journal is. A dropped or slow event listener must never change what a turn does, so nothing here + * carries a result the turn depends on. + * + *

Every event is projected from a durable engine event, which is what keeps the narration + * consistent with what was actually committed. + */ +public sealed interface AgentEvent { + + /** A short, stable name for this event, suitable for logs and filters. */ + String type(); + + /** The turn began. */ + record TurnStart(String agent, int maxIterations) implements AgentEvent { + @Override + public String type() { + return "turn_start"; + } + } + + /** The turn reached a terminal state. Always the last event. */ + record TurnEnd(String status, int iterations, Object response) implements AgentEvent { + @Override + public String type() { + return "turn_end"; + } + } + + /** A model call is about to be made. */ + record LlmStart(String provider, String modelId, int messageCount, int iteration) + implements AgentEvent { + @Override + public String type() { + return "llm_start"; + } + } + + /** A model call returned. */ + record LlmComplete(int iteration) implements AgentEvent { + @Override + public String type() { + return "llm_complete"; + } + } + + /** A transient failure will be retried. */ + record Retry(String operation, int attempt, int maxAttempts, String reason) + implements AgentEvent { + @Override + public String type() { + return "retry"; + } + } + + /** A streamed text token. */ + record Token(String text) implements AgentEvent { + @Override + public String type() { + return "token"; + } + } + + /** A streamed reasoning token. */ + record Thinking(String text) implements AgentEvent { + @Override + public String type() { + return "thinking"; + } + } + + /** A tool is about to run. */ + record ToolCallStart(String name, String arguments) implements AgentEvent { + @Override + public String type() { + return "tool_call_start"; + } + } + + /** A tool produced a result. */ + record ToolResult(String name, String result) implements AgentEvent { + @Override + public String type() { + return "tool_result"; + } + } + + /** A tool finished, with normalized success metadata. */ + record ToolCallComplete(String name, boolean success, String result, String errorKind) + implements AgentEvent { + @Override + public String type() { + return "tool_call_complete"; + } + } + + /** A human-readable progress note. */ + record Status(String message) implements AgentEvent { + @Override + public String type() { + return "status"; + } + } + + /** The conversation changed — tool results appended, context trimmed, steering injected. */ + record MessagesUpdated(List messages) implements AgentEvent { + @Override + public String type() { + return "messages_updated"; + } + } + + /** The turn produced its final response. Emitted before {@link TurnEnd}. */ + record Done(Object response, List messages) implements AgentEvent { + @Override + public String type() { + return "done"; + } + } + + /** Something went wrong. Not necessarily terminal — a failed tool is reported here too. */ + record Error(String message) implements AgentEvent { + @Override + public String type() { + return "error"; + } + } + + /** The turn was cancelled. */ + record Cancelled() implements AgentEvent { + @Override + public String type() { + return "cancelled"; + } + } + + /** Receives events as a turn runs. */ + @FunctionalInterface + interface Listener { + void onEvent(AgentEvent event); + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/CancellationToken.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/CancellationToken.java new file mode 100644 index 000000000..c8514fdc1 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/CancellationToken.java @@ -0,0 +1,91 @@ +package com.microsoft.prompty; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * A cooperative cancellation signal shared between a caller and a long-running provider call. + * + *

Cancellation is one-way and idempotent: once cancelled a token stays cancelled, and cancelling + * again is a no-op. Registered callbacks fire exactly once, on the thread that calls {@link + * #cancel()}, or immediately on the registering thread if the token is already cancelled. + * + *

A callback that throws does not prevent the remaining callbacks from running — cancellation + * must not be derailed by a misbehaving listener — but the first such failure is rethrown once every + * callback has been given its turn. + */ +public final class CancellationToken { + + private static final CancellationToken NONE = new CancellationToken(); + + private final AtomicBoolean cancelled = new AtomicBoolean(false); + private final List callbacks = new CopyOnWriteArrayList<>(); + + /** A token that is never cancelled. Safe to share; registering on it does nothing. */ + public static CancellationToken none() { + return NONE; + } + + /** Create a fresh, uncancelled token. */ + public static CancellationToken create() { + return new CancellationToken(); + } + + public boolean isCancelled() { + return cancelled.get(); + } + + /** Request cancellation and run every registered callback. */ + public void cancel() { + if (this == NONE) { + throw new IllegalStateException("the shared 'none' token cannot be cancelled"); + } + if (!cancelled.compareAndSet(false, true)) { + return; + } + List pending = new ArrayList<>(callbacks); + callbacks.clear(); + RuntimeException firstFailure = null; + for (Runnable callback : pending) { + try { + callback.run(); + } catch (RuntimeException e) { + if (firstFailure == null) { + firstFailure = e; + } + } + } + if (firstFailure != null) { + throw firstFailure; + } + } + + /** + * Run {@code callback} when this token is cancelled, or immediately if it already has been. + * + *

Registering on {@link #none()} is a no-op, since that token can never be cancelled. + */ + public void onCancel(Runnable callback) { + if (this == NONE) { + return; + } + if (cancelled.get()) { + callback.run(); + return; + } + callbacks.add(callback); + // Re-check: cancel() may have drained the list between the guard above and the add. + if (cancelled.get() && callbacks.remove(callback)) { + callback.run(); + } + } + + /** Throw if cancellation has been requested. */ + public void throwIfCancelled(String message) { + if (isCancelled()) { + throw InvokerException.cancelled(message); + } + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/Compaction.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Compaction.java new file mode 100644 index 000000000..84ce4fe85 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Compaction.java @@ -0,0 +1,47 @@ +package com.microsoft.prompty; + +import com.microsoft.prompty.model.Message; +import java.nio.file.Path; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Summarizes messages that context trimming is about to drop. + * + *

Trimming alone loses information. Compaction gives that loss somewhere to go: the dropped + * messages are summarized and the summary replaces them, so the model keeps the gist of a long + * conversation without carrying its full text. + * + *

Compaction is best-effort by design. If the summarizer fails or returns nothing usable, the + * turn keeps the default mechanical summary and continues — a failed summarization must never fail + * the turn it was trying to help. + */ +@FunctionalInterface +public interface Compaction { + + /** + * Summarize dropped messages. + * + * @return the summary text, or null/blank to keep the default summary + */ + String summarize(List dropped); + + /** + * Summarize by invoking a {@code .prompty} file with the dropped messages as its {@code messages} + * input. + */ + static Compaction fromPrompty(Path path) { + return dropped -> { + Map inputs = new LinkedHashMap<>(); + inputs.put("messages", Context.formatDroppedMessages(dropped)); + Object result = Pipeline.invoke(path, inputs); + return result instanceof String text ? text : null; + }; + } + + /** Summarize by invoking a {@code .prompty} file at {@code path}. */ + static Compaction fromPrompty(String path) { + return fromPrompty(Path.of(path)); + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/Connections.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Connections.java new file mode 100644 index 000000000..746758619 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Connections.java @@ -0,0 +1,99 @@ +package com.microsoft.prompty; + +import com.microsoft.prompty.model.Connection; +import com.microsoft.prompty.model.ReferenceConnection; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Named connections a prompt can refer to instead of embedding. + * + *

A {@code kind: reference} connection names a connection the host supplies at runtime. That + * indirection is what lets a {@code .prompty} file be committed, shared, and reviewed without + * carrying an endpoint or a credential — the file says which connection it needs, the host decides + * what that resolves to. + */ +public final class Connections { + + private static final Map REGISTERED = new ConcurrentHashMap<>(); + + private Connections() {} + + /** Register a connection under a name prompts can reference. */ + public static void register(String name, Connection connection) { + if (name == null || name.isEmpty()) { + throw new IllegalArgumentException("Connection name must not be empty"); + } + if (connection == null) { + throw new IllegalArgumentException("Connection must not be null"); + } + REGISTERED.put(name, connection); + } + + /** Remove a registered connection, reporting whether one was present. */ + public static boolean unregister(String name) { + return REGISTERED.remove(name) != null; + } + + /** Forget every registered connection. Intended for tests. */ + public static void clear() { + REGISTERED.clear(); + } + + /** Look up a registered connection, or null when the name is unknown. */ + public static Connection get(String name) { + return name == null ? null : REGISTERED.get(name); + } + + /** + * Follow a connection to the one that actually carries endpoint and credentials. + * + *

Anything that is not a reference is already concrete and is returned unchanged. + * + * @throws InvokerException with {@link InvokerException.Kind#EXECUTE} when a reference names a + * connection nothing has registered — failing here says which name was missing, whereas + * proceeding would surface later as an unexplained authentication error + */ + public static Connection resolve(Connection connection) { + Connection current = connection; + // A reference may legitimately point at another; a cycle among them is a configuration mistake + // that would otherwise hang, so the chain is walked with the names already seen. + java.util.Set seen = new java.util.LinkedHashSet<>(); + while (current instanceof ReferenceConnection reference) { + if (reference.name == null || reference.name.isEmpty()) { + throw InvokerException.execute("Reference connection is missing its 'name'"); + } + if (!seen.add(reference.name)) { + throw InvokerException.execute( + "Connection reference cycle: " + String.join(" -> ", seen) + " -> " + reference.name); + } + Connection resolved = REGISTERED.get(reference.name); + if (resolved == null) { + throw InvokerException.execute( + "No connection registered under '" + + reference.name + + "'; register it with Connections.register before running this prompt"); + } + current = resolved; + } + return current; + } + + /** + * Strip trailing slashes from an endpoint so a path can be appended to it. + * + *

Endpoints are typed by hand and pasted from consoles, so a trailing slash is common and + * occasionally there is more than one. Removing only the last would leave {@code https://host//v1}, + * which some gateways route differently from {@code https://host/v1} and others reject outright. + */ + public static String trimTrailingSlashes(String endpoint) { + if (endpoint == null) { + return ""; + } + int end = endpoint.length(); + while (end > 0 && endpoint.charAt(end - 1) == '/') { + end--; + } + return endpoint.substring(0, end); + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/Context.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Context.java new file mode 100644 index 000000000..3803a4d5d --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Context.java @@ -0,0 +1,210 @@ +package com.microsoft.prompty; + +import com.microsoft.prompty.model.Message; +import com.microsoft.prompty.model.Role; +import com.microsoft.prompty.model.TextPart; +import com.microsoft.prompty.model.TypraJson; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Trims a conversation to fit a character budget. + * + *

The budget is measured in characters rather than tokens on purpose: tokenization is + * provider- and model-specific, and a turn has to decide what to drop before it knows which + * tokenizer applies. A character estimate is stable across providers and errs toward keeping + * the conversation smaller than the real limit. + * + *

Character counts use UTF-16 code units, where the Rust reference counts UTF-8 bytes. The two + * agree for ASCII and diverge for non-ASCII text, which shifts only where the trim boundary falls, + * never which messages are structurally preserved. + */ +public final class Context { + + /** Per-message formatting overhead, on top of the role name. */ + private static final int ROLE_OVERHEAD = 4; + + /** What a non-text part (image, file, audio) is assumed to cost. */ + private static final int RICH_PART_CHARS = 200; + + /** Longest per-message excerpt kept in a summary. */ + private static final int SUMMARY_EXCERPT_CHARS = 200; + + /** Hard cap on a generated summary. */ + private static final int SUMMARY_MAX_CHARS = 4000; + + /** Ceiling on the share of the budget reserved for the summary message. */ + private static final int SUMMARY_BUDGET_CAP = 5000; + + /** Fraction of the budget reserved for the summary, as a divisor (20 → 5%). */ + private static final int SUMMARY_BUDGET_DIVISOR = 20; + + /** Non-system messages that are never dropped, however tight the budget. */ + private static final int MIN_KEPT_MESSAGES = 2; + + private Context() {} + + /** The result of a trim: what was dropped, and what remains. */ + public record Trimmed(List dropped, List messages) {} + + /** Estimate the character cost of a conversation. */ + public static int estimateChars(List messages) { + if (messages == null) { + return 0; + } + int total = 0; + for (Message message : messages) { + total += roleName(message).length() + ROLE_OVERHEAD; + if (message.parts != null) { + for (Object part : message.parts) { + if (part instanceof TextPart text) { + total += text.value == null ? 0 : text.value.length(); + } else { + total += RICH_PART_CHARS; + } + } + } + if (message.metadata != null) { + Object toolCalls = message.metadata.get("tool_calls"); + if (toolCalls != null) { + total += TypraJson.stringify(toolCalls).length(); + } + } + } + return total; + } + + /** Summarize messages that are about to be dropped. */ + public static String summarizeDropped(List messages) { + if (messages == null || messages.isEmpty()) { + return ""; + } + List parts = new ArrayList<>(messages.size()); + for (Message message : messages) { + String role = roleName(message); + String text = Messages.text(message); + if (text.isEmpty()) { + parts.add("[" + role + " message]"); + } else { + String truncated = + text.length() > SUMMARY_EXCERPT_CHARS + ? text.substring(0, SUMMARY_EXCERPT_CHARS) + "..." + : text; + parts.add("[" + role + "]: " + truncated); + } + } + String summary = String.join("\n", parts); + return summary.length() > SUMMARY_MAX_CHARS + ? summary.substring(0, SUMMARY_MAX_CHARS) + "..." + : summary; + } + + /** + * Trim a conversation to fit {@code budgetChars}. + * + *

Leading system messages are always preserved — they carry the agent's identity and + * instructions, so dropping them changes what the agent is rather than merely what it remembers. + * Beyond those, the oldest messages are dropped first and replaced by a single synthetic summary, + * and at least {@value #MIN_KEPT_MESSAGES} non-system messages always survive so the model still + * sees a real exchange. + * + *

Returns the input unchanged when it already fits, when there is too little to trim, or when + * no single drop would help. + */ + public static Trimmed trimToContextWindow(List messages, int budgetChars) { + List all = messages == null ? List.of() : messages; + if (estimateChars(all) <= budgetChars) { + return new Trimmed(List.of(), new ArrayList<>(all)); + } + + int systemCount = 0; + while (systemCount < all.size() && all.get(systemCount).role == Role.SYSTEM) { + systemCount++; + } + List systemMessages = all.subList(0, systemCount); + List rest = all.subList(systemCount, all.size()); + + if (rest.size() <= MIN_KEPT_MESSAGES) { + return new Trimmed(List.of(), new ArrayList<>(all)); + } + + int systemChars = estimateChars(systemMessages); + int summaryBudget = Math.min(SUMMARY_BUDGET_CAP, budgetChars / SUMMARY_BUDGET_DIVISOR); + int available = Math.max(0, budgetChars - (systemChars + summaryBudget)); + + int dropCount = 0; + int restChars = estimateChars(rest); + int droppable = Math.max(0, rest.size() - MIN_KEPT_MESSAGES); + while (restChars > available && dropCount < droppable) { + restChars -= estimateChars(List.of(rest.get(dropCount))); + dropCount++; + } + + if (dropCount == 0) { + return new Trimmed(List.of(), new ArrayList<>(all)); + } + + List dropped = new ArrayList<>(rest.subList(0, dropCount)); + List kept = rest.subList(dropCount, rest.size()); + + List result = new ArrayList<>(systemMessages.size() + 1 + kept.size()); + result.addAll(systemMessages); + result.add( + Messages.user( + "[Context summary: " + + summarizeDropped(dropped) + + "\n... (" + + dropCount + + " messages omitted)]")); + result.addAll(kept); + + return new Trimmed(dropped, result); + } + + /** + * Render dropped messages as prompt text for a compaction summarizer. + * + *

Unlike {@link #summarizeDropped}, nothing is truncated: this feeds a model that is being + * asked to produce the summary, so withholding content would degrade the very thing being built. + */ + public static String formatDroppedMessages(List messages) { + List lines = new ArrayList<>(); + if (messages == null) { + return ""; + } + for (Message message : messages) { + String role = roleName(message); + String text = Messages.text(message); + + if (message.metadata != null + && message.metadata.get("tool_calls") instanceof List calls) { + for (Object call : calls) { + if (call instanceof Map map) { + Object name = map.get("name"); + Object arguments = map.get("arguments"); + lines.add( + "[" + + role + + "]: Called: " + + (name instanceof String value ? value : "unknown") + + "(" + + (arguments instanceof String value ? value : "{}") + + ")"); + } + } + } + + if (!text.isEmpty()) { + lines.add("[" + role + "]: " + text); + } else if (lines.isEmpty() || !lines.get(lines.size() - 1).startsWith("[" + role + "]")) { + lines.add("[" + role + " message]"); + } + } + return String.join("\n", lines); + } + + private static String roleName(Message message) { + return message.role == null ? "" : message.role.value; + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/Discovery.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Discovery.java new file mode 100644 index 000000000..6874c6bd1 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Discovery.java @@ -0,0 +1,195 @@ +package com.microsoft.prompty; + +import com.microsoft.prompty.model.ModelInfo; +import com.microsoft.prompty.model.TypraJson; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Capability enrichment for provider model discovery. + * + *

Provider {@code /models} endpoints differ in how much they say. Anthropic and Foundry return + * context windows and modalities; OpenAI returns little more than an id. Left alone, the same model + * would describe itself differently depending on where it was listed from, so Prompty ships one + * shared dataset and one rule for applying it: + * + *

+ * + * The provider always wins. Dataset entries fill only the fields the provider left empty, and a + * model id is matched by its longest prefix. + * + *
+ * + *

The dataset is deliberately not emitted from TypeSpec. TypeSpec owns the shape of {@link + * ModelInfo}; this is volatile vendor data — context windows and model families that change with + * every release — kept as a refreshable snapshot. + * + *

Vendored copy. The cross-runtime source of truth is {@code + * spec/data/model_capabilities.json}. A published jar cannot reach outside its own tree, so this + * module carries a copy on the classpath and {@code DiscoveryTest} fails if the two drift apart. To + * refresh, edit the file under {@code spec/} and copy it to {@code + * runtime/java/prompty/src/main/resources/com/microsoft/prompty/}. Every runtime vendors it the same + * way, which is what lets the shared enrichment vectors converge. + */ +public final class Discovery { + + /** Where the vendored dataset sits on the classpath. */ + static final String RESOURCE = "/com/microsoft/prompty/model_capabilities.json"; + + private Discovery() {} + + /** + * The capability fields a dataset entry can supply. + * + *

Every field is nullable, and null means the dataset says nothing — leave whatever the + * provider returned. That is distinct from an empty modality list, which is a real answer: an + * embedding model genuinely produces no output modality. + */ + public record Capabilities( + Integer contextWindow, List inputModalities, List outputModalities) {} + + private record Entry(String prefix, Capabilities capabilities) {} + + /** Parsed once on first use; the dataset is immutable for the life of the process. */ + private static final class Table { + static final Map> BY_PROVIDER = parse(); + + private static Map> parse() { + Map> providers = new HashMap<>(); + Object parsed = TypraJson.parse(read()); + if (!(parsed instanceof Map root)) { + return providers; + } + if (!(root.get("providers") instanceof Map map)) { + return providers; + } + for (Map.Entry provider : map.entrySet()) { + if (!(provider.getValue() instanceof Iterable list)) { + continue; + } + List entries = new ArrayList<>(); + for (Object item : list) { + Entry entry = toEntry(item); + if (entry != null) { + entries.add(entry); + } + } + // Longest prefix first, so the first match is the most specific one and the order the file + // happens to be authored in cannot change the answer. + entries.sort(Comparator.comparingInt((Entry e) -> e.prefix().length()).reversed()); + providers.put(String.valueOf(provider.getKey()), List.copyOf(entries)); + } + return Map.copyOf(providers); + } + + private static Entry toEntry(Object item) { + if (!(item instanceof Map map) || !(map.get("prefix") instanceof String prefix)) { + return null; + } + return new Entry( + prefix, + new Capabilities( + map.get("contextWindow") instanceof Number n ? n.intValue() : null, + modalities(map.get("inputModalities")), + modalities(map.get("outputModalities")))); + } + + private static List modalities(Object value) { + if (!(value instanceof Iterable items)) { + return null; + } + List result = new ArrayList<>(); + for (Object item : items) { + if (item instanceof String text) { + result.add(text); + } + } + return List.copyOf(result); + } + + private static String read() { + try (InputStream stream = Discovery.class.getResourceAsStream(RESOURCE)) { + if (stream == null) { + throw new IllegalStateException("missing vendored capability dataset at " + RESOURCE); + } + return new String(stream.readAllBytes(), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new IllegalStateException("unable to read " + RESOURCE, e); + } + } + } + + /** + * Find the dataset entry for a model id, or null when the provider has none. + * + *

Matching is by longest prefix, and only at a token boundary — see {@link #prefixMatches}. + */ + public static Capabilities lookup(String provider, String id) { + List entries = Table.BY_PROVIDER.get(provider == null ? "" : provider); + if (entries == null || id == null) { + return null; + } + for (Entry entry : entries) { + if (prefixMatches(id, entry.prefix())) { + return entry.capabilities(); + } + } + return null; + } + + /** + * Whether a dataset prefix claims a model id. + * + *

A prefix matches only at a token boundary: the id must either be the prefix exactly, or the + * next character must be a separator. Without that rule {@code gpt-4} would swallow a future + * {@code gpt-45} and hand it the wrong context window, while the ids that should match — {@code + * gpt-4-0613}, {@code gpt-4o-2024-05-13} — still do. Every runtime implements this same rule, so + * the shared enrichment vectors agree. + */ + static boolean prefixMatches(String id, String prefix) { + if (!id.startsWith(prefix)) { + return false; + } + if (id.length() == prefix.length()) { + return true; + } + char next = id.charAt(prefix.length()); + boolean alphanumeric = + (next >= '0' && next <= '9') + || (next >= 'a' && next <= 'z') + || (next >= 'A' && next <= 'Z'); + return !alphanumeric; + } + + /** + * Fill a model's empty capability fields from the shared dataset. + * + *

Only fields the provider left null are written; anything it supplied stands, including an + * empty list it chose to send. A dataset entry that is itself an empty list is a legitimate fill — + * that is how an embedding model's absent output modality is expressed. + */ + public static void enrich(String provider, ModelInfo info) { + if (info == null) { + return; + } + Capabilities caps = lookup(provider, info.id); + if (caps == null) { + return; + } + if (info.contextWindow == null && caps.contextWindow() != null) { + info.contextWindow = caps.contextWindow(); + } + if (info.inputModalities == null && caps.inputModalities() != null) { + info.inputModalities = new ArrayList<>(caps.inputModalities()); + } + if (info.outputModalities == null && caps.outputModalities() != null) { + info.outputModalities = new ArrayList<>(caps.outputModalities()); + } + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/Environment.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Environment.java new file mode 100644 index 000000000..283c424a8 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Environment.java @@ -0,0 +1,85 @@ +package com.microsoft.prompty; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +/** + * The source of values for {@code ${env:...}} references. + * + *

Resolution consults, in order: values set through {@link #set}, JVM system properties, then the + * process environment. The first two exist because a JVM cannot modify its own environment — without + * them, configuration could only ever come from outside the process, which makes tests awkward and + * makes it impossible for a host application to supply secrets it has already loaded (from a key + * vault, a {@code .env} file, or its own configuration system). + * + *

Explicit overrides win so that a caller who has deliberately supplied a value is never + * second-guessed by the ambient environment. + * + *

{@link #mask} is the other half of that control: a JVM cannot remove a variable from its own + * environment, so without it a caller could add a value but never state that one is deliberately + * absent. That asymmetry leaves behaviour at the mercy of whatever the surrounding machine happens + * to export -- a test for "no credential is configured" would pass on a clean machine and fail on a + * developer's, and a host that wants to run a prompt without inheriting an ambient key could not + * say so. + */ +public final class Environment { + + /** + * Decisions that outrank the surrounding process, keyed by name. + * + *

A present optional is a value supplied through {@link #set}; an empty one is a mask. Holding + * both states in one entry is what makes each of {@link #set}, {@link #mask} and {@link #clear} a + * single map mutation, so a concurrent {@link #lookup} always sees one decision or the other and + * never a gap in which the ambient value shows through. + */ + private static final Map> DECISIONS = new ConcurrentHashMap<>(); + + private Environment() {} + + /** + * Supply a value for {@code name}, taking precedence over system properties and the process + * environment. A null value drops any decision recorded here, exactly as {@link #clear} does. + */ + public static void set(String name, String value) { + if (value == null) { + DECISIONS.remove(name); + } else { + DECISIONS.put(name, Optional.of(value)); + } + } + + /** + * Report {@code name} as unset, whatever the system properties and process environment say. + * + *

This is not the same as {@link #clear}: clearing drops a decision made here and lets + * resolution fall back to the surrounding process, whereas masking stops that fallback. Undo it + * with {@link #clear} or {@link #set}; whichever of {@code set} and {@code mask} runs last wins. + */ + public static void mask(String name) { + DECISIONS.put(name, Optional.empty()); + } + + /** Remove a value previously supplied through {@link #set}, or a mask applied by {@link #mask}. */ + public static void clear(String name) { + DECISIONS.remove(name); + } + + /** Remove every value supplied through {@link #set} and every mask applied by {@link #mask}. */ + public static void clearAll() { + DECISIONS.clear(); + } + + /** Look up {@code name}, or an empty optional if it is set nowhere or has been masked. */ + public static Optional lookup(String name) { + Optional decision = DECISIONS.get(name); + if (decision != null) { + return decision; + } + String property = System.getProperty(name); + if (property != null) { + return Optional.of(property); + } + return Optional.ofNullable(System.getenv(name)); + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/Executor.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Executor.java new file mode 100644 index 000000000..d39d22fbe --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Executor.java @@ -0,0 +1,147 @@ +package com.microsoft.prompty; + +import com.microsoft.prompty.model.Message; +import com.microsoft.prompty.model.ModelInvocationRequest; +import com.microsoft.prompty.model.Prompty; +import com.microsoft.prompty.model.Role; +import com.microsoft.prompty.model.ToolCall; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Sends messages to a model provider and returns its raw response. + * + *

Registered under the {@code prompty.executors} group, keyed by {@code agent.model.provider}. + * + *

Only {@link #execute} is required. Streaming and context-aware invocation have defaults so a + * minimal executor stays small, and so adding a capability to this interface does not break existing + * implementations. + */ +public interface Executor { + + /** + * Invoke the provider and return its raw, unprocessed response. + * + *

The return value is a plain JSON-shaped tree — {@code Map}, {@code List}, {@code String}, + * {@code Number}, {@code Boolean}, or null — matching the representation the generated model layer + * loads from. Interpreting it is the {@link Processor}'s job. + * + * @throws InvokerException with {@link InvokerException.Kind#EXECUTE} if the call fails + */ + Object execute(Prompty agent, List messages); + + /** + * Invoke the provider from a generated invocation request. + * + *

The default forwards the request's message snapshot to {@link #execute}, so an executor that + * knows nothing about delegated provider state still works. Providers that can resume server-side + * conversation state should override this and consume the request directly. + */ + default Object executeWithContext( + Prompty agent, ModelInvocationRequest request, CancellationToken cancellation) { + cancellation.throwIfCancelled("execution cancelled before provider invocation"); + Object result = execute(agent, messagesOf(request)); + cancellation.throwIfCancelled("execution cancelled during provider invocation"); + return result; + } + + /** + * Invoke the provider and return an iterator over raw response chunks. + * + *

Each element is one raw chunk as the provider sent it, before any processing. + * + * @throws InvokerException with {@link InvokerException.Kind#EXECUTE} if streaming is unsupported + */ + default Iterator executeStream(Prompty agent, List messages) { + throw InvokerException.execute("Streaming not supported by this executor"); + } + + /** + * Stream from the provider, abandoning the response as soon as cancellation is requested. + * + *

The default opens the stream through {@link #executeStream} and wraps it so every subsequent + * advance observes the token. A provider that can abort the underlying HTTP request should + * override this to release the connection rather than merely stop reading. + */ + default Iterator executeStreamCancellable( + Prompty agent, List messages, CancellationToken cancellation) { + cancellation.throwIfCancelled("streaming execution cancelled before provider invocation"); + Iterator stream = executeStream(agent, messages); + return Streams.cancellable(stream, cancellation); + } + + /** Stream from the provider using a generated invocation request. */ + default Iterator executeStreamWithContext( + Prompty agent, ModelInvocationRequest request, CancellationToken cancellation) { + return executeStreamCancellable(agent, messagesOf(request), cancellation); + } + + /** + * Build the messages that carry a round of tool results back to the provider. + * + *

The default is the OpenAI-style pattern: one assistant message echoing the tool calls, + * followed by one tool message per result. Providers that expect a different shape — Anthropic + * nests tool results inside a user message, for instance — override this. + * + * @param rawResponse the raw response the tool calls came from, for providers that must echo it + * @param toolCalls the calls the model requested + * @param toolResults the results, positionally aligned with {@code toolCalls} + * @param textContent any assistant text that accompanied the tool calls + */ + default List formatToolMessages( + Object rawResponse, List toolCalls, List toolResults, String textContent) { + List messages = new ArrayList<>(); + + List wireCalls = new ArrayList<>(toolCalls.size()); + for (ToolCall call : toolCalls) { + Map function = new LinkedHashMap<>(); + function.put("name", call.name); + function.put("arguments", call.arguments); + + Map wire = new LinkedHashMap<>(); + wire.put("id", call.id); + wire.put("type", "function"); + wire.put("function", function); + wireCalls.add(wire); + } + + Message assistant = new Message(); + assistant.role = Role.ASSISTANT; + assistant.parts = new ArrayList<>(); + assistant.metadata = new LinkedHashMap<>(); + assistant.metadata.put("tool_calls", wireCalls); + messages.add(assistant); + + int count = Math.min(toolCalls.size(), toolResults.size()); + for (int i = 0; i < count; i++) { + messages.add(Messages.toolResult(toolCalls.get(i).id, toolResults.get(i))); + } + + return messages; + } + + /** + * Build tool-result messages for a streamed response. + * + *

Defaults to {@link #formatToolMessages} with no raw response, which is correct for + * OpenAI-compatible providers. Providers that must replay raw streamed assistant content override + * this and consume {@code rawChunks}. + */ + default List formatStreamToolMessages( + List rawChunks, + List toolCalls, + List toolResults, + String textContent) { + return formatToolMessages(null, toolCalls, toolResults, textContent); + } + + private static List messagesOf(ModelInvocationRequest request) { + if (request == null || request.context == null || request.context.messages == null) { + return List.of(); + } + return request.context.messages; + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/Frontmatter.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Frontmatter.java new file mode 100644 index 000000000..8ca90b058 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Frontmatter.java @@ -0,0 +1,170 @@ +package com.microsoft.prompty; + +import java.util.LinkedHashMap; +import java.util.Map; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.SafeConstructor; +import org.yaml.snakeyaml.error.YAMLException; + +/** + * Splits a {@code .prompty} document into its YAML frontmatter and its markdown body. + * + *

Frontmatter is delimited by a line of {@code ---} or {@code +++}. Behaviour is matched to the + * Rust runtime's {@code loader::frontmatter}: + * + *

    + *
  • Leading whitespace before the opening delimiter is allowed. + *
  • A document with no opening delimiter is entirely body, with empty frontmatter. + *
  • The closing delimiter must be a line that trims to {@code ---} or {@code +++}. Notably it + * need not match the opener — the Rust implementation ignores the opener when searching, and + * that leniency is reproduced here rather than tightened. + *
  • An opening delimiter with no closing match is an error. + *
  • Frontmatter that parses to anything other than a mapping is an error. + *
+ * + *

The body is returned untrimmed; trimming is the loader's responsibility. + */ +public final class Frontmatter { + + private Frontmatter() {} + + /** The result of splitting a {@code .prompty} document. */ + public record Split(Map frontmatter, String body) {} + + /** + * Split raw {@code .prompty} content into frontmatter and body. + * + * @throws LoadException if the delimiters are unbalanced or the frontmatter is not a YAML mapping + */ + public static Split split(String raw) { + String trimmed = stripLeading(raw); + + if (!trimmed.startsWith("---") && !trimmed.startsWith("+++")) { + // No delimiter at the start, so the whole document is body. + return new Split(new LinkedHashMap<>(), raw); + } + + int firstNewline = trimmed.indexOf('\n', 3); + if (firstNewline < 0) { + // An opening delimiter with no newline after it: empty frontmatter, empty body. + return new Split(new LinkedHashMap<>(), ""); + } + int afterOpener = firstNewline + 1; + + String rest = trimmed.substring(afterOpener); + int closePos = findClosingDelimiter(rest); + if (closePos < 0) { + throw LoadException.invalidFrontmatter("Opening delimiter without closing match"); + } + + String yaml = rest.substring(0, closePos); + String afterClose = rest.substring(closePos); + int closeNewline = afterClose.indexOf('\n'); + String body = closeNewline < 0 ? "" : afterClose.substring(closeNewline + 1); + + return new Split(parseYamlMapping(yaml), body); + } + + /** + * Index of the first line that trims to a closing delimiter, or {@code -1}. + * + *

Returns the offset of the start of that line. + */ + private static int findClosingDelimiter(String text) { + int pos = 0; + int length = text.length(); + while (pos <= length) { + int newline = text.indexOf('\n', pos); + int lineEnd = newline < 0 ? length : newline; + String line = text.substring(pos, lineEnd).trim(); + if (line.equals("---") || line.equals("+++")) { + return pos; + } + if (newline < 0) { + return -1; + } + pos = newline + 1; + } + return -1; + } + + /** Parse a YAML mapping, returning an empty map for blank input. */ + static Map parseYamlMapping(String yaml) { + String trimmed = yaml.trim(); + if (trimmed.isEmpty()) { + return new LinkedHashMap<>(); + } + + Object parsed; + try { + parsed = newYaml().load(trimmed); + } catch (YAMLException e) { + throw LoadException.invalidFrontmatter(e.getMessage()); + } + + if (parsed == null) { + return new LinkedHashMap<>(); + } + if (!(parsed instanceof Map map)) { + throw LoadException.invalidFrontmatter("Frontmatter must be a YAML mapping"); + } + return stringKeyed(map); + } + + /** + * A SnakeYAML instance restricted to the safe constructor, so a {@code .prompty} document can + * never name an arbitrary Java class to instantiate. + */ + static Yaml newYaml() { + LoaderOptions options = new LoaderOptions(); + options.setAllowDuplicateKeys(false); + return new Yaml(new SafeConstructor(options)); + } + + /** + * Re-key a parsed YAML map to {@code String} keys. + * + *

YAML permits non-string keys; the generated model layer is string-keyed throughout, so keys + * are stringified rather than rejected. + */ + @SuppressWarnings("unchecked") + static Map stringKeyed(Map map) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : map.entrySet()) { + String key = entry.getKey() == null ? "null" : String.valueOf(entry.getKey()); + Object value = entry.getValue(); + if (value instanceof Map nested) { + result.put(key, stringKeyed(nested)); + } else if (value instanceof java.util.List list) { + result.put(key, stringKeyedList(list)); + } else { + result.put(key, value); + } + } + return result; + } + + static java.util.List stringKeyedList(java.util.List list) { + java.util.List result = new java.util.ArrayList<>(list.size()); + for (Object item : list) { + if (item instanceof Map nested) { + result.add(stringKeyed(nested)); + } else if (item instanceof java.util.List nestedList) { + result.add(stringKeyedList(nestedList)); + } else { + result.add(item); + } + } + return result; + } + + /** Strip leading whitespace, matching Rust's {@code str::trim_start}. */ + private static String stripLeading(String value) { + int i = 0; + while (i < value.length() && Character.isWhitespace(value.charAt(i))) { + i++; + } + return value.substring(i); + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/GuardrailResult.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/GuardrailResult.java new file mode 100644 index 000000000..3f1b523e7 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/GuardrailResult.java @@ -0,0 +1,37 @@ +package com.microsoft.prompty; + +/** + * A guardrail's decision about one operation. + * + *

A guardrail may allow, deny with a reason, or allow while substituting a replacement value. + * The rewrite is deliberately only honoured for output checks: rewriting an input or a tool + * argument would silently change what the model was asked, whereas rewriting an output changes + * only what the caller is told. + */ +public record GuardrailResult(boolean allowed, String reason, Object rewrite) { + + /** Allow the operation unchanged. */ + public static GuardrailResult allow() { + return new GuardrailResult(true, null, null); + } + + /** Deny the operation, recording why. */ + public static GuardrailResult deny(String reason) { + return new GuardrailResult(false, reason, null); + } + + /** Allow the operation, substituting {@code rewrite} for the value that was checked. */ + public static GuardrailResult rewrite(Object rewrite) { + return new GuardrailResult(true, null, rewrite); + } + + /** Whether this result carries a replacement value. */ + public boolean hasRewrite() { + return rewrite != null; + } + + /** The denial reason, or {@code fallback} when none was given. */ + public String reasonOr(String fallback) { + return reason == null || reason.isEmpty() ? fallback : reason; + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/Guardrails.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Guardrails.java new file mode 100644 index 000000000..3aab1340d --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Guardrails.java @@ -0,0 +1,79 @@ +package com.microsoft.prompty; + +import com.microsoft.prompty.model.Message; +import com.microsoft.prompty.model.Prompty; +import java.util.List; + +/** + * Optional policy hooks that run around a turn's model calls and tool dispatch. + * + *

Each hook is independent and optional; an absent hook allows unconditionally, so a partially + * configured {@code Guardrails} is always safe to pass. A hook returns a decision rather than + * throwing, which keeps "denied" an ordinary, inspectable outcome instead of an exceptional one — + * the turn engine needs to record a denial durably, not unwind through it. + */ +public final class Guardrails { + + /** Checked before each model call, against the messages about to be sent. */ + @FunctionalInterface + public interface Input { + GuardrailResult check(List messages, Prompty agent); + } + + /** Checked against the final output, once no more tool calls are outstanding. */ + @FunctionalInterface + public interface Output { + GuardrailResult check(Object output, Prompty agent); + } + + /** Checked before each tool execution, against the tool's name and parsed arguments. */ + @FunctionalInterface + public interface Tool { + GuardrailResult check(String name, Object arguments, Prompty agent); + } + + private final Input input; + private final Output output; + private final Tool tool; + + private Guardrails(Input input, Output output, Tool tool) { + this.input = input; + this.output = output; + this.tool = tool; + } + + /** Guardrails with no hooks configured; every check allows. */ + public static Guardrails none() { + return new Guardrails(null, null, null); + } + + /** A copy of these guardrails with the input hook replaced. */ + public Guardrails withInput(Input input) { + return new Guardrails(input, output, tool); + } + + /** A copy of these guardrails with the output hook replaced. */ + public Guardrails withOutput(Output output) { + return new Guardrails(input, output, tool); + } + + /** A copy of these guardrails with the tool hook replaced. */ + public Guardrails withTool(Tool tool) { + return new Guardrails(input, output, tool); + } + + /** Run the input hook, or allow if none is configured. */ + public GuardrailResult checkInput(List messages, Prompty agent) { + return input == null ? GuardrailResult.allow() : input.check(messages, agent); + } + + /** Run the output hook, or allow if none is configured. */ + public GuardrailResult checkOutput(Object output, Prompty agent) { + return this.output == null ? GuardrailResult.allow() : this.output.check(output, agent); + } + + /** Run the tool hook, or allow if none is configured. */ + public GuardrailResult checkTool(String name, Object arguments, Prompty agent) { + return tool == null ? GuardrailResult.allow() : tool.check(name, arguments, agent); + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/Http.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Http.java new file mode 100644 index 000000000..b627ca58a --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Http.java @@ -0,0 +1,333 @@ +package com.microsoft.prompty; + +import com.microsoft.prompty.model.TypraJson; +import java.io.BufferedReader; +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URI; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; + +/** + * The HTTP transport every provider shares. + * + *

Providers differ in what they send, not in how it travels, so connection pooling, error + * classification, and SSE framing live here once. Sharing them also means a fix to any of those — + * particularly the determinate/indeterminate distinction below — reaches every provider at once. + */ +public final class Http { + + /** + * One client for the process. + * + *

Each {@code HttpClient} owns a connection pool and a selector thread, so building one per + * request would both defeat keep-alive and leak threads under load. + */ + private static final HttpClient CLIENT = + HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(30)).build(); + + private Http() {} + + /** + * POST a JSON body and decode the JSON reply. + * + * @param provider the provider name, used only to attribute failures + */ + public static Object postJson( + String provider, String url, Map headers, Object body) { + HttpResponse response = + send(provider, url, headers, body, HttpResponse.BodyHandlers.ofString()); + checkStatus(provider, response.statusCode(), response.body()); + try { + return TypraJson.parse(response.body()); + } catch (RuntimeException e) { + // The provider accepted and acted on the request; only the reply was unreadable. Retrying + // could duplicate whatever it already did, so the caller is told the outcome is unknown. + throw InvokerException.indeterminateExecution( + "Failed to parse " + provider + " response after provider dispatch: " + e.getMessage(), + Map.of("provider", provider, "phase", "response_body")); + } + } + + /** + * GET a URL and decode the JSON reply. + * + *

Unlike a POST, a read has no effect to duplicate, so every failure here is determinate: the + * caller may retry freely. + * + * @param provider the provider name, used only to attribute failures + */ + public static Object getJson(String provider, String url, Map headers) { + return getJson(provider, url, headers, null); + } + + /** + * As {@link #getJson(String, String, Map)}, with a bound on the whole exchange. + * + *

The client-level timeout only covers establishing the connection, so a server that accepts a + * connection and then stalls holds the caller indefinitely. That is tolerable where the caller is + * waiting on a model anyway, and not tolerable for control-plane calls made while a person waits + * on a picker — hence a bound the caller chooses rather than one imposed on every read. + * + * @param timeout the limit on the complete exchange, or null for none + */ + public static Object getJson( + String provider, String url, Map headers, Duration timeout) { + HttpRequest.Builder builder = HttpRequest.newBuilder(URI.create(url)).GET(); + headers.forEach(builder::header); + if (timeout != null) { + builder.timeout(timeout); + } + + HttpResponse response; + try { + response = CLIENT.send(builder.build(), HttpResponse.BodyHandlers.ofString()); + } catch (IOException e) { + throw InvokerException.execute("HTTP request failed: " + e, e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw InvokerException.cancelled("Interrupted while calling " + provider); + } + + checkStatus(provider, response.statusCode(), response.body()); + try { + return TypraJson.parse(response.body()); + } catch (RuntimeException e) { + throw InvokerException.execute( + "Failed to parse " + provider + " response: " + e.getMessage(), e); + } + } + + /** + * POST a JSON body and read the reply as a stream of server-sent events. + * + *

The returned iterator holds an open connection. It closes itself once the stream ends, but a + * caller that abandons it early should close it — see {@link Streams#close}. + */ + public static Iterator postSse( + String provider, String url, Map headers, Object body) { + HttpResponse response = + send(provider, url, headers, body, HttpResponse.BodyHandlers.ofInputStream()); + if (response.statusCode() < 200 || response.statusCode() >= 300) { + checkStatus(provider, response.statusCode(), readAll(response.body())); + } + return new SseIterator(response.body()); + } + + /** + * POST a form-encoded body and return the raw status and body. + * + *

OAuth token endpoints speak {@code application/x-www-form-urlencoded} rather than JSON, and + * unlike every other call here a non-success status is not necessarily a failure: the device-code + * grant reports "the user has not finished signing in yet" as an HTTP error with a machine-readable + * body. Deciding what a status means is therefore left to the caller, and this method throws only + * when the exchange never completed at all. + */ + public static FormResult postForm(String provider, String url, Map form) { + HttpRequest.Builder builder = + HttpRequest.newBuilder(URI.create(url)) + .header("Content-Type", "application/x-www-form-urlencoded") + .timeout(Duration.ofSeconds(30)) + .POST(HttpRequest.BodyPublishers.ofString(encodeForm(form), StandardCharsets.UTF_8)); + + try { + HttpResponse response = CLIENT.send(builder.build(), HttpResponse.BodyHandlers.ofString()); + return new FormResult(response.statusCode(), response.body() == null ? "" : response.body()); + } catch (IOException e) { + throw classifyTransportFailure(provider, e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw InvokerException.cancelled("Interrupted while calling " + provider); + } + } + + /** The status and body of a form POST, before any interpretation. */ + public record FormResult(int status, String body) { + + /** Whether the status is in the 2xx range. */ + public boolean isSuccess() { + return status >= 200 && status < 300; + } + } + + /** + * Encode a map as {@code application/x-www-form-urlencoded}. + * + *

Public because the same encoding serves both a form body and a URL query string — OAuth needs + * one of each, and they must agree on how a space is written. + * + *

{@link URLEncoder} writes a space as {@code +}, which is what this media type specifies and + * what the Rust runtime's form serializer also emits — the two must agree, because a scope such as + * {@code "https://ai.azure.com/.default offline_access"} contains one. + */ + public static String encodeForm(Map form) { + StringBuilder encoded = new StringBuilder(); + for (Map.Entry entry : form.entrySet()) { + if (encoded.length() > 0) { + encoded.append('&'); + } + encoded + .append(URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8)) + .append('=') + .append(URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8)); + } + return encoded.toString(); + } + + private static HttpResponse send( + String provider, + String url, + Map headers, + Object body, + HttpResponse.BodyHandler handler) { + HttpRequest.Builder builder = + HttpRequest.newBuilder(URI.create(url)) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(TypraJson.stringify(body), StandardCharsets.UTF_8)); + headers.forEach(builder::header); + + try { + return CLIENT.send(builder.build(), handler); + } catch (IOException e) { + throw classifyTransportFailure(provider, e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw InvokerException.cancelled("Interrupted while calling " + provider); + } + } + + /** + * Decide whether a transport failure leaves the request's outcome knowable. + * + *

A connection that was never established is determinate: nothing reached the provider, so a + * retry is safe. A failure after the request went out is not — the provider may have completed it + * — and a blind retry could duplicate a tool call or a charge. + */ + private static InvokerException classifyTransportFailure(String provider, IOException error) { + String message = "HTTP request failed: " + error; + // A connect timeout is as determinate as a refused connection: the request never left, so the + // work is simply lost and retrying it cannot duplicate anything. + if (error instanceof java.net.ConnectException + || error instanceof java.net.UnknownHostException + || error instanceof java.net.http.HttpConnectTimeoutException) { + return InvokerException.execute(message, error); + } + return InvokerException.indeterminateExecution( + message, Map.of("provider", provider, "phase", "request_dispatch")); + } + + private static void checkStatus(String provider, int status, String body) { + if (status < 200 || status >= 300) { + throw InvokerException.execute( + provider + " API error (HTTP " + status + "): " + (body == null ? "" : body)); + } + } + + private static String readAll(InputStream stream) { + try (InputStream input = stream) { + return new String(input.readAllBytes(), StandardCharsets.UTF_8); + } catch (IOException e) { + return "unable to read body"; + } + } + + /** + * Frames a server-sent event stream into the JSON payloads it carries. + * + *

Failures are surfaced as {@code error} events in the stream rather than thrown, because a + * stream that has already yielded chunks cannot un-yield them; the consumer needs to see what + * arrived and then why it stopped. + */ + private static final class SseIterator implements Iterator, Closeable { + + private final BufferedReader reader; + private final List pending = new ArrayList<>(); + private boolean done; + + SseIterator(InputStream stream) { + this.reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8)); + } + + @Override + public boolean hasNext() { + fill(); + return !pending.isEmpty(); + } + + @Override + public Object next() { + fill(); + if (pending.isEmpty()) { + throw new NoSuchElementException(); + } + return pending.remove(0); + } + + private void fill() { + while (pending.isEmpty() && !done) { + String line; + try { + line = reader.readLine(); + } catch (IOException e) { + pending.add( + Map.of( + "error", + Map.of("type", "sse_transport_error", "message", "SSE stream error: " + e))); + close(); + return; + } + + if (line == null) { + close(); + return; + } + if (!line.startsWith("data:")) { + // Comments, event names, and the blank lines between events carry no payload. + continue; + } + + String data = line.substring("data:".length()).trim(); + if ("[DONE]".equals(data)) { + close(); + return; + } + if (data.isEmpty()) { + continue; + } + try { + pending.add(TypraJson.parse(data)); + } catch (RuntimeException e) { + pending.add( + Map.of( + "error", + Map.of( + "type", "sse_parse_error", + "message", "Failed to parse SSE data: " + e.getMessage(), + "raw", data))); + } + } + } + + @Override + public void close() { + done = true; + try { + reader.close(); + } catch (IOException e) { + // The stream is already finished; a failure releasing it tells the caller nothing useful + // and must not mask the chunks or the error already queued for them. + } + } + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/InvokerException.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/InvokerException.java new file mode 100644 index 000000000..e82c1c9c1 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/InvokerException.java @@ -0,0 +1,157 @@ +package com.microsoft.prompty; + +import java.util.List; +import java.util.Map; + +/** + * Raised by any of the four pipeline stages — render, parse, execute, process. + * + *

Mirrors the {@code InvokerError} enum in the Rust runtime. The variant is carried on {@link + * #kind()} rather than encoded in a class hierarchy so the set stays closed and comparable across + * runtimes. + */ +public class InvokerException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + /** The class of pipeline failure, mirroring the Rust {@code InvokerError} variants. */ + public enum Kind { + /** No invoker was registered for the requested group and key. */ + NOT_FOUND, + /** The renderer failed. */ + RENDER, + /** The parser failed. */ + PARSE, + /** The executor failed. */ + EXECUTE, + /** + * The provider request may already have been dispatched, so the outcome requires + * reconciliation rather than a retry or a commit. + */ + EXECUTE_INDETERMINATE, + /** The processor failed. */ + PROCESS, + /** Input validation failed. */ + VALIDATION, + /** Loading a {@code .prompty} document failed. */ + LOAD, + /** The operation was cancelled through its cancellation token. */ + CANCELLED, + /** The agent loop exhausted its retries; accumulated conversation state is attached. */ + EXECUTE_RETRY_EXHAUSTED, + /** Any other failure. */ + OTHER + } + + private final Kind kind; + private final Map metadata; + private final List messages; + + public InvokerException(Kind kind, String message) { + this(kind, message, null, null, null); + } + + public InvokerException(Kind kind, String message, Throwable cause) { + this(kind, message, cause, null, null); + } + + private InvokerException( + Kind kind, + String message, + Throwable cause, + Map metadata, + List messages) { + super(message, cause); + this.kind = kind; + this.metadata = metadata; + this.messages = messages; + } + + public Kind kind() { + return kind; + } + + /** + * Reconciliation metadata attached to an {@link Kind#EXECUTE_INDETERMINATE} failure. Empty for + * every other variant. + */ + public Map metadata() { + return metadata == null ? Map.of() : metadata; + } + + /** + * Conversation accumulated before an {@link Kind#EXECUTE_RETRY_EXHAUSTED} failure, so a caller can + * resume rather than restart. Empty for every other variant. + */ + public List messages() { + return messages == null ? List.of() : messages; + } + + public static InvokerException notFound(String group, String key) { + return new InvokerException( + Kind.NOT_FOUND, "no " + group + " registered for key '" + key + "'"); + } + + public static InvokerException render(String message) { + return new InvokerException(Kind.RENDER, "render error: " + message); + } + + public static InvokerException render(String message, Throwable cause) { + return new InvokerException(Kind.RENDER, "render error: " + message, cause); + } + + public static InvokerException parse(String message) { + return new InvokerException(Kind.PARSE, "parse error: " + message); + } + + public static InvokerException execute(String message) { + return new InvokerException(Kind.EXECUTE, "execute error: " + message); + } + + public static InvokerException execute(String message, Throwable cause) { + return new InvokerException(Kind.EXECUTE, "execute error: " + message, cause); + } + + /** + * Mark an execution failure as requiring model-outcome reconciliation. + * + *

Executors should reach for this only once dispatch has become ambiguous. Configuration, + * validation, and connection-establishment failures stay ordinary retryable {@link Kind#EXECUTE} + * errors. + */ + public static InvokerException indeterminateExecution( + String message, Map metadata) { + return new InvokerException( + Kind.EXECUTE_INDETERMINATE, "indeterminate execution: " + message, null, metadata, null); + } + + public static InvokerException process(String message) { + return new InvokerException(Kind.PROCESS, "process error: " + message); + } + + public static InvokerException process(String message, Throwable cause) { + return new InvokerException(Kind.PROCESS, "process error: " + message, cause); + } + + public static InvokerException validation(String message) { + return new InvokerException(Kind.VALIDATION, "validation error: " + message); + } + + public static InvokerException load(String message) { + return new InvokerException(Kind.LOAD, "load error: " + message); + } + + public static InvokerException cancelled(String message) { + return new InvokerException(Kind.CANCELLED, "cancelled: " + message); + } + + public static InvokerException retryExhausted( + String message, List messages) { + return new InvokerException( + Kind.EXECUTE_RETRY_EXHAUSTED, message, null, null, List.copyOf(messages)); + } + + public static InvokerException other(String message) { + return new InvokerException(Kind.OTHER, message); + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/LiveTurn.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/LiveTurn.java new file mode 100644 index 000000000..a0d790c5d --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/LiveTurn.java @@ -0,0 +1,1104 @@ +package com.microsoft.prompty; + +import com.microsoft.prompty.engine.ContextPipeline; +import com.microsoft.prompty.engine.HostPolicyException; +import com.microsoft.prompty.engine.ModelStreamChunk; +import com.microsoft.prompty.engine.PortException; +import com.microsoft.prompty.engine.Ports; +import com.microsoft.prompty.engine.ToolResults; +import com.microsoft.prompty.engine.TurnEngine; +import com.microsoft.prompty.engine.TurnEngineEffects; +import com.microsoft.prompty.engine.TurnEngineException; +import com.microsoft.prompty.engine.TurnEngineRequest; +import com.microsoft.prompty.model.EngineCheckpoint; +import com.microsoft.prompty.model.EngineEvent; +import com.microsoft.prompty.model.EngineEventKind; +import com.microsoft.prompty.model.EnginePermissionDecision; +import com.microsoft.prompty.model.EngineTurnStatus; +import com.microsoft.prompty.model.ErrorChunk; +import com.microsoft.prompty.model.FinalOutputPolicyRequest; +import com.microsoft.prompty.model.FinalOutputPolicyResult; +import com.microsoft.prompty.model.HostPolicyRequest; +import com.microsoft.prompty.model.HostPolicyResult; +import com.microsoft.prompty.model.InvocationContextPortability; +import com.microsoft.prompty.model.InvocationContextState; +import com.microsoft.prompty.model.LoadContext; +import com.microsoft.prompty.model.Message; +import com.microsoft.prompty.model.ModelInvocationRequest; +import com.microsoft.prompty.model.ModelInvocationResponse; +import com.microsoft.prompty.model.ModelToolOutcome; +import com.microsoft.prompty.model.ModelToolRequest; +import com.microsoft.prompty.model.ModelToolResult; +import com.microsoft.prompty.model.Prompty; +import com.microsoft.prompty.model.RetryPolicyRequest; +import com.microsoft.prompty.model.SaveContext; +import com.microsoft.prompty.model.StreamChunk; +import com.microsoft.prompty.model.TextChunk; +import com.microsoft.prompty.model.ThinkingChunk; +import com.microsoft.prompty.model.ToolCall; +import com.microsoft.prompty.model.ToolChunk; +import com.microsoft.prompty.model.TurnEngineResult; +import com.microsoft.prompty.model.TypraJson; +import com.microsoft.prompty.model.UsageChunk; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Runs a live turn by binding the runtime's providers and extensions onto the canonical engine. + * + *

The engine does no I/O of its own — it decides what should happen and asks a port to make it + * happen. Everything here is one of those ports: the executor and processor behind + * {@link Ports.ModelPort}, guardrails and trimming and steering behind {@link Ports.HostPolicyPort}, + * tool dispatch behind {@link Ports.ToolPort}. Keeping the adaptation here rather than in the + * engine is what lets the same loop drive a live provider and a deterministic replay. + * + *

Live events are projected from durable engine events rather than emitted alongside them, so a + * caller watching the turn sees exactly what was committed, in commit order. + */ +final class LiveTurn { + + /** Numbers the anonymous sessions created for turns the caller did not name. */ + private static final AtomicLong TURN_IDS = new AtomicLong(); + + private LiveTurn() {} + + /** Run a turn for an unnamed, non-resumable session. */ + static Object turn(Prompty agent, Map inputs, TurnOptions options) { + long number = TURN_IDS.incrementAndGet(); + TurnEngineRequest request = + TurnEngineRequest.of("legacy-session-" + number, "legacy-turn-" + number, List.of()); + request.inputs = inputs == null ? new LinkedHashMap() : inputs; + return turn(agent, request, options); + } + + /** Run a turn against a caller-owned engine request, which may resume a durable checkpoint. */ + static Object turn(Prompty agent, TurnEngineRequest request, TurnOptions options) { + TurnOptions opts = options == null ? TurnOptions.defaults() : options; + Events events = new Events(opts.onEvent()); + + try (Tracer.Span span = Tracer.start("turn")) { + span.emit("signature", "prompty.turn"); + + Object inputs = request.inputs == null ? new LinkedHashMap() : request.inputs; + request.inputs = inputs; + span.emit("inputs", inputs); + + if (opts.parallelToolCalls()) { + // The engine commits one effect at a time so a resumed turn replays tool results in the + // order they were journaled. Running them concurrently would make that order depend on + // scheduling, which is exactly what durability cannot tolerate. + String message = + "parallel_tool_calls=true is not supported by the canonical engine; tool effects " + + "execute sequentially for deterministic durable ordering"; + events.emit(new AgentEvent.TurnStart(agent.name, opts.maxIterations())); + events.emit(new AgentEvent.Error(message)); + events.emit(new AgentEvent.TurnEnd("error", 0, null)); + span.emit("error", message); + throw InvokerException.validation(message); + } + + String provider = Pipeline.provider(agent); + boolean streaming = Pipeline.isStreaming(agent); + boolean agentMode = + !opts.tools().isEmpty() || (agent.tools != null && !agent.tools.isEmpty()); + + Failures failures = new Failures(); + AtomicBoolean skipOutputGuardrail = new AtomicBoolean(false); + + request.maxIterations = + agentMode ? opts.maxIterations() : Math.max(opts.maxIterations(), 1); + request.maxModelAttempts = Math.max(opts.maxLlmRetries(), 1); + + Durability durability = + new Durability( + events, + agent.name, + provider, + agent.model == null || agent.model.id == null || agent.model.id.isEmpty() + ? null + : agent.model.id, + opts.maxIterations(), + agentMode, + opts.durability()); + + TurnEngineEffects effects = + TurnEngineEffects.of( + new LiveModel( + agent, + provider, + streaming, + opts.raw() && !agentMode, + agentMode, + skipOutputGuardrail, + failures)) + .withStream(new LiveStream(events)) + .withPolicy( + new LivePolicy( + agent, + inputs, + opts, + // A resumed turn already has its prepared conversation in the checkpoint; + // re-preparing would discard the tool exchange it was resumed to continue. + request.startIteration > 0 + || request.policyAppliedForIteration + || !request.messages.isEmpty(), + skipOutputGuardrail, + failures)) + .withRetry(new LiveRetry(events, failures)) + .withConversation(new LiveConversation(provider, failures)) + .withPermission( + opts.permission() != null + ? opts.permission() + : new LivePermission(agent, opts.guardrails())) + .withTools(new LiveTool(agent, inputs, opts.tools(), events)) + .withDurability(durability); + + if (opts.postCommit() != null) { + effects = effects.withPostCommit(opts.postCommit()); + } + + TurnEngine engine = new TurnEngine(ContextPipeline.appendOnly(), effects); + + Object result; + try { + result = finish(engine.run(request, opts.cancellation()), opts, failures); + } catch (TurnEngineException failure) { + durability.finishUncommittedError(); + InvokerException recorded = failures.takeInvoker(); + throw recorded != null ? recorded : InvokerException.execute(failure.getMessage(), failure); + } catch (InvokerException failure) { + durability.finishUncommittedError(); + throw failure; + } + + span.emit("result", result); + return result; + } + } + + /** Map a committed turn onto the value a caller expects, or the failure it describes. */ + private static Object finish(TurnEngineResult result, TurnOptions opts, Failures failures) { + EngineTurnStatus status = result.commit.status; + + if (status == EngineTurnStatus.SUCCESS) { + return result.commit.output; + } + + if (status == EngineTurnStatus.CANCELLED) { + throw InvokerException.cancelled(failures.cancellationReasonOr("Operation cancelled")); + } + + Object output = result.commit.output; + String errorKind = stringAt(output, "errorKind", "engine_error"); + String message = stringAt(output, "message", "Turn failed"); + + switch (errorKind) { + case "prepare_error" -> throw orElse(failures.takeInvoker(), InvokerException.other(message)); + case "output_validation_failed" -> throw InvokerException.validation(message); + case "model_error" -> throw InvokerException.retryExhausted( + "LLM call failed after " + opts.maxLlmRetries() + " retries: " + message, + result.commit.messages == null ? List.of() : result.commit.messages); + case "model_outcome_unknown" -> throw orElse( + failures.takeInvoker(), InvokerException.execute(message)); + case "max_iterations" -> throw InvokerException.execute( + "Agent loop exceeded max iterations (" + opts.maxIterations() + ")"); + default -> throw InvokerException.execute(message); + } + } + + private static InvokerException orElse(InvokerException recorded, InvokerException fallback) { + return recorded != null ? recorded : fallback; + } + + private static String stringAt(Object value, String key, String fallback) { + if (value instanceof Map map && map.get(key) instanceof String text && !text.isEmpty()) { + return text; + } + return fallback; + } + + // ------------------------------------------------------------------------- + // Event fan-out + // ------------------------------------------------------------------------- + + /** + * Delivers events to the caller's listener. + * + *

A listener that throws is ignored on purpose: observing a turn must not be able to change + * it, and a broken progress display is not a reason to abandon work the model already did. + */ + private static final class Events { + private final AgentEvent.Listener listener; + + Events(AgentEvent.Listener listener) { + this.listener = listener; + } + + void emit(AgentEvent event) { + if (listener == null) { + return; + } + try { + listener.onEvent(event); + } catch (RuntimeException ignored) { + // Observation must not perturb execution. + } + } + } + + /** + * Carries a rich failure across the port boundary. + * + *

Ports may only fail with {@link PortException}, which is deliberately narrow — the engine + * needs to know whether an effect definitely did not happen, not why. But the caller wants the + * original error, so the specific exception is stashed here on the way out and recovered when the + * turn resolves. + */ + private static final class Failures { + private volatile InvokerException invoker; + private volatile String cancellationReason; + + PortException record(InvokerException failure) { + this.invoker = failure; + if (failure.kind() == InvokerException.Kind.EXECUTE_INDETERMINATE) { + return PortException.indeterminate(failure.getMessage(), failure.metadata()); + } + return PortException.of(failure.getMessage(), failure); + } + + InvokerException takeInvoker() { + InvokerException taken = invoker; + invoker = null; + return taken; + } + + void setCancellationReason(String reason) { + this.cancellationReason = reason; + } + + String cancellationReasonOr(String fallback) { + String reason = cancellationReason; + return reason == null || reason.isEmpty() ? fallback : reason; + } + } + + // ------------------------------------------------------------------------- + // Host policy — prepare, steering, trimming, guardrails, validation + // ------------------------------------------------------------------------- + + /** + * The seam every conversation-shaping extension plugs into. + * + *

The engine calls this once before each model invocation and once before committing the + * final output. Doing the work here rather than inside the loop means every rewrite is visible + * to the durable journal as a policy event, so a resumed turn sees the same conversation the + * original one did. + */ + private static final class LivePolicy implements Ports.HostPolicyPort { + private final Prompty agent; + private final Object inputs; + private final TurnOptions options; + private final AtomicBoolean prepared; + private final AtomicBoolean skipOutputGuardrail; + private final Failures failures; + + LivePolicy( + Prompty agent, + Object inputs, + TurnOptions options, + boolean alreadyPrepared, + AtomicBoolean skipOutputGuardrail, + Failures failures) { + this.agent = agent; + this.inputs = inputs; + this.options = options; + this.prepared = new AtomicBoolean(alreadyPrepared); + this.skipOutputGuardrail = skipOutputGuardrail; + this.failures = failures; + } + + @Override + public HostPolicyResult beforeModel(HostPolicyRequest request, CancellationToken cancellation) { + List messages = + request.messages == null ? new ArrayList<>() : new ArrayList<>(request.messages); + int stablePrefix = + Math.min(Math.max(0, request.stablePrefixMessages == null ? 0 : request.stablePrefixMessages), + messages.size()); + boolean preparedNow = false; + + if (prepared.compareAndSet(false, true)) { + try { + messages = new ArrayList<>(Pipeline.prepare(agent, asInputMap(inputs))); + } catch (InvokerException failure) { + failures.record(failure); + throw new HostPolicyException("prepare_error", failure.getMessage()); + } + stablePrefix = messages.size(); + preparedNow = true; + } + + int steeringCount = 0; + Steering steering = options.steering(); + if (steering != null) { + List injected = steering.drain(); + steeringCount = injected.size(); + messages.addAll(injected); + } + + int trimmedCount = 0; + Integer budget = options.contextBudget(); + if (budget != null) { + List beforeTrim = messages; + Context.Trimmed trimmed = Context.trimToContextWindow(messages, budget); + trimmedCount = trimmed.dropped().size(); + List kept = trimmed.messages(); + if (trimmedCount > 0 && options.compaction() != null) { + compact(options.compaction(), trimmed.dropped(), kept); + } + // The stable prefix is a claim about what the provider has already seen. Trimming can + // invalidate it, so it is re-derived from where the two lists actually still agree. + stablePrefix = Math.min(stablePrefix, commonPrefixLength(beforeTrim, kept)); + messages = kept; + } + + Guardrails guardrails = options.guardrails(); + if (guardrails != null) { + GuardrailResult decision = guardrails.checkInput(messages, agent); + if (!decision.allowed()) { + throw new HostPolicyException( + "input_guardrail_denied", + "Input guardrail denied: " + decision.reasonOr("Input denied")); + } + } + + HostPolicyResult result = new HostPolicyResult(); + result.messages = messages; + result.stablePrefixMessages = stablePrefix; + result.metadata = new LinkedHashMap<>(); + result.metadata.put("prepared", preparedNow); + result.metadata.put("steeringCount", steeringCount); + result.metadata.put("trimmedCount", trimmedCount); + result.metadata.put("notifyMessagesUpdated", steeringCount > 0 || trimmedCount > 0); + return result; + } + + @Override + public FinalOutputPolicyResult beforeCommit( + FinalOutputPolicyRequest request, CancellationToken cancellation) { + Object output = request.output; + + Guardrails guardrails = options.guardrails(); + if (guardrails != null && !skipOutputGuardrail.get()) { + GuardrailResult decision = guardrails.checkOutput(output, agent); + if (!decision.allowed()) { + throw new HostPolicyException( + "output_guardrail_denied", + "Output guardrail denied: " + decision.reasonOr("Output denied")); + } + if (decision.hasRewrite()) { + output = decision.rewrite(); + } + } + + output = StructuredResult.unwrap(output); + + if (options.validator() != null) { + String failure = options.validator().apply(output); + if (failure != null && !failure.isEmpty()) { + throw new HostPolicyException( + "output_validation_failed", "Output validation failed: " + failure); + } + } + + FinalOutputPolicyResult result = new FinalOutputPolicyResult(); + result.output = output; + return result; + } + + /** + * Replace the mechanical summary with a model-written one, in place. + * + *

Best-effort: a summarizer that fails or returns nothing leaves the default summary + * standing, because losing the compaction is far better than losing the turn. + */ + private static void compact(Compaction compaction, List dropped, List kept) { + String summary; + try { + summary = compaction.summarize(dropped); + } catch (RuntimeException failure) { + return; + } + if (summary == null || summary.isBlank()) { + return; + } + for (int i = 0; i < kept.size(); i++) { + Message message = kept.get(i); + if (message.role == com.microsoft.prompty.model.Role.USER + && Messages.text(message).startsWith("[Context summary:")) { + kept.set(i, Messages.user("[Context summary: " + summary + "]")); + return; + } + } + } + + private static int commonPrefixLength(List left, List right) { + int limit = Math.min(left.size(), right.size()); + int count = 0; + SaveContext context = new SaveContext(); + while (count < limit + && left.get(count).save(context).equals(right.get(count).save(context))) { + count++; + } + return count; + } + + @SuppressWarnings("unchecked") + private static Map asInputMap(Object inputs) { + return inputs instanceof Map map ? (Map) map : Map.of(); + } + } + + // ------------------------------------------------------------------------- + // Model invocation + // ------------------------------------------------------------------------- + + /** Invokes the registered executor and processor for one prepared context. */ + private static final class LiveModel implements Ports.ModelPort { + private final Prompty agent; + private final String provider; + private final boolean streaming; + private final boolean rawFinal; + private final boolean agentMode; + private final AtomicBoolean skipOutputGuardrail; + private final Failures failures; + + LiveModel( + Prompty agent, + String provider, + boolean streaming, + boolean rawFinal, + boolean agentMode, + AtomicBoolean skipOutputGuardrail, + Failures failures) { + this.agent = agent; + this.provider = provider; + this.streaming = streaming; + this.rawFinal = rawFinal; + this.agentMode = agentMode; + this.skipOutputGuardrail = skipOutputGuardrail; + this.failures = failures; + } + + @Override + public ModelInvocationResponse invoke( + ModelInvocationRequest request, + CancellationToken cancellation, + Ports.ModelStreamPort stream) { + if (cancellation.isCancelled()) { + throw PortException.of("Operation cancelled"); + } + + if (!streaming) { + return nonStreaming(request, cancellation, null); + } + + Iterator raw; + try { + raw = Registry.executor(provider).executeStreamWithContext(agent, request, cancellation); + } catch (InvokerException streamFailure) { + if (streamFailure.kind() == InvokerException.Kind.EXECUTE_INDETERMINATE) { + // The provider may or may not have run. Falling back would risk a duplicate call, so + // the engine is told the outcome is unknown and handles reconciliation. + throw failures.record(streamFailure); + } + try { + return nonStreaming(request, cancellation, streamFailure.getMessage()); + } catch (InvokerException fallbackFailure) { + throw failures.record( + InvokerException.execute( + streamFailure.getMessage() + + " (stream), then " + + fallbackFailure.getMessage() + + " (non-stream)")); + } + } + + return consumeStream(request, cancellation, stream, raw); + } + + private ModelInvocationResponse consumeStream( + ModelInvocationRequest request, + CancellationToken cancellation, + Ports.ModelStreamPort stream, + Iterator raw) { + List rawChunks = new ArrayList<>(); + StringBuilder text = new StringBuilder(); + List toolCalls = new ArrayList<>(); + com.microsoft.prompty.model.InvocationUsage usage = null; + + Iterator tee = Streams.peeking(raw, rawChunks::add); + Iterator chunks; + try { + chunks = Registry.processor(provider).processStream(agent, tee); + } catch (InvokerException failure) { + Streams.close(raw); + throw failures.record(failure); + } + + try { + while (chunks.hasNext()) { + if (cancellation.isCancelled()) { + throw PortException.of("Operation cancelled"); + } + StreamChunk chunk = chunks.next(); + if (chunk instanceof TextChunk value) { + stream.emit(new ModelStreamChunk.Text(value.value)); + text.append(value.value); + } else if (chunk instanceof ThinkingChunk value) { + stream.emit(new ModelStreamChunk.Thinking(value.value)); + } else if (chunk instanceof ToolChunk value) { + toolCalls.add(value.toolCall); + } else if (chunk instanceof UsageChunk value) { + usage = value.usage; + } else if (chunk instanceof StreamFailure failure) { + // A stream that dies mid-flight may already have been completed by the provider, in + // which case retrying would run the same tools and incur the same charge twice. Keeping + // the indeterminate marking is what lets the engine reconcile rather than blindly retry. + throw failures.record( + failure.outcomeUnknown + ? InvokerException.indeterminateExecution( + failure.message, + Map.of("provider", provider, "phase", "stream_transport")) + : InvokerException.execute(failure.message)); + } else if (chunk instanceof ErrorChunk value) { + throw failures.record(InvokerException.execute(value.message)); + } + } + } catch (InvokerException failure) { + throw failures.record(failure); + } finally { + Streams.close(chunks); + Streams.close(raw); + } + + // Responses-style providers deliver the authoritative final object as a terminal chunk. + // When present it is processed directly, because it carries structure the deltas do not. + Object completed = completedResponse(rawChunks); + if (completed != null) { + ModelInvocationResponse response = processed(completed, request); + if (isEmpty(response.toolRequests)) { + response.output = StructuredResult.unwrap(response.output); + } + response.metadata = + envelope(completed, rawChunks, asText(response.output), true, response.metadata, null); + return response; + } + + ModelInvocationResponse response = new ModelInvocationResponse(); + response.toolRequests = normalize(toolCalls); + response.output = + response.toolRequests.isEmpty() ? StructuredResult.unwrap(text.toString()) : null; + response.usage = usage; + response.assistantMessages = new ArrayList<>(); + response.nextContextState = portable(); + response.metadata = envelope(null, rawChunks, text.toString(), true, null, null); + return response; + } + + private ModelInvocationResponse nonStreaming( + ModelInvocationRequest request, CancellationToken cancellation, String streamError) { + Object raw; + ModelInvocationResponse response; + try { + raw = Registry.executor(provider).executeWithContext(agent, request, cancellation); + response = + rawFinal && !agentMode + ? Registry.processor(provider).processRawWithContext(agent, raw, request) + : Registry.processor(provider).processWithContext(agent, raw, request); + } catch (InvokerException failure) { + throw failures.record(failure); + } + + if (rawFinal && !agentMode) { + // The caller asked for the provider's own words. Guardrails inspect processed output, so + // running them over an unprocessed body would compare against a shape they never expect. + skipOutputGuardrail.set(true); + response.output = raw; + response.toolRequests = new ArrayList<>(); + } else if (isEmpty(response.toolRequests)) { + response.output = StructuredResult.unwrap(response.output); + } + + response.metadata = + envelope(raw, List.of(), asText(response.output), false, response.metadata, streamError); + return response; + } + + private ModelInvocationResponse processed(Object raw, ModelInvocationRequest request) { + try { + return Registry.processor(provider).processWithContext(agent, raw, request); + } catch (InvokerException failure) { + throw failures.record(failure); + } + } + + /** + * Metadata the conversation port needs to rebuild a provider-valid tool exchange. + * + *

Formatting a tool round is provider-specific and needs the original response, so it is + * carried forward here rather than reconstructed from the normalized contract, which has + * already discarded the provider's shape. + */ + private static Map envelope( + Object rawResponse, + List rawChunks, + String textContent, + boolean streamed, + Map providerMetadata, + String streamError) { + Map metadata = new LinkedHashMap<>(); + metadata.put("rawResponse", rawResponse); + metadata.put("rawChunks", rawChunks); + metadata.put("textContent", textContent); + metadata.put("streamed", streamed); + metadata.put("providerMetadata", providerMetadata); + if (streamError != null) { + metadata.put("streamError", streamError); + } + return metadata; + } + + private static Object completedResponse(List rawChunks) { + for (int i = rawChunks.size() - 1; i >= 0; i--) { + if (rawChunks.get(i) instanceof Map chunk + && "response.completed".equals(chunk.get("type")) + && chunk.get("response") != null) { + return chunk.get("response"); + } + } + return null; + } + + /** + * Convert provider tool calls to the engine's contract. + * + *

The raw argument text is preserved alongside the parsed value: providers expect their own + * encoding echoed back verbatim in the next request, and re-serializing a parsed object would + * not reproduce it byte for byte. + */ + private static List normalize(List toolCalls) { + List requests = new ArrayList<>(toolCalls.size()); + for (ToolCall call : toolCalls) { + ModelToolRequest request = new ModelToolRequest(); + request.id = call.id; + request.name = call.name; + Object parsed; + try { + parsed = TypraJson.parse(call.arguments); + } catch (RuntimeException notJson) { + parsed = call.arguments; + } + request.arguments = parsed; + request.metadata = new LinkedHashMap<>(); + request.metadata.put("argumentsText", call.arguments); + requests.add(request); + } + return requests; + } + + private static InvocationContextState portable() { + InvocationContextState state = new InvocationContextState(); + state.portability = InvocationContextPortability.PORTABLE; + state.delegatedState = new ArrayList<>(); + return state; + } + + private static String asText(Object output) { + return output instanceof String text ? text : null; + } + + private static boolean isEmpty(List list) { + return list == null || list.isEmpty(); + } + } + + /** Forwards in-flight model chunks to the caller as events. */ + private record LiveStream(Events events) implements Ports.ModelStreamPort { + @Override + public void emit(ModelStreamChunk chunk) { + if (chunk instanceof ModelStreamChunk.Text text) { + events.emit(new AgentEvent.Token(text.value())); + } else if (chunk instanceof ModelStreamChunk.Thinking thinking) { + events.emit(new AgentEvent.Thinking(thinking.value())); + } + } + } + + /** Reports each retry to the caller. */ + private record LiveRetry(Events events, Failures failures) implements Ports.RetryPolicyPort { + @Override + public void backoff(RetryPolicyRequest request, CancellationToken cancellation) { + if (cancellation.isCancelled()) { + failures.setCancellationReason("Operation cancelled"); + throw PortException.of("Operation cancelled"); + } + events.emit( + new AgentEvent.Retry( + "model", + request.nextAttempt == null ? 0 : request.nextAttempt, + request.maxAttempts == null ? 0 : request.maxAttempts, + request.reason == null ? "" : request.reason)); + } + } + + // ------------------------------------------------------------------------- + // Conversation, permission, tools + // ------------------------------------------------------------------------- + + /** Delegates tool-exchange formatting to the provider's executor. */ + private record LiveConversation(String provider, Failures failures) + implements Ports.ConversationPort { + + @Override + public List formatToolExchange( + ModelInvocationResponse response, List results) { + if (response.toolRequests == null || response.toolRequests.isEmpty() || results.isEmpty()) { + throw PortException.configuration( + "tool conversation formatting requires non-empty requests and results"); + } + + List calls = new ArrayList<>(response.toolRequests.size()); + List outputs = new ArrayList<>(response.toolRequests.size()); + for (ModelToolRequest request : response.toolRequests) { + ToolCall call = new ToolCall(); + call.id = request.id; + call.name = request.name; + call.arguments = argumentsText(request); + calls.add(call); + + // Results are matched by request id rather than position: the engine may commit them out + // of order across a resume, and pairing the wrong result with a call would silently + // mislead the model. + String output = ""; + for (ModelToolResult result : results) { + if (result.requestId != null && result.requestId.equals(request.id)) { + output = ToolResults.modelText(result); + break; + } + } + outputs.add(output); + } + + Map metadata = response.metadata == null ? Map.of() : response.metadata; + String textContent = + metadata.get("textContent") instanceof String text ? text : null; + boolean streamed = Boolean.TRUE.equals(metadata.get("streamed")); + + try { + Executor executor = Registry.executor(provider); + if (streamed) { + List rawChunks = + metadata.get("rawChunks") instanceof List chunks + ? new ArrayList<>(chunks) + : List.of(); + return executor.formatStreamToolMessages(rawChunks, calls, outputs, textContent); + } + return executor.formatToolMessages(metadata.get("rawResponse"), calls, outputs, textContent); + } catch (InvokerException failure) { + throw failures.record(failure); + } + } + } + + /** Applies the tool guardrail, when one is configured. */ + private record LivePermission(Prompty agent, Guardrails guardrails) + implements Ports.PermissionPort { + + @Override + public EnginePermissionDecision authorize( + ModelToolRequest request, CancellationToken cancellation) { + EnginePermissionDecision decision = new EnginePermissionDecision(); + if (guardrails == null) { + decision.approved = true; + return decision; + } + + Object arguments; + try { + arguments = ToolDispatch.parseArguments(argumentsText(request)); + } catch (IllegalArgumentException unparseable) { + arguments = Map.of(); + } + + GuardrailResult result = guardrails.checkTool(request.name, arguments, agent); + if (result.allowed()) { + decision.approved = true; + return decision; + } + + decision.approved = false; + // The reason is written as the model-visible result text, so it reads as a tool outcome the + // model can respond to rather than an opaque refusal. + decision.reason = "Error: Tool guardrail denied: " + result.reasonOr("Tool denied"); + decision.metadata = new LinkedHashMap<>(); + decision.metadata.put("errorKind", "guardrail_denied"); + return decision; + } + } + + /** Runs one authorized tool request through the dispatcher. */ + private record LiveTool( + Prompty agent, Object inputs, Map tools, Events events) + implements Ports.ToolPort { + + @Override + public ModelToolResult execute(ModelToolRequest request, CancellationToken cancellation) { + ToolCall call = new ToolCall(); + call.id = request.id; + call.name = request.name; + call.arguments = argumentsText(request); + + String output; + try { + output = ToolDispatch.dispatch(call, tools, agent, inputs); + } catch (RuntimeException unexpected) { + // The dispatcher already converts handler failures to text; reaching here means the + // dispatcher itself failed. Reporting it as a tool error keeps the turn recoverable. + String message = unexpected.getMessage(); + output = + "Error: Tool '" + + request.name + + "' failed: " + + (message == null ? unexpected.getClass().getSimpleName() : message); + events.emit(new AgentEvent.Error(output)); + } + + boolean failed = output.startsWith("Error:"); + ModelToolResult result = new ModelToolResult(); + result.requestId = request.id; + result.name = request.name; + result.outcome = failed ? ModelToolOutcome.FAILED : ModelToolOutcome.SUCCESS; + result.output = output; + result.errorKind = failed ? "tool_error" : null; + return result; + } + } + + /** + * The argument text a provider expects echoed back. + * + *

Prefers the verbatim text the provider sent; falls back to re-encoding the parsed value for + * requests that were reconstructed from a checkpoint rather than received live. + */ + private static String argumentsText(ModelToolRequest request) { + if (request.metadata != null + && request.metadata.get("argumentsText") instanceof String text) { + return text; + } + if (request.arguments instanceof String text) { + return text; + } + return request.arguments == null ? "" : TypraJson.stringify(request.arguments); + } + + // ------------------------------------------------------------------------- + // Durability and event projection + // ------------------------------------------------------------------------- + + /** + * Persists the journal and projects it as live events. + * + *

Projection happens here, after persistence, so a caller never observes something the + * journal does not record. The terminal event is emitted at most once regardless of how the turn + * ends, because a caller that has been told the turn is over must not be told again. + */ + private static final class Durability implements Ports.DurabilityPort { + private final Events events; + private final String agentName; + private final String provider; + private final String modelId; + private final int configuredMaxIterations; + private final boolean agentMode; + private final Ports.DurabilityPort delegate; + + private List messages = List.of(); + private int completedModelIterations; + private boolean terminalEmitted; + + Durability( + Events events, + String agentName, + String provider, + String modelId, + int configuredMaxIterations, + boolean agentMode, + Ports.DurabilityPort delegate) { + this.events = events; + this.agentName = agentName; + this.provider = provider; + this.modelId = modelId; + this.configuredMaxIterations = configuredMaxIterations; + this.agentMode = agentMode; + this.delegate = delegate; + } + + @Override + public void append(EngineEvent event) { + if (delegate != null) { + delegate.append(event); + } + project(event); + } + + @Override + public void appendWithCheckpoint(List batch, EngineCheckpoint checkpoint) { + if (delegate != null) { + delegate.appendWithCheckpoint(batch, checkpoint); + } + messages = checkpoint.messages == null ? List.of() : List.copyOf(checkpoint.messages); + completedModelIterations = + checkpoint.completedModelIterations == null ? 0 : checkpoint.completedModelIterations; + + for (EngineEvent event : batch) { + project(event); + } + + // A tool round that leaves nothing outstanding has just folded its results into the + // conversation, which is a change the caller should see even though no separate + // conversation event was written for it. + boolean toolCommitted = false; + for (EngineEvent event : batch) { + if (event.kind == EngineEventKind.TOOL_EXECUTION_COMPLETED + || event.kind == EngineEventKind.TOOL_RESULT_COMMITTED) { + toolCommitted = true; + break; + } + } + if (toolCommitted + && isEmpty(checkpoint.pendingToolRequests) + && checkpoint.pendingModelResponse == null) { + events.emit(new AgentEvent.MessagesUpdated(messages)); + } + } + + private void project(EngineEvent event) { + Map payload = + event.payload instanceof Map map ? castMap(map) : Map.of(); + + switch (event.kind) { + case TURN_STARTED -> + events.emit(new AgentEvent.TurnStart(agentName, configuredMaxIterations)); + + case POLICY_APPLIED -> { + Map metadata = + payload.get("metadata") instanceof Map map ? castMap(map) : Map.of(); + long steeringCount = asLong(metadata.get("steeringCount")); + if (steeringCount > 0) { + events.emit( + new AgentEvent.Status("Injected " + steeringCount + " steering message(s)")); + } + if (Boolean.TRUE.equals(metadata.get("notifyMessagesUpdated"))) { + events.emit(new AgentEvent.MessagesUpdated(messages)); + } + } + + case MODEL_INVOCATION_STARTED -> + events.emit( + new AgentEvent.LlmStart( + provider, + modelId, + (int) asLong(payload.get("messageCount")), + event.iteration == null ? 0 : event.iteration)); + + case MODEL_INVOCATION_COMPLETED, MODEL_INVOCATION_RECONCILED -> + events.emit(new AgentEvent.LlmComplete(event.iteration == null ? 0 : event.iteration)); + + case TOOL_EXECUTION_STARTED -> { + if (payload.get("toolRequest") instanceof Map map) { + ModelToolRequest request = ModelToolRequest.load(map, new LoadContext()); + events.emit(new AgentEvent.ToolCallStart(request.name, argumentsText(request))); + } + } + + case TOOL_EXECUTION_COMPLETED, TOOL_RESULT_COMMITTED -> { + if (payload.get("toolResult") instanceof Map map) { + ModelToolResult result = ModelToolResult.load(map, new LoadContext()); + String output = ToolResults.modelText(result); + events.emit(new AgentEvent.ToolResult(result.name, output)); + events.emit( + new AgentEvent.ToolCallComplete( + result.name, + result.outcome == ModelToolOutcome.SUCCESS, + output, + result.errorKind)); + } + } + + case CONVERSATION_UPDATED -> events.emit(new AgentEvent.MessagesUpdated(messages)); + + case TURN_COMMITTED -> projectTerminal(payload, "success"); + + case TURN_CANCELLED -> { + events.emit(new AgentEvent.Cancelled()); + projectTerminal(payload, "cancelled"); + } + + case TURN_FAILED, TURN_RECONCILIATION_REQUIRED -> { + if (payload.get("output") instanceof Map output + && "max_iterations".equals(output.get("errorKind"))) { + events.emit( + new AgentEvent.Error( + "Agent loop exceeded max iterations (" + configuredMaxIterations + ")")); + } + projectTerminal(payload, "error"); + } + + default -> {} + } + } + + private void projectTerminal(Map payload, String status) { + if (terminalEmitted) { + return; + } + terminalEmitted = true; + // Iterations are only meaningful for an agent loop; a plain round-trip reports zero rather + // than claiming a loop it never ran. + int iterations = agentMode ? completedModelIterations : 0; + boolean success = "success".equals(status); + Object response = success ? payload.get("output") : null; + if (success) { + events.emit(new AgentEvent.Done(response, messages)); + } + events.emit(new AgentEvent.TurnEnd(status, iterations, response)); + } + + /** Report a turn that failed before it could commit anything. */ + void finishUncommittedError() { + if (terminalEmitted) { + return; + } + terminalEmitted = true; + events.emit( + new AgentEvent.TurnEnd("error", agentMode ? completedModelIterations : 0, null)); + } + + private static long asLong(Object value) { + return value instanceof Number number ? number.longValue() : 0L; + } + + @SuppressWarnings("unchecked") + private static Map castMap(Map map) { + return (Map) map; + } + + private static boolean isEmpty(List list) { + return list == null || list.isEmpty(); + } + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/LoadException.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/LoadException.java new file mode 100644 index 000000000..e91556bfe --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/LoadException.java @@ -0,0 +1,81 @@ +package com.microsoft.prompty; + +/** + * Raised when a {@code .prompty} document cannot be read, its frontmatter is malformed, or a + * {@code ${env:...}} / {@code ${file:...}} reference cannot be resolved. + * + *

Mirrors the {@code LoadError} enum in the Rust runtime. The variant is carried on {@link #kind} + * so callers can branch without string matching, while {@link #getMessage()} keeps the wording + * aligned across runtimes. + */ +public class LoadException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + /** The class of load failure, mirroring the Rust {@code LoadError} variants. */ + public enum Kind { + /** The prompt file could not be opened or read. */ + FILE_NOT_FOUND, + /** The frontmatter was not a well-formed YAML mapping, or the delimiters were unbalanced. */ + INVALID_FRONTMATTER, + /** A {@code ${env:VAR}} reference named an unset variable and declared no default. */ + ENV_VAR_NOT_SET, + /** A {@code ${file:path}} reference could not be read, parsed, or escaped its allowed roots. */ + FILE_REFERENCE, + /** + * {@code template} was authored as a bare string. Prompty v2 requires an object carrying + * {@code format} and {@code parser}. + */ + INVALID_TEMPLATE, + /** Any other load failure. */ + OTHER + } + + private final Kind kind; + + public LoadException(Kind kind, String message) { + super(message); + this.kind = kind; + } + + public LoadException(Kind kind, String message, Throwable cause) { + super(message, cause); + this.kind = kind; + } + + public Kind kind() { + return kind; + } + + /* + * Message wording is kept character-for-character in step with the Rust runtime's + * `LoadError::Display`, so a diagnostic produced by either runtime reads identically and the + * shared vector suite's error matching behaves the same for both. + */ + + public static LoadException fileNotFound(String path, String detail) { + return new LoadException(Kind.FILE_NOT_FOUND, "File not found: " + path + ": " + detail); + } + + public static LoadException invalidFrontmatter(String detail) { + return new LoadException(Kind.INVALID_FRONTMATTER, "Invalid frontmatter: " + detail); + } + + public static LoadException envVarNotSet(String varName, String key) { + return new LoadException( + Kind.ENV_VAR_NOT_SET, + "Environment variable '" + varName + "' not set for key '" + key + "'"); + } + + public static LoadException fileReference(String path, String detail) { + return new LoadException(Kind.FILE_REFERENCE, "File reference error: " + path + ": " + detail); + } + + public static LoadException invalidTemplate(String detail) { + return new LoadException(Kind.INVALID_TEMPLATE, "Invalid template format: " + detail); + } + + public static LoadException other(String detail) { + return new LoadException(Kind.OTHER, "Load error: " + detail); + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/LoadOptions.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/LoadOptions.java new file mode 100644 index 000000000..9e1a07064 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/LoadOptions.java @@ -0,0 +1,29 @@ +package com.microsoft.prompty; + +import java.nio.file.Path; +import java.util.List; + +/** + * Options controlling how a {@code .prompty} document is loaded. + * + * @param allowedFileRoots additional directories that {@code ${file:...}} references may read from. + * The prompt file's own directory is always allowed and need not be listed. + */ +public record LoadOptions(List allowedFileRoots) { + + private static final LoadOptions DEFAULT = new LoadOptions(List.of()); + + public LoadOptions { + allowedFileRoots = List.copyOf(allowedFileRoots); + } + + /** Default options: {@code ${file:...}} is confined to the prompt's own directory tree. */ + public static LoadOptions defaults() { + return DEFAULT; + } + + /** Options allowing {@code ${file:...}} to additionally read from the given roots. */ + public static LoadOptions withAllowedFileRoots(Path... roots) { + return new LoadOptions(List.of(roots)); + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/Loader.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Loader.java new file mode 100644 index 000000000..66930d6e1 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Loader.java @@ -0,0 +1,249 @@ +package com.microsoft.prompty; + +import com.microsoft.prompty.model.LoadContext; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Loads {@code .prompty} documents into typed {@link com.microsoft.prompty.model.Prompty} values. + * + *

The pipeline is fixed by spec §4.3 and matched to the Rust runtime: + * + *

    + *
  1. Read the file and normalise {@code \r\n} to {@code \n}. + *
  2. Split YAML frontmatter from the markdown body. + *
  3. Trim trailing newlines from the body and, if anything remains, set it as + * {@code instructions}. + *
  4. Normalise authored shorthands — dictionary-form inputs and outputs whose value is a bare + * scalar become full properties. + *
  5. Inject {@code kind: "prompt"}; a {@code .prompty} document always describes a prompt agent. + *
  6. Resolve {@code ${env:...}} and {@code ${file:...}} references. + *
  7. Hand the resulting map to the generated model's {@code load}. + *
  8. Record the source path in {@code metadata.__source_path}. + *
+ * + *

Per the spec the runtime never auto-loads a {@code .env} file. Populating the environment is + * the application's job, which keeps loading deterministic and keeps secrets out of the library's + * control flow. + */ +public final class Loader { + + /** Metadata key recording the absolute path a prompt was loaded from. */ + public static final String SOURCE_PATH_KEY = "__source_path"; + + private Loader() {} + + /** Load a {@code .prompty} file using default options. */ + public static com.microsoft.prompty.model.Prompty load(Path path) { + return load(path, LoadOptions.defaults()); + } + + /** Load a {@code .prompty} file using default options. */ + public static com.microsoft.prompty.model.Prompty load(String path) { + return load(Path.of(path), LoadOptions.defaults()); + } + + /** + * Load a {@code .prompty} file. + * + * @throws LoadException if the file cannot be read, the frontmatter is malformed, or a reference + * cannot be resolved + */ + public static com.microsoft.prompty.model.Prompty load(Path path, LoadOptions options) { + Path resolved; + try { + resolved = path.toRealPath(); + } catch (IOException e) { + throw LoadException.fileNotFound(path.toString(), e.toString()); + } + + String raw; + try { + raw = Files.readString(resolved, StandardCharsets.UTF_8); + } catch (IOException e) { + throw LoadException.fileNotFound(resolved.toString(), e.toString()); + } + + return buildAgent(raw.replace("\r\n", "\n"), resolved, options); + } + + /** + * Load from raw {@code .prompty} content, resolving {@code ${file:...}} relative to the given base + * path. + * + *

{@code basePath} names the document's notional location — its parent directory becomes the + * reference root — and need not exist as a file. + */ + public static com.microsoft.prompty.model.Prompty loadFromString(String raw, Path basePath) { + return loadFromString(raw, basePath, LoadOptions.defaults()); + } + + /** Load from raw {@code .prompty} content with explicit options. */ + public static com.microsoft.prompty.model.Prompty loadFromString( + String raw, Path basePath, LoadOptions options) { + return buildAgent(raw.replace("\r\n", "\n"), basePath, options); + } + + // ------------------------------------------------------------------------- + + private static com.microsoft.prompty.model.Prompty buildAgent( + String raw, Path filePath, LoadOptions options) { + Frontmatter.Split split = Frontmatter.split(raw); + Map data = split.frontmatter(); + + // Editors habitually append trailing newlines; strip them, but keep leading and internal + // whitespace, which is meaningful inside instructions. + String body = trimTrailingNewlines(split.body()); + if (!body.isEmpty()) { + data.put("instructions", body); + } + + rejectStringTemplate(data); + expandScalarShorthand(data, "inputs"); + expandScalarShorthand(data, "outputs"); + + data.put("kind", "prompt"); + + Path agentDir = filePath.getParent() == null ? Path.of(".") : filePath.getParent(); + References.resolveReferences(data, agentDir, options.allowedFileRoots()); + + LoadContext context = makeLoadContext(agentDir, options.allowedFileRoots()); + com.microsoft.prompty.model.Prompty agent = + com.microsoft.prompty.model.Prompty.load(data, context); + + if (agent.metadata == null) { + agent.metadata = new LinkedHashMap<>(); + } + agent.metadata.put(SOURCE_PATH_KEY, filePath.toString()); + return agent; + } + + /** + * A {@code LoadContext} whose {@code preProcess} resolves references as the model tree is walked. + * + *

The whole tree is resolved up front, so this mostly re-checks already-resolved values. It is + * still wired in because the model layer is the documented seam for reference expansion, and a + * value reachable only through the model's own recursion would otherwise be missed. + */ + private static LoadContext makeLoadContext(Path agentDir, List allowedFileRoots) { + return new LoadContext( + value -> { + if (value instanceof Map rawMap) { + @SuppressWarnings("unchecked") + Map map = (Map) rawMap; + for (Map.Entry entry : map.entrySet()) { + if (entry.getValue() instanceof String s) { + References.resolveSingleRef(s, agentDir, allowedFileRoots) + .ifPresent(entry::setValue); + } + } + } + return value; + }, + null); + } + + /** + * Reject {@code template: "jinja2"}. + * + *

Prompty v1 allowed a bare string; v2 requires {@code {format: {...}, parser: {...}}}. Failing + * loudly here rather than silently upgrading keeps the two shapes from quietly coexisting, and + * matches the shared {@code template_string_invalid} vector. + */ + private static void rejectStringTemplate(Map data) { + if (data.get("template") instanceof String) { + throw LoadException.invalidTemplate( + "template must be an object with 'format' and 'parser', not a bare string"); + } + } + + /** + * Expand dictionary-form scalar shorthand into full property objects (spec §4.3 step 6d). + * + *

{@code inputs: {topic: science}} becomes + * {@code inputs: {topic: {kind: "string", default: "science"}}}. Only the dictionary form is + * eligible: in list form a bare scalar has no name to attach it to. + * + *

This runs in the loader rather than the model layer because kind inference is a loading + * concern — the model layer's own shorthand handling reads a scalar as an {@code example}, which + * is the right reading for a hand-constructed model but the wrong one for authored frontmatter. + */ + private static void expandScalarShorthand(Map data, String key) { + if (!(data.get(key) instanceof Map rawMap)) { + return; + } + @SuppressWarnings("unchecked") + Map map = (Map) rawMap; + for (Map.Entry entry : map.entrySet()) { + Object value = entry.getValue(); + if (value instanceof Map) { + continue; + } + Map property = new LinkedHashMap<>(); + property.put("kind", inferKind(value)); + property.put("default", value); + entry.setValue(property); + } + } + + /** + * Infer a property kind from a scalar (spec §2.7). + * + *

{@code null} has no representable kind, so it falls back to {@code "string"} rather than + * inventing one. + */ + static String inferKind(Object value) { + if (value instanceof Boolean) { + return "boolean"; + } + if (value instanceof Integer || value instanceof Long || value instanceof Short + || value instanceof Byte || value instanceof java.math.BigInteger) { + return "integer"; + } + if (value instanceof Float || value instanceof Double + || value instanceof java.math.BigDecimal) { + return "float"; + } + if (value instanceof List) { + return "array"; + } + if (value instanceof Map) { + return "object"; + } + return "string"; + } + + private static String trimTrailingNewlines(String value) { + int end = value.length(); + while (end > 0) { + char c = value.charAt(end - 1); + if (c == '\n' || c == '\r') { + end--; + } else { + break; + } + } + return value.substring(0, end); + } + + /** Convenience accessor for the source path recorded at load time, if any. */ + public static String sourcePath(com.microsoft.prompty.model.Prompty agent) { + if (agent.metadata == null) { + return null; + } + Object value = agent.metadata.get(SOURCE_PATH_KEY); + return value instanceof String s ? s : null; + } + + /** Inputs declared by an agent, or an empty list when none are declared. */ + public static List inputsOf( + com.microsoft.prompty.model.Prompty agent) { + return agent.inputs == null ? new ArrayList<>() : agent.inputs; + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/Memory.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Memory.java new file mode 100644 index 000000000..965c05c7c --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Memory.java @@ -0,0 +1,289 @@ +package com.microsoft.prompty; + +import com.microsoft.prompty.model.MemoryCategory; +import com.microsoft.prompty.model.MemoryEntry; +import com.microsoft.prompty.model.MemoryStore; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; +import java.util.Objects; + +/** + * Agent memory: tiered recall, formatting, and mutation over the generated memory contract. + * + *

The data types are generated; what a runtime must not reinvent is what the data means. + * That is what lives here — ranking, injection, eviction — so a host is left owning only + * persistence, and every runtime agrees on which memory a query should surface. + * + *

Memory is tiered. A {@link MemoryCategory#CORE} memory is a persistent fact: injected into + * every system prompt, replaced on write when an identical tag set restates it, and boosted during + * recall. A {@link MemoryCategory#ARCHIVAL} memory is a compressed summary, surfaced only through + * recall and the first thing evicted when the store is full. A {@link MemoryCategory#INSIGHT} + * memory is a saved reflection, surfaced through recall. + * + *

Associating a memory with a session, a user, or a project is host convention expressed through + * the general {@code tags} field, not engine logic. + * + *

These operations mutate the store in place, because Java cannot add methods to the generated + * type the way the reference runtime can. + */ +public final class Memory { + + private Memory() {} + + /** A recalled memory paired with the score that ranked it. */ + public record Scored(MemoryEntry entry, double score, int keywordMatches) {} + + /** + * Host-owned persistence for a whole-store snapshot. + * + *

Deliberately whole-store rather than per-entry: a host that persists entries individually + * has to reason about ordering, which is exactly the deterministic logic this module owns. + */ + public interface Port { + MemoryStore load(); + + void save(MemoryStore store); + } + + /** Append a memory with no tier policy applied. */ + public static void add(MemoryStore store, MemoryEntry entry) { + store.entries.add(entry); + } + + /** + * Insert a memory applying tier policy. + * + *

A restated core fact replaces the old one rather than accumulating beside it, which is what + * keeps an always-injected tier from growing without bound. Identity for that purpose is the tag + * set, since that is what a host uses to scope a fact. + * + *

A {@code maxEntries} of {@code 0} means no cap. + */ + public static void remember(MemoryStore store, MemoryEntry entry, int maxEntries) { + if (entry.category == MemoryCategory.CORE) { + store.entries.removeIf( + existing -> existing.category == MemoryCategory.CORE && tagsEqual(existing.tags, entry.tags)); + } + store.entries.add(entry); + evictToCap(store, maxEntries); + } + + /** + * Evict until the store holds at most {@code maxEntries}, and report how many went. + * + *

Archival memories go first because they are summaries of things already said; only when + * there are none does the oldest memory of any tier go. A {@code maxEntries} of {@code 0} means + * no cap and evicts nothing. + */ + public static int evictToCap(MemoryStore store, int maxEntries) { + if (maxEntries <= 0) { + return 0; + } + int evicted = 0; + while (store.entries.size() > maxEntries) { + int victim = 0; + for (int i = 0; i < store.entries.size(); i++) { + if (store.entries.get(i).category == MemoryCategory.ARCHIVAL) { + victim = i; + break; + } + } + store.entries.remove(victim); + evicted++; + } + return evicted; + } + + /** Replace the memory at {@code index}. */ + public static void update(MemoryStore store, int index, MemoryEntry entry) { + requireIndex(store, index); + store.entries.set(index, entry); + } + + /** Replace only the content at {@code index}, preserving category, timestamp, and tags. */ + public static void updateContent(MemoryStore store, int index, String content) { + requireIndex(store, index); + store.entries.get(index).content = content; + } + + /** Remove and return the memory at {@code index}. */ + public static MemoryEntry remove(MemoryStore store, int index) { + requireIndex(store, index); + return store.entries.remove(index); + } + + /** Remove memories in {@code category}, or every memory when it is null, and report how many. */ + public static int clear(MemoryStore store, MemoryCategory category) { + int before = store.entries.size(); + if (category == null) { + store.entries.clear(); + } else { + store.entries.removeIf(entry -> entry.category == category); + } + return before - store.entries.size(); + } + + /** The core memories, in insertion order. */ + public static List coreMemories(MemoryStore store) { + List core = new ArrayList<>(); + for (MemoryEntry entry : store.entries) { + if (entry.category == MemoryCategory.CORE) { + core.add(entry); + } + } + return core; + } + + /** + * Recall the most relevant memories for {@code query}. + * + *

Ranking is lexical and dependency-free, so it produces the same order in every runtime and + * on every machine. A keyword found in the content is worth 2; found in the tags it is worth 3, + * because tags were chosen deliberately and content merely happens to contain the word. A core + * memory that matched at all gets a further 1, being the always-relevant tier. Ties fall back to + * insertion order. + * + *

A query with no keywords returns everything in insertion order at score 0; a {@code limit} + * of {@code 0} returns every match. A host wanting embedding-based recall keeps the vectors in + * its own storage and does that itself. + */ + public static List recall(MemoryStore store, String query, int limit) { + List tokens = queryTokens(query); + boolean hasQuery = !tokens.isEmpty(); + + List scored = new ArrayList<>(); + for (MemoryEntry entry : store.entries) { + double[] result = scoreEntry(entry, tokens); + int matches = (int) result[1]; + if (hasQuery && matches == 0) { + continue; + } + scored.add(new Scored(entry, result[0], matches)); + } + + // List.sort is contractually stable, which is what keeps equal scores in insertion order. + scored.sort(Comparator.comparingDouble(Scored::score).reversed()); + + return limit > 0 && scored.size() > limit ? List.copyOf(scored.subList(0, limit)) : scored; + } + + /** + * Format the core memories as a block for injection into a system prompt. + * + *

Returns an empty string when there are none, so a host can inject conditionally without + * first asking. Only core memories are injected; the other tiers reach the model through recall. + */ + public static String formatForSystemPrompt(MemoryStore store) { + List core = coreMemories(store); + if (core.isEmpty()) { + return ""; + } + StringBuilder out = new StringBuilder("## Memory\n"); + for (MemoryEntry entry : core) { + out.append("- ").append(entry.content).append('\n'); + } + return out.toString(); + } + + /** + * Format a recall result set for presentation, e.g. to show which memories informed a response. + * + *

Returns an empty string for an empty result set. + */ + public static String formatRecallResults(List results) { + if (results.isEmpty()) { + return ""; + } + StringBuilder out = new StringBuilder(); + for (int i = 0; i < results.size(); i++) { + Scored scored = results.get(i); + out.append(i + 1) + .append(". [") + .append(scored.entry().category.value) + .append("] ") + .append(scored.entry().content) + .append('\n'); + List tags = scored.entry().tags; + if (tags != null && !tags.isEmpty()) { + out.append(" tags: ").append(String.join(", ", tags)).append('\n'); + } + } + return out.toString(); + } + + private static void requireIndex(MemoryStore store, int index) { + if (index < 0 || index >= store.entries.size()) { + throw new IndexOutOfBoundsException( + "memory index " + index + " out of bounds (len " + store.entries.size() + ")"); + } + } + + /** Whether two tag lists carry the same tags, treating an absent list and an empty one as equal. */ + private static boolean tagsEqual(List left, List right) { + List a = left == null ? List.of() : left; + List b = right == null ? List.of() : right; + return Objects.equals(a, b); + } + + private static List queryTokens(String query) { + List tokens = new ArrayList<>(); + if (query == null) { + return tokens; + } + for (String raw : query.split("\\s+")) { + String token = trimNonAlphanumeric(raw).toLowerCase(Locale.ROOT); + if (!token.isEmpty() && !tokens.contains(token)) { + tokens.add(token); + } + } + return tokens; + } + + private static String trimNonAlphanumeric(String value) { + int start = 0; + int end = value.length(); + while (start < end && !Character.isLetterOrDigit(value.charAt(start))) { + start++; + } + while (end > start && !Character.isLetterOrDigit(value.charAt(end - 1))) { + end--; + } + return value.substring(start, end); + } + + /** Returns {@code [weightedScore, distinctMatches]}; both are zero for an empty query. */ + private static double[] scoreEntry(MemoryEntry entry, List tokens) { + if (tokens.isEmpty()) { + return new double[] {0.0, 0}; + } + String content = entry.content == null ? "" : entry.content.toLowerCase(Locale.ROOT); + List tags = new ArrayList<>(); + if (entry.tags != null) { + for (String tag : entry.tags) { + tags.add(tag == null ? "" : tag.toLowerCase(Locale.ROOT)); + } + } + + double weighted = 0.0; + int distinct = 0; + for (String token : tokens) { + boolean inContent = content.contains(token); + boolean inTags = tags.stream().anyMatch(tag -> tag.contains(token)); + if (inContent || inTags) { + distinct++; + } + if (inContent) { + weighted += 2.0; + } + if (inTags) { + weighted += 3.0; + } + } + if (weighted > 0.0 && entry.category == MemoryCategory.CORE) { + weighted += 1.0; + } + return new double[] {weighted, distinct}; + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/Messages.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Messages.java new file mode 100644 index 000000000..56477e506 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Messages.java @@ -0,0 +1,161 @@ +package com.microsoft.prompty; + +import com.microsoft.prompty.model.AudioPart; +import com.microsoft.prompty.model.ContentPart; +import com.microsoft.prompty.model.FilePart; +import com.microsoft.prompty.model.ImagePart; +import com.microsoft.prompty.model.Message; +import com.microsoft.prompty.model.Role; +import com.microsoft.prompty.model.SaveContext; +import com.microsoft.prompty.model.TextPart; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Behaviour for the generated {@link Message} and {@link ContentPart} types. + * + *

The TypeSpec declares these helpers as methods, but the Java emitter has no seam for a + * hand-written method body and emits stubs that throw. Rather than edit generated code — which + * regeneration would discard — the behaviour lives here as static functions over the generated + * types. The generated model stays canonical and single-sourced; only the verb form differs, so + * {@code Messages.text(msg)} stands in for what other runtimes spell {@code msg.text()}. + */ +public final class Messages { + + /** Metadata key carrying the identifier of the tool call a tool message answers. */ + public static final String TOOL_CALL_ID = "tool_call_id"; + + private Messages() {} + + /** + * Concatenated text of a message's text parts. + * + *

Non-text parts contribute nothing, so an image-only message yields an empty string rather + * than a placeholder. + */ + public static String text(Message message) { + if (message == null || message.parts == null) { + return ""; + } + StringBuilder builder = new StringBuilder(); + for (ContentPart part : message.parts) { + if (part instanceof TextPart textPart && textPart.value != null) { + builder.append(textPart.value); + } + } + return builder.toString(); + } + + /** Whether a message carries any part that is not plain text. */ + public static boolean hasRichContent(Message message) { + if (message == null || message.parts == null) { + return false; + } + for (ContentPart part : message.parts) { + if (!(part instanceof TextPart)) { + return true; + } + } + return false; + } + + /** + * The message content in provider wire form. + * + *

A message that is entirely text collapses to a single string, which is what every provider + * accepts and what keeps simple requests readable. Anything richer stays a list of typed parts. + */ + public static Object toTextContent(Message message) { + if (!hasRichContent(message)) { + return text(message); + } + List parts = new ArrayList<>(); + SaveContext context = new SaveContext(); + for (ContentPart part : message.parts) { + parts.add(part.save(context)); + } + return parts; + } + + /** A user message carrying a single text part. */ + public static Message user(String text) { + return withText(Role.USER, text); + } + + /** A system message carrying a single text part. */ + public static Message system(String text) { + return withText(Role.SYSTEM, text); + } + + /** An assistant message carrying a single text part. */ + public static Message assistant(String text) { + return withText(Role.ASSISTANT, text); + } + + /** A message with the given role carrying a single text part. */ + public static Message withText(Role role, String text) { + Message message = new Message(); + message.role = role; + message.parts = new ArrayList<>(List.of(textPart(text))); + message.metadata = new LinkedHashMap<>(); + return message; + } + + /** + * A tool-result message answering a specific tool call. + * + *

The call identifier travels in metadata rather than in the content, so the result text stays + * exactly what the tool returned. + */ + public static Message toolResult(String toolCallId, String content) { + Message message = withText(Role.TOOL, content); + message.metadata.put(TOOL_CALL_ID, toolCallId); + return message; + } + + /** A text content part. */ + public static TextPart textPart(String value) { + TextPart part = new TextPart(); + part.kind = "text"; + part.value = value == null ? "" : value; + return part; + } + + /** An image content part. */ + public static ImagePart imagePart(String source, String detail, String mediaType) { + ImagePart part = new ImagePart(); + part.kind = "image"; + part.source = source; + part.detail = detail; + part.mediaType = mediaType; + return part; + } + + /** A file content part. */ + public static FilePart filePart(String source, String mediaType) { + FilePart part = new FilePart(); + part.kind = "file"; + part.source = source; + part.mediaType = mediaType; + return part; + } + + /** An audio content part. */ + public static AudioPart audioPart(String source, String mediaType) { + AudioPart part = new AudioPart(); + part.kind = "audio"; + part.source = source; + part.mediaType = mediaType; + return part; + } + + /** Metadata of a message, never null. */ + public static Map metadata(Message message) { + if (message.metadata == null) { + message.metadata = new LinkedHashMap<>(); + } + return message.metadata; + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/Nonces.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Nonces.java new file mode 100644 index 000000000..b6f4c883b --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Nonces.java @@ -0,0 +1,89 @@ +package com.microsoft.prompty; + +import com.microsoft.prompty.model.Property; +import com.microsoft.prompty.model.Prompty; +import java.security.SecureRandom; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Nonce markers that stand in for rich inputs while a template renders. + * + *

A thread, image, file or audio input cannot survive being flattened into a template's output + * string. Instead the renderer receives an unguessable marker in its place, and the pipeline + * substitutes the real value back once parsing has produced structured messages. + * + *

The marker is unguessable on purpose. Because it is what the parser trusts when deciding which + * role boundaries are genuine, a predictable marker would let untrusted input forge one. + */ +public final class Nonces { + + /** Input kinds whose values are replaced by a marker during rendering. */ + public static final Set RICH_KINDS = Set.of("thread", "image", "file", "audio"); + + /** The one rich kind that is expanded during parsing rather than during wire conversion. */ + public static final String THREAD_KIND = "thread"; + + private static final SecureRandom RANDOM = new SecureRandom(); + private static final String HEX = "0123456789abcdef"; + + private Nonces() {} + + /** + * Inputs rewritten for rendering, paired with the markers that were substituted in. + * + * @param nonces every marker, by property name + * @param threadNonces the {@code thread} subset — the only kind expanded during parsing. Per spec + * §5.2, image, file and audio markers survive parsing and are resolved during wire conversion, + * so expanding them here would destroy them. + */ + public record Prepared( + Map inputs, Map nonces, Map threadNonces) {} + + /** Generate a marker of the form {@code __PROMPTY_THREAD_<8 hex>___}. */ + public static String generate(String name) { + return "__PROMPTY_THREAD_" + hex(8) + "_" + name + "__"; + } + + /** Generate a lowercase hex string of {@code length} digits. */ + public static String hex(int length) { + StringBuilder builder = new StringBuilder(length); + for (int i = 0; i < length; i++) { + builder.append(HEX.charAt(RANDOM.nextInt(16))); + } + return builder.toString(); + } + + /** + * Replace every rich-kind input with a freshly generated marker. + * + *

A marker is injected for each declared rich input whether or not the caller supplied a value, + * so the template always has something to render and the pipeline always has a marker to expand. + * + * @return the rewritten inputs and a property-name to marker mapping + */ + public static Prepared prepareRenderInputs(Prompty agent, Map inputs) { + Map modified = new LinkedHashMap<>(inputs == null ? Map.of() : inputs); + Map nonces = new LinkedHashMap<>(); + Map threadNonces = new LinkedHashMap<>(); + + List properties = agent == null ? null : agent.inputs; + if (properties != null) { + for (Property property : properties) { + if (property == null || property.name == null || !RICH_KINDS.contains(property.kind)) { + continue; + } + String nonce = generate(property.name); + modified.put(property.name, nonce); + nonces.put(property.name, nonce); + if (THREAD_KIND.equals(property.kind)) { + threadNonces.put(property.name, nonce); + } + } + } + + return new Prepared(modified, nonces, threadNonces); + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/Parser.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Parser.java new file mode 100644 index 000000000..c65202f76 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Parser.java @@ -0,0 +1,40 @@ +package com.microsoft.prompty; + +import com.microsoft.prompty.model.Message; +import com.microsoft.prompty.model.Prompty; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * Parses rendered text into a list of messages. + * + *

Registered under the {@code prompty.parsers} group, keyed by + * {@code agent.template.parser.kind}. + */ +public interface Parser { + + /** The result of a {@link #preRender} hook: a rewritten template plus context for {@link #parse}. */ + record PreRender(String template, Map context) {} + + /** + * Optional hook run before rendering. + * + *

A parser can rewrite the template and carry context forward to {@link #parse}. The chat + * parser uses this to stamp a per-render nonce onto every role marker, so that markers injected + * by a template variable can be told apart from markers the author wrote. + * + * @return the rewritten template and its context, or empty to render the template unchanged + */ + default Optional preRender(String template) { + return Optional.empty(); + } + + /** + * Parse rendered text into messages. + * + * @param context the context returned by {@link #preRender}, or null if there was none + * @throws InvokerException with {@link InvokerException.Kind#PARSE} if parsing fails + */ + List parse(Prompty agent, String rendered, Map context); +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/Pipeline.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Pipeline.java new file mode 100644 index 000000000..f1af8eb1d --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Pipeline.java @@ -0,0 +1,379 @@ +package com.microsoft.prompty; + +import com.microsoft.prompty.model.Message; +import com.microsoft.prompty.model.ModelOptions; +import com.microsoft.prompty.model.Property; +import com.microsoft.prompty.model.Prompty; +import com.microsoft.prompty.model.StreamChunk; +import com.microsoft.prompty.model.Template; +import com.microsoft.prompty.model.ToolCall; +import com.microsoft.prompty.engine.TurnEngineRequest; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * The prompt execution pipeline: render, parse, execute, process. + * + *

Each stage resolves its implementation from the {@link Registry} using a key taken from the + * agent itself — template format for the renderer, template parser for the parser, model provider + * for the executor and processor. Nothing here knows about any specific template engine or model + * vendor, which is what lets a {@code .prompty} file choose its own pipeline. + * + *

The stages compose into three entry points of increasing scope: {@link #prepare} turns an agent + * plus inputs into messages, {@link #run} turns messages into a result, and {@link #invoke} does + * both. + */ +public final class Pipeline { + + private static final String DEFAULT_FORMAT = "nunjucks"; + private static final String DEFAULT_PARSER = "prompty"; + private static final String DEFAULT_PROVIDER = "openai"; + + private Pipeline() {} + + // ---------------------------------------------------------------- configuration + + /** The template-format key an agent's renderer is looked up under. */ + public static String formatKind(Prompty agent) { + Template template = agent == null ? null : agent.template; + if (template != null && template.format != null && !isBlank(template.format.kind)) { + return template.format.kind; + } + return DEFAULT_FORMAT; + } + + /** The template-parser key an agent's parser is looked up under. */ + public static String parserKind(Prompty agent) { + Template template = agent == null ? null : agent.template; + if (template != null && template.parser != null && !isBlank(template.parser.kind)) { + return template.parser.kind; + } + return DEFAULT_PARSER; + } + + /** The provider key an agent's executor and processor are looked up under. */ + public static String provider(Prompty agent) { + if (agent != null && agent.model != null && !isBlank(agent.model.provider)) { + return agent.model.provider; + } + return DEFAULT_PROVIDER; + } + + /** + * Whether injection defence is active. + * + *

Defaults to on. A prompt is only as trustworthy as its weakest input, so opting out has to be + * a deliberate act recorded in the file. + */ + public static boolean isStrict(Prompty agent) { + Template template = agent == null ? null : agent.template; + if (template != null && template.format != null && template.format.strict != null) { + return template.format.strict; + } + return true; + } + + /** Whether the agent asks for a streamed response. */ + public static boolean isStreaming(Prompty agent) { + ModelOptions options = agent == null || agent.model == null ? null : agent.model.options; + if (options == null || options.additionalProperties == null) { + return false; + } + return Boolean.TRUE.equals(options.additionalProperties.get("stream")); + } + + // ---------------------------------------------------------------- inputs + + /** + * Fill in defaults and check that every required input is present. + * + * @throws InvokerException with {@link InvokerException.Kind#VALIDATION} if a required input is + * missing + */ + public static Map validateInputs(Prompty agent, Map inputs) { + Map result = new LinkedHashMap<>(inputs == null ? Map.of() : inputs); + List properties = agent == null ? null : agent.inputs; + if (properties == null) { + return result; + } + + for (Property property : properties) { + if (property == null || isBlank(property.name) || result.containsKey(property.name)) { + continue; + } + if (property.defaultValue != null) { + result.put(property.name, property.defaultValue); + } else if (Boolean.TRUE.equals(property.required)) { + throw InvokerException.validation("Missing required input: \"" + property.name + "\""); + } + } + return result; + } + + // ---------------------------------------------------------------- stages + + /** Render the agent's instructions with the given inputs. */ + public static String render(Prompty agent, Map inputs) { + return renderWithNonces(agent, agent == null ? "" : agent.instructions, inputs).rendered(); + } + + /** A rendered template together with the thread markers substituted into it. */ + private record Rendered(String rendered, Map threadNonces) {} + + private static Rendered renderWithNonces( + Prompty agent, String template, Map inputs) { + // Validation happens here rather than only in prepare() so that the public render() entry point + // also fills defaults and reports missing required inputs. Calling it twice is harmless. + Nonces.Prepared prepared = Nonces.prepareRenderInputs(agent, validateInputs(agent, inputs)); + String kind = formatKind(agent); + + try (Tracer.Span span = Tracer.start("Renderer")) { + span.emit("signature", "prompty.renderers." + kind + ".render"); + span.emit("inputs", prepared.inputs()); + try { + String rendered = + Registry.renderer(kind).render(agent, template == null ? "" : template, prepared.inputs()); + span.emit("result", rendered); + return new Rendered(rendered, prepared.threadNonces()); + } catch (RuntimeException e) { + span.error(e); + throw e; + } + } + } + + /** Parse rendered text into messages using the agent's registered parser. */ + public static List parse(Prompty agent, String rendered, Map context) { + String kind = parserKind(agent); + try (Tracer.Span span = Tracer.start("Parser")) { + span.emit("signature", "prompty.parsers." + kind + ".parse"); + span.emit("inputs", rendered); + try { + List messages = Registry.parser(kind).parse(agent, rendered, context); + span.emit("result", messages); + return messages; + } catch (RuntimeException e) { + span.error(e); + throw e; + } + } + } + + /** + * Render, parse, and splice in any thread history. + * + *

In strict mode the parser is first given a chance to stamp the template's role markers, so + * that markers appearing later — that is, ones that arrived through an input value — can be told + * apart from the ones the prompt author wrote. + */ + public static List prepare(Prompty agent, Map inputs) { + try (Tracer.Span span = Tracer.start("prepare")) { + span.emit("signature", "prompty.prepare"); + + Map validated = validateInputs(agent, inputs); + span.emit("inputs", validated); + + String instructions = agent == null || agent.instructions == null ? "" : agent.instructions; + String template = instructions; + Map parseContext = null; + + if (isStrict(agent)) { + Parser parser = Registry.parser(parserKind(agent)); + java.util.Optional preRender = parser.preRender(instructions); + if (preRender.isPresent()) { + template = preRender.get().template(); + parseContext = preRender.get().context(); + } + } + + Rendered rendered = renderWithNonces(agent, template, validated); + List messages = parse(agent, rendered.rendered(), parseContext); + List expanded = Threads.expand(messages, rendered.threadNonces(), validated); + + span.emit("result", expanded); + return expanded; + } + } + + /** Process a raw provider response using the agent's registered processor. */ + public static Object process(Prompty agent, Object response) { + String key = provider(agent); + try (Tracer.Span span = Tracer.start("Processor")) { + span.emit("signature", "prompty.processors." + key + ".process"); + try { + Object result = StructuredResult.wrapIfNeeded(agent, Registry.processor(key).process(agent, response)); + span.emit("result", result); + return result; + } catch (RuntimeException e) { + span.error(e); + throw e; + } + } + } + + /** + * Execute messages against the provider and process the response. + * + *

When the agent asks for streaming, the stream is consumed to completion and the accumulated + * text returned, so a streaming agent and a non-streaming one produce the same kind of value here. + * A provider that cannot stream falls back to a single call rather than failing. + */ + public static Object run(Prompty agent, List messages) { + String key = provider(agent); + + try (Tracer.Span span = Tracer.start("run")) { + span.emit("signature", "prompty.run"); + span.emit("inputs", messages); + + try { + Object result; + if (isStreaming(agent)) { + result = runStreaming(agent, messages, key); + } else { + Object response = Registry.executor(key).execute(agent, messages); + result = StructuredResult.unwrap(process(agent, response)); + } + span.emit("result", result); + return result; + } catch (RuntimeException e) { + span.error(e); + throw e; + } + } + } + + private static Object runStreaming(Prompty agent, List messages, String key) { + Iterator raw; + try { + raw = Registry.executor(key).executeStream(agent, messages); + } catch (InvokerException e) { + // The provider cannot open a stream, so nothing has been dispatched yet and a plain call is + // safe. Failures after this point must propagate: the request is already in flight, and + // retrying it would double-charge the caller and re-run any side effects. + Object response = Registry.executor(key).execute(agent, messages); + return StructuredResult.unwrap(process(agent, response)); + } + Iterator chunks = Registry.processor(key).processStream(agent, raw); + return Streams.consume(chunks, null).text(); + } + + /** Prepare and run in one call. */ + public static Object invoke(Prompty agent, Map inputs) { + try (Tracer.Span span = Tracer.start("invoke")) { + span.emit("signature", "prompty.invoke"); + span.emit("description", agent == null ? null : agent.description); + try { + Object result = run(agent, prepare(agent, inputs)); + span.emit("result", result); + return result; + } catch (RuntimeException e) { + span.error(e); + throw e; + } + } + } + + /** Load a {@code .prompty} file and invoke it. */ + public static Object invoke(Path path, Map inputs) { + return invoke(Loader.load(path), inputs); + } + + /** Load a {@code .prompty} file and invoke it. */ + public static Object invoke(String path, Map inputs) { + return invoke(Loader.load(path), inputs); + } + + // ---------------------------------------------------------------- agent turn + + /** + * Run a full agent turn: prepare, invoke the model, run any tools it asks for, and repeat until + * the model produces a final answer. + * + *

Where {@link #invoke} is a single round-trip, this drives the loop, and every extension — + * guardrails, steering, context trimming, durability, event listeners — attaches through {@link + * TurnOptions}. The loop itself is the canonical turn engine, so the same sequence of decisions + * is made here as in a replayed or resumed turn. + * + * @return the model's final output + */ + public static Object turn(Prompty agent, Map inputs, TurnOptions options) { + return LiveTurn.turn(agent, inputs, options); + } + + /** Run an agent turn with default options. */ + public static Object turn(Prompty agent, Map inputs) { + return LiveTurn.turn(agent, inputs, TurnOptions.defaults()); + } + + /** + * Run an agent turn against a caller-owned engine request. + * + *

This is the entry point for durability: the request carries the session and turn + * identifiers the journal is keyed by, and may describe a checkpoint to resume from rather than + * a fresh conversation. + */ + public static Object turn( + Prompty agent, TurnEngineRequest request, TurnOptions options) { + return LiveTurn.turn(agent, request, options); + } + + /** Load a {@code .prompty} file and run an agent turn. */ + public static Object turn(Path path, Map inputs, TurnOptions options) { + return LiveTurn.turn(Loader.load(path), inputs, options); + } + + /** Load a {@code .prompty} file and run an agent turn. */ + public static Object turn(String path, Map inputs, TurnOptions options) { + return LiveTurn.turn(Loader.load(path), inputs, options); + } + + // ---------------------------------------------------------------- result inspection + + /** + * Extract tool calls from a processed result. + * + *

Every provider's processor reports tool calls the same way — a list of {@code + * {id, name, arguments}} maps — so callers do not have to branch on provider. + * + * @return the calls, or an empty list if the result is not a tool-call round + */ + public static List toolCalls(Object result) { + List calls = new ArrayList<>(); + if (!(result instanceof List items) || items.isEmpty()) { + return calls; + } + for (Object item : items) { + if (!(item instanceof Map map)) { + return new ArrayList<>(); + } + Object id = map.get("id"); + Object name = map.get("name"); + if (!(id instanceof String) || !(name instanceof String)) { + return new ArrayList<>(); + } + ToolCall call = new ToolCall(); + call.id = (String) id; + call.name = (String) name; + Object arguments = map.get("arguments"); + call.arguments = + arguments instanceof String text + ? text + : arguments == null ? "" : com.microsoft.prompty.model.TypraJson.stringify(arguments); + calls.add(call); + } + return calls; + } + + /** The text of a processed result, or null if the result is not plain text. */ + public static String textOf(Object result) { + return result instanceof String text ? text : null; + } + + private static boolean isBlank(String value) { + return value == null || value.isEmpty(); + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/Processor.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Processor.java new file mode 100644 index 000000000..aa41918d6 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Processor.java @@ -0,0 +1,153 @@ +package com.microsoft.prompty; + +import com.microsoft.prompty.model.InvocationContextPortability; +import com.microsoft.prompty.model.InvocationContextState; +import com.microsoft.prompty.model.Message; +import com.microsoft.prompty.model.ModelInvocationRequest; +import com.microsoft.prompty.model.ModelInvocationResponse; +import com.microsoft.prompty.model.ModelToolRequest; +import com.microsoft.prompty.model.Prompty; +import com.microsoft.prompty.model.StreamChunk; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * Turns a provider's raw response into a usable result. + * + *

Registered under the {@code prompty.processors} group, keyed by {@code agent.model.provider}. + */ +public interface Processor { + + /** + * Extract the usable result from a raw provider response. + * + *

What "usable" means is provider- and API-shaped: assistant text for a chat completion, an + * embedding vector for an embedding call, structured data when the agent declares outputs. + * + * @throws InvokerException with {@link InvokerException.Kind#PROCESS} if the response cannot be + * interpreted + */ + Object process(Prompty agent, Object response); + + /** + * Convert a raw streaming response into typed chunks. + * + * @throws InvokerException with {@link InvokerException.Kind#PROCESS} if streaming is unsupported + */ + default Iterator processStream(Prompty agent, Iterator response) { + throw InvokerException.process("Streaming not supported by this processor"); + } + + /** + * Map a raw response onto the generated live-invocation contract. + * + *

The default runs {@link #process} and then recognises the established + * {@code {id, name, arguments}} tool-call shape, so an existing processor participates in the turn + * engine without changes. It reports the resulting context as portable, because a processor that + * has not opted in cannot be holding provider-side state. A provider with native continuation + * support should override this and return a typed delegated state reference instead. + */ + default ModelInvocationResponse processWithContext( + Prompty agent, Object response, ModelInvocationRequest request) { + Object output = process(agent, response); + List toolRequests = legacyToolRequests(output); + ModelInvocationResponse result = new ModelInvocationResponse(); + result.output = toolRequests.isEmpty() ? output : null; + result.assistantMessages = legacyAssistantMessages(output, toolRequests); + result.toolRequests = toolRequests; + result.nextContextState = portableState(); + return result; + } + + /** + * Map a raw response without running {@link #process}. + * + *

Preserves raw execution semantics — the caller asked for the provider's own words — while + * still producing the contract the turn engine consumes. + */ + default ModelInvocationResponse processRawWithContext( + Prompty agent, Object response, ModelInvocationRequest request) { + ModelInvocationResponse result = new ModelInvocationResponse(); + result.output = response; + result.assistantMessages = legacyAssistantMessages(response, List.of()); + result.toolRequests = new ArrayList<>(); + result.nextContextState = portableState(); + return result; + } + + private static InvocationContextState portableState() { + InvocationContextState state = new InvocationContextState(); + state.portability = InvocationContextPortability.PORTABLE; + state.delegatedState = new ArrayList<>(); + return state; + } + + /** + * Recognise tool calls in a processed output that predates the typed contract. + * + *

Accepts a list of {@code {id, name, arguments}} maps, which is what every processor written + * against the pre-contract shape produces. + */ + private static List legacyToolRequests(Object output) { + List requests = new ArrayList<>(); + if (!(output instanceof List list)) { + return requests; + } + for (Object item : list) { + if (!(item instanceof Map map)) { + return new ArrayList<>(); + } + Object id = map.get("id"); + Object name = map.get("name"); + Object arguments = map.get("arguments"); + if (!(id instanceof String) || !(name instanceof String) || arguments == null) { + return new ArrayList<>(); + } + ModelToolRequest request = new ModelToolRequest(); + request.id = (String) id; + request.name = (String) name; + request.arguments = arguments; + requests.add(request); + } + return requests; + } + + /** + * The assistant message implied by a processed output. + * + *

Always exactly one message, because a turn the model took is a turn that has to appear in the + * conversation the next request is built from. A tool-call round carries empty text and records + * the calls under a {@code tool_calls} metadata key; anything that is not already text is + * stringified rather than dropped. + */ + private static List legacyAssistantMessages( + Object output, List toolRequests) { + String content; + if (output instanceof String text) { + content = text; + } else if (!toolRequests.isEmpty()) { + content = ""; + } else { + content = output == null ? "null" : String.valueOf(output); + } + + Message assistant = Messages.assistant(content); + if (!toolRequests.isEmpty()) { + List toolCalls = new ArrayList<>(); + for (ModelToolRequest request : toolRequests) { + Object arguments = request.arguments; + String encoded = + arguments instanceof String text ? text : arguments == null ? "{}" : String.valueOf(arguments); + toolCalls.add( + Map.of( + "id", request.id, + "type", "function", + "function", Map.of("name", request.name, "arguments", encoded))); + } + Messages.metadata(assistant).put("tool_calls", toolCalls); + } + return new ArrayList<>(List.of(assistant)); + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/PromptyExtension.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/PromptyExtension.java new file mode 100644 index 000000000..8b7876912 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/PromptyExtension.java @@ -0,0 +1,34 @@ +package com.microsoft.prompty; + +/** + * Service-provider interface for contributing invokers to the {@link Registry}. + * + *

An implementation is discovered through {@link java.util.ServiceLoader}, so a provider module + * becomes available simply by being on the classpath. Declare it in + * {@code META-INF/services/com.microsoft.prompty.PromptyExtension}, or as a {@code provides} clause + * in a module descriptor. + * + *

One extension may register any number of invokers under any number of keys, which is what lets + * a single provider module serve several closely related back ends. + */ +public interface PromptyExtension { + + /** Contribute this extension's invokers. Called once, the first time the registry is used. */ + void register(Registrar registrar); + + /** The subset of the registry an extension is allowed to write to. */ + interface Registrar { + + /** Register a renderer under a template-format key such as {@code "nunjucks"}. */ + Registrar renderer(String key, Renderer renderer); + + /** Register a parser under a template-parser key such as {@code "prompty"}. */ + Registrar parser(String key, Parser parser); + + /** Register an executor under a provider key such as {@code "openai"}. */ + Registrar executor(String key, Executor executor); + + /** Register a processor under a provider key such as {@code "openai"}. */ + Registrar processor(String key, Processor processor); + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/References.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/References.java new file mode 100644 index 000000000..9a8679fe0 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/References.java @@ -0,0 +1,222 @@ +package com.microsoft.prompty; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import org.yaml.snakeyaml.error.YAMLException; + +/** + * Resolution of {@code ${protocol:value}} references in loaded frontmatter. + * + *

Two protocols are recognised: + * + *

    + *
  • {@code ${env:VAR}} and {@code ${env:VAR:default}} — an environment variable, with an + * optional literal default used when the variable is unset. The default is everything after + * the first colon, so it may itself contain colons. + *
  • {@code ${file:path}} — the contents of a file resolved relative to the prompt's directory. + * {@code .json}, {@code .yaml} and {@code .yml} are parsed into structured values; anything + * else is inlined as raw text. + *
+ * + *

Any other protocol is left untouched, so unrelated {@code ${...}} syntax in a template survives + * loading. + * + *

File sandboxing

+ * + *

A {@code ${file:...}} target must resolve inside the prompt's own directory or one of the + * explicitly allowed roots from {@link LoadOptions}. This is enforced after canonicalisation, so + * {@code ../} traversal and symlinks cannot escape. + * + *

A deliberate quirk

+ * + *

{@link #resolveReferences} descends into nested maps and lists, but it only resolves strings + * that are map values — a bare string sitting directly in a list is left alone. That + * matches the Rust runtime exactly, and the runtimes are kept identical here rather than + * independently "fixed", because the wire behaviour is observable. + */ +public final class References { + + private References() {} + + /** + * Recursively resolve references in place, throwing on an unresolvable one. + * + * @param value the frontmatter tree; maps and lists are walked, other values are left alone + * @param agentDir the prompt file's directory, always an allowed {@code ${file:}} root + * @param allowedFileRoots additional directories {@code ${file:}} may read from + */ + public static void resolveReferences(Object value, Path agentDir, List allowedFileRoots) { + if (value instanceof Map rawMap) { + @SuppressWarnings("unchecked") + Map map = (Map) rawMap; + for (String key : new ArrayList<>(map.keySet())) { + Object item = map.get(key); + if (item instanceof String s) { + Optional resolved = tryResolveString(s, key, agentDir, allowedFileRoots, true); + if (resolved.isPresent()) { + map.put(key, resolved.get()); + } + } else if (item != null) { + resolveReferences(item, agentDir, allowedFileRoots); + } + } + } else if (value instanceof List rawList) { + @SuppressWarnings("unchecked") + List list = (List) rawList; + for (int i = 0; i < list.size(); i++) { + Object item = list.get(i); + if (item instanceof String s) { + // Spec §4.2 resolves every string value, not only the ones that happen to sit directly + // under a key. A list of file references is a perfectly ordinary thing to write. + Optional resolved = + tryResolveString(s, "[" + i + "]", agentDir, allowedFileRoots, true); + if (resolved.isPresent()) { + list.set(i, resolved.get()); + } + } else if (item != null) { + resolveReferences(item, agentDir, allowedFileRoots); + } + } + } + } + + /** + * Resolve a single string reference, swallowing failures. + * + *

Used as the model layer's {@code preProcess} hook, where the tree has normally already been + * resolved by {@link #resolveReferences} and a second, throwing pass would be redundant. + * + * @return the resolved value, or empty if the string was not a resolvable reference + */ + public static Optional resolveSingleRef( + String value, Path agentDir, List allowedFileRoots) { + return tryResolveString(value, "", agentDir, allowedFileRoots, false); + } + + private static Optional tryResolveString( + String value, String key, Path agentDir, List allowedFileRoots, boolean throwOnError) { + if (!value.startsWith("${") || !value.endsWith("}")) { + return Optional.empty(); + } + + String inner = value.substring(2, value.length() - 1); + int colon = inner.indexOf(':'); + if (colon < 0) { + return Optional.empty(); + } + + String protocol = inner.substring(0, colon).toLowerCase(Locale.ROOT); + String rest = inner.substring(colon + 1); + + try { + return switch (protocol) { + case "env" -> resolveEnv(rest, key); + case "file" -> resolveFile(rest, agentDir, allowedFileRoots, key); + // Unknown protocol — leave the string as authored. + default -> Optional.empty(); + }; + } catch (LoadException e) { + if (throwOnError) { + throw e; + } + return Optional.empty(); + } + } + + private static Optional resolveEnv(String spec, String key) { + int colon = spec.indexOf(':'); + String varName = colon < 0 ? spec : spec.substring(0, colon); + String defaultValue = colon < 0 ? null : spec.substring(colon + 1); + + Optional actual = Environment.lookup(varName); + if (actual.isPresent()) { + return Optional.of(actual.get()); + } + if (defaultValue != null) { + return Optional.of(defaultValue); + } + throw LoadException.envVarNotSet(varName, key); + } + + private static Optional resolveFile( + String relativePath, Path agentDir, List allowedFileRoots, String key) { + Path requested = Path.of(relativePath); + Path full = requested.isAbsolute() ? requested : agentDir.resolve(requested); + + Path canonical = canonicalize(full); + + List roots = new ArrayList<>(allowedFileRoots.size() + 1); + roots.add(canonicalize(agentDir)); + for (Path root : allowedFileRoots) { + roots.add(canonicalize(root)); + } + + boolean allowed = roots.stream().anyMatch(canonical::startsWith); + if (!allowed) { + throw LoadException.fileReference( + canonical.toString(), + "File reference '" + + relativePath + + "' for key '" + + key + + "' resolves outside allowed roots"); + } + + String content; + try { + content = Files.readString(canonical, StandardCharsets.UTF_8); + } catch (IOException e) { + throw LoadException.fileReference(canonical.toString(), e.toString()); + } + + String extension = extensionOf(canonical); + return switch (extension) { + case "json" -> Optional.of(parseStructured(content, canonical, "Invalid JSON")); + case "yaml", "yml" -> Optional.of(parseStructured(content, canonical, "Invalid YAML")); + default -> Optional.of(content); + }; + } + + /** + * Parse JSON or YAML content. + * + *

YAML is a superset of JSON, so SnakeYAML covers both; only the failure wording differs, which + * keeps diagnostics aligned with the other runtimes. + */ + private static Object parseStructured(String content, Path path, String errorPrefix) { + Object parsed; + try { + parsed = Frontmatter.newYaml().load(content); + } catch (YAMLException e) { + throw LoadException.fileReference(path.toString(), errorPrefix + ": " + e.getMessage()); + } + if (parsed instanceof Map map) { + return Frontmatter.stringKeyed(map); + } + if (parsed instanceof List list) { + return Frontmatter.stringKeyedList(list); + } + return parsed; + } + + private static Path canonicalize(Path path) { + try { + return path.toRealPath(); + } catch (IOException e) { + throw LoadException.fileReference(path.toString(), e.toString()); + } + } + + private static String extensionOf(Path path) { + String name = path.getFileName().toString(); + int dot = name.lastIndexOf('.'); + return dot < 0 ? "" : name.substring(dot + 1).toLowerCase(Locale.ROOT); + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/Registry.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Registry.java new file mode 100644 index 000000000..2b368f4f0 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Registry.java @@ -0,0 +1,215 @@ +package com.microsoft.prompty; + +import com.microsoft.prompty.parsers.PromptyChatParser; +import com.microsoft.prompty.renderers.JinjaRenderer; +import com.microsoft.prompty.renderers.MustacheRenderer; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.concurrent.ConcurrentHashMap; + +/** + * The process-wide lookup of pipeline invokers, keyed by group and name. + * + *

Four independent groups are held: renderers keyed by template format, parsers keyed by template + * parser, and executors and processors keyed by model provider. The keys come straight from a + * loaded agent, which is what lets a {@code .prompty} file select its own pipeline without the + * caller wiring anything up. + * + *

The registry populates itself on first use: the built-in renderers and parser are registered + * directly, then every {@link PromptyExtension} on the classpath is given a chance to contribute. + * Explicit {@code register*} calls always win over discovery, so a test or an embedding application + * can substitute a fake without removing the real provider from the classpath. + */ +public final class Registry { + + private static final Map RENDERERS = new ConcurrentHashMap<>(); + private static final Map PARSERS = new ConcurrentHashMap<>(); + private static final Map EXECUTORS = new ConcurrentHashMap<>(); + private static final Map PROCESSORS = new ConcurrentHashMap<>(); + + private static final Object BOOTSTRAP_LOCK = new Object(); + private static volatile boolean bootstrapped = false; + + private Registry() {} + + // ---------------------------------------------------------------- registration + + /** Register a renderer under a template-format key. */ + public static void registerRenderer(String key, Renderer renderer) { + bootstrap(); + RENDERERS.put(key, renderer); + } + + /** Register a parser under a template-parser key. */ + public static void registerParser(String key, Parser parser) { + bootstrap(); + PARSERS.put(key, parser); + } + + /** Register an executor under a model-provider key. */ + public static void registerExecutor(String key, Executor executor) { + bootstrap(); + EXECUTORS.put(key, executor); + } + + /** Register a processor under a model-provider key. */ + public static void registerProcessor(String key, Processor processor) { + bootstrap(); + PROCESSORS.put(key, processor); + } + + // ---------------------------------------------------------------- lookup + + public static boolean hasRenderer(String key) { + bootstrap(); + return RENDERERS.containsKey(key); + } + + public static boolean hasParser(String key) { + bootstrap(); + return PARSERS.containsKey(key); + } + + public static boolean hasExecutor(String key) { + bootstrap(); + return EXECUTORS.containsKey(key); + } + + public static boolean hasProcessor(String key) { + bootstrap(); + return PROCESSORS.containsKey(key); + } + + /** + * The renderer registered under {@code key}. + * + * @throws InvokerException with {@link InvokerException.Kind#NOT_FOUND} if none is registered + */ + public static Renderer renderer(String key) { + bootstrap(); + Renderer renderer = RENDERERS.get(key); + if (renderer == null) { + throw InvokerException.notFound("renderer", key); + } + return renderer; + } + + /** + * The parser registered under {@code key}. + * + * @throws InvokerException with {@link InvokerException.Kind#NOT_FOUND} if none is registered + */ + public static Parser parser(String key) { + bootstrap(); + Parser parser = PARSERS.get(key); + if (parser == null) { + throw InvokerException.notFound("parser", key); + } + return parser; + } + + /** + * The executor registered under {@code key}. + * + * @throws InvokerException with {@link InvokerException.Kind#NOT_FOUND} if none is registered + */ + public static Executor executor(String key) { + bootstrap(); + Executor executor = EXECUTORS.get(key); + if (executor == null) { + throw InvokerException.notFound("executor", key); + } + return executor; + } + + /** + * The processor registered under {@code key}. + * + * @throws InvokerException with {@link InvokerException.Kind#NOT_FOUND} if none is registered + */ + public static Processor processor(String key) { + bootstrap(); + Processor processor = PROCESSORS.get(key); + if (processor == null) { + throw InvokerException.notFound("processor", key); + } + return processor; + } + + // ---------------------------------------------------------------- lifecycle + + /** + * Drop every registration and force rediscovery on next use. + * + *

Intended for tests that install fakes and must not leak them into later tests. + */ + public static void clearCache() { + synchronized (BOOTSTRAP_LOCK) { + RENDERERS.clear(); + PARSERS.clear(); + EXECUTORS.clear(); + PROCESSORS.clear(); + bootstrapped = false; + } + } + + /** Populate the registry if it has not been populated since the last {@link #clearCache()}. */ + public static void bootstrap() { + if (bootstrapped) { + return; + } + synchronized (BOOTSTRAP_LOCK) { + if (bootstrapped) { + return; + } + // Set first: a discovered extension may itself call into the registry, and re-entering + // bootstrap would deadlock on this lock's non-reentrant intent or recurse indefinitely. + bootstrapped = true; + registerBuiltins(); + loadExtensions(); + } + } + + private static void registerBuiltins() { + Renderer jinja = new JinjaRenderer(); + // Nunjucks is the canonical format name in the spec; jinja2 is the long-standing alias used by + // existing .prompty files. Both resolve to the same engine. + RENDERERS.put("nunjucks", jinja); + RENDERERS.put("jinja2", jinja); + RENDERERS.put("mustache", new MustacheRenderer()); + PARSERS.put("prompty", new PromptyChatParser()); + } + + private static void loadExtensions() { + Registrar registrar = new Registrar(); + for (PromptyExtension extension : ServiceLoader.load(PromptyExtension.class)) { + extension.register(registrar); + } + } + + private static final class Registrar implements PromptyExtension.Registrar { + @Override + public PromptyExtension.Registrar renderer(String key, Renderer renderer) { + RENDERERS.put(key, renderer); + return this; + } + + @Override + public PromptyExtension.Registrar parser(String key, Parser parser) { + PARSERS.put(key, parser); + return this; + } + + @Override + public PromptyExtension.Registrar executor(String key, Executor executor) { + EXECUTORS.put(key, executor); + return this; + } + + @Override + public PromptyExtension.Registrar processor(String key, Processor processor) { + PROCESSORS.put(key, processor); + return this; + } + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/Renderer.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Renderer.java new file mode 100644 index 000000000..98daf5fc7 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Renderer.java @@ -0,0 +1,24 @@ +package com.microsoft.prompty; + +import com.microsoft.prompty.model.Prompty; +import java.util.Map; + +/** + * Renders a template string against a set of inputs. + * + *

Registered under the {@code prompty.renderers} group, keyed by + * {@code agent.template.format.kind}. + */ +public interface Renderer { + + /** + * Render {@code template} with {@code inputs}. + * + * @param agent the agent being rendered, for renderers that consult its declarations + * @param template the template text, normally the agent's instructions + * @param inputs input values by name + * @return the rendered text + * @throws InvokerException with {@link InvokerException.Kind#RENDER} if rendering fails + */ + String render(Prompty agent, String template, Map inputs); +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/Steering.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Steering.java new file mode 100644 index 000000000..d5614e6ae --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Steering.java @@ -0,0 +1,54 @@ +package com.microsoft.prompty; + +import com.microsoft.prompty.model.Message; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * A queue of mid-turn instructions to fold into the conversation. + * + *

Steering exists so a caller can influence a turn that is already running — a user typing a + * correction while the agent works through tool calls, for instance. Sends are therefore safe from + * any thread, and the turn drains the queue once at the start of each iteration so injected text + * lands on an iteration boundary rather than partway through a model call. + * + *

Draining is atomic: the queue is emptied and its contents returned in one step, so a message + * can never be delivered twice or lost to a concurrent send. + */ +public final class Steering { + + private final ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue<>(); + + /** Queue an instruction to inject before the next model call. */ + public void send(String message) { + if (message != null) { + queue.add(message); + } + } + + /** Atomically remove every queued instruction, as user messages. */ + public List drain() { + List messages = new ArrayList<>(); + String next; + while ((next = queue.poll()) != null) { + messages.add(Messages.user(next)); + } + return messages; + } + + /** Whether anything is waiting to be injected. */ + public boolean hasPending() { + return !queue.isEmpty(); + } + + /** Whether nothing is waiting to be injected. */ + public boolean isEmpty() { + return queue.isEmpty(); + } + + /** How many instructions are waiting. */ + public int size() { + return queue.size(); + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/StreamFailure.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/StreamFailure.java new file mode 100644 index 000000000..eae7cc3f4 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/StreamFailure.java @@ -0,0 +1,50 @@ +package com.microsoft.prompty; + +import com.microsoft.prompty.model.ErrorChunk; + +/** + * An error chunk that also says whether the request's outcome is known. + * + *

A stream can fail in two materially different ways. A refusal or an API error is + * determinate: the provider decided, nothing was applied, and retrying is safe. A transport + * failure mid-stream is indeterminate: the request may have completed server-side, so a + * blind retry can duplicate a tool call or a charge. + * + *

The generated {@code ErrorChunk} carries only a message, because the distinction is a runtime + * concern rather than part of the portable schema. Extending it keeps that nuance available to + * callers who need it while remaining an ordinary {@code ErrorChunk} to everyone else — including + * serialization, which is inherited unchanged. + */ +public class StreamFailure extends ErrorChunk { + + /** + * Whether the request may have taken effect despite the failure. + * + *

When true, the caller must reconcile before retrying rather than simply reissuing. + */ + public boolean outcomeUnknown; + + public StreamFailure() {} + + /** A failure whose outcome is known: nothing was applied. */ + public static StreamFailure determinate(String message) { + return create(message, false); + } + + /** A failure whose outcome is unknown: the request may have completed server-side. */ + public static StreamFailure indeterminate(String message) { + return create(message, true); + } + + private static StreamFailure create(String message, boolean outcomeUnknown) { + StreamFailure failure = new StreamFailure(); + failure.message = message == null ? "" : message; + failure.outcomeUnknown = outcomeUnknown; + return failure; + } + + /** Whether a chunk reports a failure whose outcome is unknown. */ + public static boolean isIndeterminate(Object chunk) { + return chunk instanceof StreamFailure failure && failure.outcomeUnknown; + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/Streams.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Streams.java new file mode 100644 index 000000000..94bbee075 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Streams.java @@ -0,0 +1,270 @@ +package com.microsoft.prompty; + +import com.microsoft.prompty.model.ErrorChunk; +import com.microsoft.prompty.model.StreamChunk; +import com.microsoft.prompty.model.TextChunk; +import com.microsoft.prompty.model.ToolCall; +import com.microsoft.prompty.model.ToolChunk; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; + +/** Helpers for the iterator-shaped streams that executors and processors exchange. */ +public final class Streams { + + private Streams() {} + + /** Text and tool calls drained from a processed chunk stream. */ + public record Consumed(List toolCalls, String text) {} + + /** + * Release a stream's underlying resources, if it has any. + * + *

Provider streams sit on a live connection. Java has no destructor to release it, so a stream + * that is abandoned — cancelled, terminated by a refusal, or simply stopped early — has to be + * closed explicitly or the connection stays checked out until the garbage collector happens to + * notice. Streams with nothing to release are unaffected, so callers can close unconditionally. + */ + public static void close(Object stream) { + if (stream instanceof AutoCloseable closeable) { + try { + closeable.close(); + } catch (Exception e) { + // Releasing a stream that is already finished with is best-effort: there is no caller left + // to act on the failure, and raising it would mask whatever ended the stream in the first + // place. + } + } + } + + /** + * Wrap a stream so that every advance first observes a cancellation token. + * + *

A cancelled stream simply reports exhaustion rather than throwing. Cancellation is a normal + * outcome for a stream — the caller asked for it — and the partial content already yielded stays + * valid. The source is released on cancellation, since nothing will read from it again. + */ + public static Iterator cancellable(Iterator source, CancellationToken cancellation) { + return new ClosingIterator(source) { + @Override + public boolean hasNext() { + if (cancellation.isCancelled()) { + close(); + return false; + } + return source.hasNext(); + } + + @Override + public T next() { + if (cancellation.isCancelled()) { + close(); + throw new NoSuchElementException("stream cancelled"); + } + return source.next(); + } + }; + } + + /** + * An iterator that forwards closure to the stream it wraps. + * + *

Streams are composed in layers — transport, cancellation, tracing, processing — and the + * connection to release sits at the bottom. Closing has to travel down the chain, or only the + * outermost wrapper hears about it and the connection stays open. + */ + abstract static class ClosingIterator implements Iterator, java.io.Closeable { + private final Iterator source; + private boolean closed; + + ClosingIterator(Iterator source) { + this.source = source; + } + + @Override + public void close() { + if (!closed) { + closed = true; + Streams.close(source); + } + } + } + + /** + * Wrap a stream so every element passes through {@code observer} on its way out. + * + *

Used to accumulate raw chunks for tracing without buffering the whole stream up front. + */ + public static Iterator peeking(Iterator source, java.util.function.Consumer observer) { + return new ClosingIterator(source) { + @Override + public boolean hasNext() { + return source.hasNext(); + } + + @Override + public T next() { + T item = source.next(); + observer.accept(item); + return item; + } + }; + } + + /** + * Drain a processed chunk stream into accumulated text and completed tool calls. + * + *

Thinking and usage chunks are informational and contribute nothing to the result. An error + * chunk terminates consumption immediately: the stream is broken from that point on, and + * continuing would append content the provider never committed to. + * + * @param onToken invoked for each text token as it arrives, or null + */ + public static Consumed consume( + Iterator stream, java.util.function.Consumer onToken) { + List toolCalls = new ArrayList<>(); + StringBuilder text = new StringBuilder(); + + while (stream.hasNext()) { + StreamChunk chunk = stream.next(); + if (chunk instanceof TextChunk textChunk) { + String value = textChunk.value == null ? "" : textChunk.value; + if (onToken != null) { + onToken.accept(value); + } + text.append(value); + } else if (chunk instanceof ToolChunk toolChunk) { + if (toolChunk.toolCall != null) { + toolCalls.add(toolChunk.toolCall); + } + } else if (chunk instanceof ErrorChunk) { + break; + } + } + + return new Consumed(toolCalls, text.toString()); + } + + /** + * Merge OpenAI-style incremental {@code tool_calls} deltas into completed tool calls. + * + *

Providers stream a tool call in pieces: the identifier and name arrive once, then the + * arguments accumulate across many chunks. Deltas are keyed by their {@code index}, and the + * resulting calls are returned in index order so the sequence matches what the model requested. + */ + public static List mergeToolCallDeltas(List chunks) { + Map byIndex = new java.util.TreeMap<>(); + Map arguments = new LinkedHashMap<>(); + + for (Object chunk : chunks) { + List deltas = toolCallDeltas(chunk); + for (Object raw : deltas) { + if (!(raw instanceof Map delta)) { + continue; + } + int index = intValue(delta.get("index")); + ToolCall call = byIndex.computeIfAbsent(index, key -> new ToolCall()); + StringBuilder args = arguments.computeIfAbsent(index, key -> new StringBuilder()); + + Object id = delta.get("id"); + if (id instanceof String s && !s.isEmpty()) { + call.id = s; + } + if (delta.get("function") instanceof Map function) { + if (function.get("name") instanceof String name && !name.isEmpty()) { + call.name = name; + } + if (function.get("arguments") instanceof String argument) { + args.append(argument); + } + } + } + } + + List result = new ArrayList<>(byIndex.size()); + for (Map.Entry entry : byIndex.entrySet()) { + ToolCall call = entry.getValue(); + StringBuilder args = arguments.get(entry.getKey()); + call.arguments = args == null ? "" : args.toString(); + result.add(call); + } + return result; + } + + /** Concatenate {@code choices[0].delta.content} across raw OpenAI-style chunks. */ + public static String collectDeltaText(List chunks) { + StringBuilder text = new StringBuilder(); + for (Object chunk : chunks) { + Object content = pointer(chunk, "choices", 0, "delta", "content"); + if (content instanceof String s) { + text.append(s); + } + } + return text.toString(); + } + + @SuppressWarnings("unchecked") + private static List toolCallDeltas(Object chunk) { + Object value = pointer(chunk, "choices", 0, "delta", "tool_calls"); + return value instanceof List list ? (List) list : List.of(); + } + + /** + * Walk a JSON-shaped tree by a mixed path of map keys and list indices. + * + * @return the value at that path, or null if any step is absent or the wrong shape + */ + public static Object pointer(Object root, Object... path) { + Object current = root; + for (Object step : path) { + if (current == null) { + return null; + } + if (step instanceof Integer index) { + if (!(current instanceof List list) || index < 0 || index >= list.size()) { + return null; + } + current = list.get(index); + } else { + if (!(current instanceof Map map)) { + return null; + } + current = map.get(step); + } + } + return current; + } + + /** + * Copy a JSON-shaped tree so the result shares no mutable node with the original. + * + *

Response fragments get stored into conversation metadata that outlives the response they came + * from. Copying only the outer container would leave the nested maps aliased, so a caller that + * reuses or mutates its response could still rewrite a message already sent. Scalars and strings + * are immutable and are shared as-is. + */ + public static Object deepCopy(Object value) { + if (value instanceof Map map) { + Map copy = new java.util.LinkedHashMap<>(); + for (Map.Entry entry : map.entrySet()) { + copy.put(String.valueOf(entry.getKey()), deepCopy(entry.getValue())); + } + return copy; + } + if (value instanceof List list) { + List copy = new java.util.ArrayList<>(list.size()); + for (Object element : list) { + copy.add(deepCopy(element)); + } + return copy; + } + return value; + } + + private static int intValue(Object value) { + return value instanceof Number number ? number.intValue() : 0; + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/StructuredResult.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/StructuredResult.java new file mode 100644 index 000000000..a0929c3fb --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/StructuredResult.java @@ -0,0 +1,113 @@ +package com.microsoft.prompty; + +import com.microsoft.prompty.model.TypraJson; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Transport for structured (schema-shaped) results. + * + *

When an agent declares outputs, its processed result is data rather than prose. Wrapping it + * preserves the exact JSON the model produced alongside the parsed form, so a later {@link + * #cast(Object, Class)} can deserialize losslessly instead of re-serializing an already-lossy + * intermediate. Callers who just want the value never see the wrapper — the pipeline unwraps before + * returning. + */ +public final class StructuredResult { + + /** Marker key identifying a wrapped structured result in transport form. */ + public static final String MARKER = "__prompty_structured"; + + private final Object data; + private final String rawJson; + + public StructuredResult(Object data, String rawJson) { + this.data = data; + this.rawJson = rawJson; + } + + /** The parsed structured value. */ + public Object data() { + return data; + } + + /** The exact JSON text the value was parsed from. */ + public String rawJson() { + return rawJson; + } + + /** Whether a pipeline value is a wrapped structured result. */ + public static boolean isWrapped(Object value) { + return value instanceof Map map && map.containsKey(MARKER); + } + + /** Wrap a structured value for pipeline transport. */ + public Map toTransport() { + Map transport = new LinkedHashMap<>(); + transport.put(MARKER, Boolean.TRUE); + transport.put("data", data); + transport.put("raw_json", rawJson); + return transport; + } + + /** Reconstruct a structured result from transport form, or null if it is not one. */ + public static StructuredResult fromTransport(Object value) { + if (!(value instanceof Map map) || !map.containsKey(MARKER)) { + return null; + } + Object raw = map.get("raw_json"); + return new StructuredResult(map.get("data"), raw instanceof String s ? s : null); + } + + /** + * Wrap {@code result} for transport when the agent declares outputs and the result is structured. + * + *

A scalar result is never wrapped: there is nothing to preserve that stringifying would lose. + */ + public static Object wrapIfNeeded(com.microsoft.prompty.model.Prompty agent, Object result) { + boolean hasOutputs = agent != null && agent.outputs != null && !agent.outputs.isEmpty(); + if (!hasOutputs || !(result instanceof Map || result instanceof List)) { + return result; + } + return new StructuredResult(result, TypraJson.stringify(result)).toTransport(); + } + + /** Unwrap transport form to its data, or return the value unchanged. */ + public static Object unwrap(Object value) { + StructuredResult wrapped = fromTransport(value); + return wrapped == null ? value : wrapped.data; + } + + /** + * Deserialize a pipeline value into a generated model type. + * + *

Accepts a wrapped structured result, a JSON string, or an already-parsed tree. The target + * must be a generated model class — that is what makes this a projection of the canonical model + * rather than a second, hand-rolled deserializer. + * + * @throws InvokerException with {@link InvokerException.Kind#PROCESS} if the value cannot be + * deserialized into {@code type} + */ + public static T cast(Object value, Class type) { + try { + Object tree = value; + StructuredResult wrapped = fromTransport(value); + if (wrapped != null) { + tree = wrapped.rawJson != null ? TypraJson.parse(wrapped.rawJson) : wrapped.data; + } else if (value instanceof String text) { + tree = TypraJson.parse(text); + } + + java.lang.reflect.Method load = + type.getMethod("load", Object.class, com.microsoft.prompty.model.LoadContext.class); + return type.cast(load.invoke(null, tree, new com.microsoft.prompty.model.LoadContext())); + } catch (NoSuchMethodException e) { + throw InvokerException.process( + "Cannot cast to " + type.getName() + ": not a generated model type"); + } catch (ReflectiveOperationException e) { + Throwable cause = e.getCause() == null ? e : e.getCause(); + throw InvokerException.process("Cast to " + type.getName() + " failed: " + cause.getMessage()); + } + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/Threads.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Threads.java new file mode 100644 index 000000000..df49ac55f --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Threads.java @@ -0,0 +1,156 @@ +package com.microsoft.prompty; + +import com.microsoft.prompty.model.ContentPart; +import com.microsoft.prompty.model.Message; +import com.microsoft.prompty.model.Role; +import com.microsoft.prompty.model.TextPart; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Substitutes conversation history back into a parsed message list. + * + *

A thread input never reaches the template as text — the renderer sees only a nonce marker (see + * {@link Nonces}). After parsing, the message holding that marker is split apart and the recorded + * conversation is spliced in where the marker stood, so prior turns arrive as real messages with + * their own roles rather than as a blob inside a system prompt. + */ +public final class Threads { + + private Threads() {} + + /** + * Expand every nonce marker in {@code messages} using the matching value from {@code inputs}. + * + *

At most one marker is expanded per message: a message is a single turn, and a second marker + * inside one would have no coherent role to fall back to. Text on either side of the marker is + * preserved as messages of the original role, and dropped when it trims to nothing. + * + * @param nonces property name to marker, as returned by {@link Nonces#prepareRenderInputs} + * @param inputs the original, unrewritten inputs, holding the real thread values + */ + public static List expand( + List messages, Map nonces, Map inputs) { + if (nonces == null || nonces.isEmpty()) { + return new ArrayList<>(messages); + } + + List result = new ArrayList<>(); + for (Message message : messages) { + if (!expandInto(result, message, nonces, inputs)) { + result.add(message); + } + } + return result; + } + + private static boolean expandInto( + List result, Message message, Map nonces, Map inputs) { + if (message.parts == null) { + return false; + } + for (ContentPart part : message.parts) { + if (!(part instanceof TextPart textPart) || textPart.value == null) { + continue; + } + for (Map.Entry entry : nonces.entrySet()) { + int index = textPart.value.indexOf(entry.getValue()); + if (index < 0) { + continue; + } + String before = textPart.value.substring(0, index).trim(); + String after = textPart.value.substring(index + entry.getValue().length()).trim(); + + if (!before.isEmpty()) { + result.add(Messages.withText(message.role, before)); + } + result.addAll(toMessages(inputs == null ? null : inputs.get(entry.getKey()))); + if (!after.isEmpty()) { + result.add(Messages.withText(message.role, after)); + } + return true; + } + } + return false; + } + + /** Convert a thread input value into messages. Anything unrecognised contributes nothing. */ + public static List toMessages(Object value) { + List messages = new ArrayList<>(); + if (!(value instanceof List items)) { + return messages; + } + for (Object item : items) { + Message message = toMessage(item); + if (message != null) { + messages.add(message); + } + } + return messages; + } + + /** + * Convert one {@code {role, content}} entry into a message. + * + *

{@code content} may be a plain string or a list of content parts — both forms appear in + * recorded conversations, and rejecting either would silently drop history. + * + * @return the message, or null if the entry has no recognisable role + */ + public static Message toMessage(Object value) { + if (!(value instanceof Map map)) { + return null; + } + Object rawRole = map.get("role"); + if (!(rawRole instanceof String roleName)) { + return null; + } + Role role; + try { + role = Role.fromValue(roleName); + } catch (IllegalArgumentException e) { + return null; + } + + Message message = new Message(); + message.role = role; + message.parts = contentParts(map.get("content")); + message.metadata = new LinkedHashMap<>(); + + Object metadata = map.get("metadata"); + if (metadata instanceof Map entries) { + for (Map.Entry entry : entries.entrySet()) { + message.metadata.put(String.valueOf(entry.getKey()), entry.getValue()); + } + } + return message; + } + + private static List contentParts(Object content) { + List parts = new ArrayList<>(); + if (content instanceof String text) { + parts.add(Messages.textPart(text)); + return parts; + } + if (content instanceof List items) { + for (Object item : items) { + if (item instanceof Map map) { + Object value = map.get("value"); + Object kind = map.get("kind"); + if (kind == null || "text".equals(kind)) { + parts.add(Messages.textPart(value == null ? "" : String.valueOf(value))); + } else { + parts.add(ContentPart.load(map, new com.microsoft.prompty.model.LoadContext())); + } + } else if (item instanceof String text) { + parts.add(Messages.textPart(text)); + } + } + return parts; + } + parts.add(Messages.textPart("")); + return parts; + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/ToolDispatch.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/ToolDispatch.java new file mode 100644 index 000000000..b36fd5b19 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/ToolDispatch.java @@ -0,0 +1,286 @@ +package com.microsoft.prompty; + +import com.microsoft.prompty.model.Binding; +import com.microsoft.prompty.model.Prompty; +import com.microsoft.prompty.model.Tool; +import com.microsoft.prompty.model.ToolCall; +import com.microsoft.prompty.model.TypraJson; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Resolves a tool call to an implementation and runs it. + * + *

Dispatch is deliberately total: every path returns text, including the failures. A tool that + * is missing, that was called with unparseable arguments, or that threw is reported back to the + * model as an {@code "Error: ..."} string rather than raised. The model is the component best + * placed to recover — it can correct the arguments, choose a different tool, or explain the + * limitation to the user — and a turn that unwound instead would throw that opportunity away. + * + *

Implementations are resolved in three layers, most specific first: handlers passed in + * {@link TurnOptions}, then globally registered handlers by tool name, then a handler registered + * for the tool's {@code kind} (falling back to a {@code "*"} wildcard). This lets a caller override + * one tool for one turn without disturbing anything registered process-wide. + */ +public final class ToolDispatch { + + private static final Map NAMED = new ConcurrentHashMap<>(); + private static final Map BY_KIND = new ConcurrentHashMap<>(); + + /** Longest prefix of unparseable arguments quoted back in an error. */ + private static final int ERROR_EXCERPT_CHARS = 200; + + private static final Pattern CODE_FENCE = + Pattern.compile("^\\s*```(?:json)?\\s*\\n?(.*?)\\n?\\s*```\\s*$", Pattern.DOTALL); + + private static final Pattern TRAILING_COMMA = Pattern.compile(",\\s*([}\\]])"); + + private ToolDispatch() {} + + /** Runs any tool of a given {@code kind}, using its declaration from the agent. */ + public interface KindHandler { + String execute(Tool definition, Object arguments, Prompty agent, Object parentInputs); + } + + /** Register a handler for one tool name, visible to every turn in this process. */ + public static void registerTool(String name, ToolHandler handler) { + NAMED.put(name, handler); + } + + /** Whether a handler is registered for {@code name}. */ + public static boolean hasTool(String name) { + return NAMED.containsKey(name); + } + + /** Remove every globally registered tool handler. */ + public static void clearTools() { + NAMED.clear(); + } + + /** Register a handler for every tool of one {@code kind}, or {@code "*"} for any kind. */ + public static void registerToolHandler(String kind, KindHandler handler) { + BY_KIND.put(kind, handler); + } + + /** Whether a handler is registered for {@code kind}. */ + public static boolean hasToolHandler(String kind) { + return BY_KIND.containsKey(kind); + } + + /** Remove every registered kind handler. */ + public static void clearToolHandlers() { + BY_KIND.clear(); + } + + /** + * Resolve and run one tool call. + * + * @param parentInputs the enclosing turn's inputs, used to satisfy the tool's bindings + * @return the text to return to the model; never null, and prefixed {@code "Error: "} on failure + */ + public static String dispatch( + ToolCall toolCall, + Map callerTools, + Prompty agent, + Object parentInputs) { + Object arguments; + try { + arguments = parseArguments(toolCall.arguments); + } catch (IllegalArgumentException failure) { + return "Error: Invalid tool arguments JSON: " + failure.getMessage(); + } + + if (parentInputs != null && arguments instanceof Map) { + arguments = resolveBindings(agent, toolCall.name, arguments, parentInputs); + } + + ToolHandler caller = callerTools == null ? null : callerTools.get(toolCall.name); + if (caller != null) { + return guard(caller, arguments); + } + + ToolHandler named = NAMED.get(toolCall.name); + if (named != null) { + return guard(named, arguments); + } + + Tool definition = findTool(agent, toolCall.name); + if (definition != null) { + KindHandler handler = BY_KIND.get(definition.kind == null ? "" : definition.kind); + if (handler == null) { + handler = BY_KIND.get("*"); + } + if (handler != null) { + try { + return handler.execute(definition, arguments, agent, parentInputs); + } catch (RuntimeException failure) { + return "Error: " + describe(failure); + } + } + } + + return "Error: No handler registered for tool '" + toolCall.name + "'"; + } + + /** + * Inject values from the enclosing turn's inputs into a tool's arguments. + * + *

Bindings let a tool receive something the model was never told about — a tenant id, a + * caller's identity — so it cannot be spoofed by the model choosing a different value. Bound + * arguments therefore overwrite whatever the model supplied under the same name. + */ + public static Object resolveBindings( + Prompty agent, String toolName, Object arguments, Object parentInputs) { + if (!(parentInputs instanceof Map parents) || !(arguments instanceof Map args)) { + return arguments; + } + Tool definition = findTool(agent, toolName); + if (definition == null || definition.bindings == null || definition.bindings.isEmpty()) { + return arguments; + } + Map merged = new LinkedHashMap<>(); + for (Map.Entry entry : args.entrySet()) { + merged.put(String.valueOf(entry.getKey()), entry.getValue()); + } + for (Binding binding : definition.bindings) { + if (binding == null || binding.name == null || binding.input == null) { + continue; + } + if (parents.containsKey(binding.input)) { + merged.put(binding.name, parents.get(binding.input)); + } + } + return merged; + } + + /** + * Parse tool arguments, tolerating the ways models commonly mangle JSON. + * + *

Models wrap JSON in markdown fences, prepend explanations, and leave trailing commas. + * Rejecting those outright would fail a turn over formatting when the intent is unambiguous, so + * each is recovered in turn: exact parse, then fence stripping, then extracting the first + * balanced object, then removing trailing commas. + * + * @throws IllegalArgumentException if no strategy yields valid JSON + */ + public static Object parseArguments(String raw) { + String text = raw == null ? "" : raw; + if (text.isBlank()) { + return new LinkedHashMap(); + } + + Object direct = tryParse(text); + if (direct != NOT_JSON) { + return direct; + } + + Matcher fenced = CODE_FENCE.matcher(text); + if (fenced.matches()) { + String stripped = fenced.group(1); + if (!stripped.equals(text)) { + Object parsed = tryParse(stripped); + if (parsed != NOT_JSON) { + return parsed; + } + } + } + + String block = firstJsonObject(text); + if (block != null) { + Object parsed = tryParse(block); + if (parsed != NOT_JSON) { + return parsed; + } + } + + String cleaned = TRAILING_COMMA.matcher(text).replaceAll("$1"); + if (!cleaned.equals(text)) { + Object parsed = tryParse(cleaned); + if (parsed != NOT_JSON) { + return parsed; + } + } + + throw new IllegalArgumentException( + "All JSON parse strategies failed for: " + + text.substring(0, Math.min(ERROR_EXCERPT_CHARS, text.length()))); + } + + /** Sentinel distinguishing "parsed to null" from "did not parse". */ + private static final Object NOT_JSON = new Object(); + + private static Object tryParse(String text) { + try { + return TypraJson.parse(text); + } catch (RuntimeException notJson) { + return NOT_JSON; + } + } + + /** Extract the first balanced {@code {...}} block, respecting strings and escapes. */ + private static String firstJsonObject(String text) { + int start = text.indexOf('{'); + if (start < 0) { + return null; + } + int depth = 0; + boolean inString = false; + boolean escaped = false; + for (int i = start; i < text.length(); i++) { + char ch = text.charAt(i); + if (escaped) { + escaped = false; + continue; + } + if (inString) { + if (ch == '\\') { + escaped = true; + } else if (ch == '"') { + inString = false; + } + continue; + } + switch (ch) { + case '"' -> inString = true; + case '{' -> depth++; + case '}' -> { + depth--; + if (depth == 0) { + return text.substring(start, i + 1); + } + } + default -> {} + } + } + return null; + } + + private static String guard(ToolHandler handler, Object arguments) { + try { + String result = handler.call(arguments); + return result == null ? "" : result; + } catch (RuntimeException failure) { + return "Error: " + describe(failure); + } + } + + private static String describe(Throwable failure) { + String message = failure.getMessage(); + return message == null || message.isEmpty() ? failure.getClass().getSimpleName() : message; + } + + private static Tool findTool(Prompty agent, String name) { + if (agent == null || agent.tools == null || name == null) { + return null; + } + for (Tool tool : agent.tools) { + if (tool != null && name.equals(tool.name)) { + return tool; + } + } + return null; + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/ToolHandler.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/ToolHandler.java new file mode 100644 index 000000000..b78ac583c --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/ToolHandler.java @@ -0,0 +1,29 @@ +package com.microsoft.prompty; + +/** + * A caller-supplied implementation of one tool. + * + *

Handlers take parsed arguments and return the text the model should see. Returning text rather + * than a typed value is deliberate: whatever a tool produces has to be rendered into the + * conversation for the model to read, so the handler is the right place to decide how. + * + *

A handler that throws is not fatal. The turn converts the failure into an {@code "Error: ..."} + * result and feeds it back to the model, which can then apologise, retry with different arguments, + * or route around the tool. A tool being broken is a fact the model can act on, not a reason to + * abandon the turn. + * + *

Handlers are synchronous, matching the rest of this runtime. Where the Rust reference + * distinguishes sync from async handlers, callers here run blocking work directly — on a virtual + * thread if it should not occupy a platform thread. + */ +@FunctionalInterface +public interface ToolHandler { + + /** + * Run the tool. + * + * @param arguments the parsed arguments, normally a {@code Map} + * @return the result text to return to the model + */ + String call(Object arguments); +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/Tracer.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Tracer.java new file mode 100644 index 000000000..6e46a248f --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Tracer.java @@ -0,0 +1,110 @@ +package com.microsoft.prompty; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.BiConsumer; + +/** + * Structured tracing for pipeline stages. + * + *

A span is opened for each stage, key/value observations are emitted into it, and it closes when + * the stage finishes. Registered listeners receive completed spans; with no listener registered the + * whole facility costs a field read and a null check, so instrumentation can stay in the hot path + * unconditionally. + * + *

Spans nest per thread, so a listener can reconstruct the call tree from {@link Span#depth()}. + */ +public final class Tracer { + + private static final List>> LISTENERS = + new CopyOnWriteArrayList<>(); + + private static final ThreadLocal DEPTH = ThreadLocal.withInitial(() -> 0); + + private Tracer() {} + + /** Register a listener invoked with the name and observations of each completed span. */ + public static void addListener(BiConsumer> listener) { + LISTENERS.add(listener); + } + + /** Remove a previously registered listener. */ + public static void removeListener(BiConsumer> listener) { + LISTENERS.remove(listener); + } + + /** Remove every registered listener. */ + public static void clearListeners() { + LISTENERS.clear(); + } + + /** Whether any listener is registered. Callers may skip building costly trace values if not. */ + public static boolean isEnabled() { + return !LISTENERS.isEmpty(); + } + + /** Open a span. The caller must close it, ideally with try-with-resources. */ + public static Span start(String name) { + return new Span(name); + } + + /** A single traced stage. */ + public static final class Span implements AutoCloseable { + + private final String name; + private final int depth; + private final Map observations; + private boolean closed; + + private Span(String name) { + this.name = name; + this.depth = DEPTH.get(); + this.observations = isEnabled() ? new LinkedHashMap<>() : null; + DEPTH.set(this.depth + 1); + } + + /** The nesting depth of this span, counting from zero at the outermost. */ + public int depth() { + return depth; + } + + /** Record an observation. Ignored when no listener is registered. */ + public Span emit(String key, Object value) { + if (observations != null) { + observations.put(key, value); + } + return this; + } + + /** Record a failure and its message. */ + public Span error(Throwable error) { + return emit("error", error.getMessage()); + } + + @Override + public void close() { + if (closed) { + return; + } + closed = true; + DEPTH.set(depth); + if (observations == null) { + return; + } + // Copy first: a listener must not be able to mutate what later listeners see. + Map snapshot = new LinkedHashMap<>(observations); + snapshot.put("__depth", depth); + for (BiConsumer> listener : new ArrayList<>(LISTENERS)) { + try { + listener.accept(name, snapshot); + } catch (RuntimeException ignored) { + // A telemetry sink must never be able to fail an operation that already succeeded, nor + // stop the sinks registered after it from seeing the span. + } + } + } + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/TurnOptions.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/TurnOptions.java new file mode 100644 index 000000000..74fe52948 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/TurnOptions.java @@ -0,0 +1,271 @@ +package com.microsoft.prompty; + +import com.microsoft.prompty.engine.Ports; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Function; + +/** + * How one turn should run. + * + *

Everything here is optional. The defaults describe a plain conversational round-trip; each + * setting opts into one additional behaviour, and they compose. Instances are immutable and built + * with {@link #builder()}, so an options bundle can be shared across turns without one turn's + * configuration leaking into another's. + */ +public final class TurnOptions { + + /** Model calls a turn will make before giving up. */ + public static final int DEFAULT_MAX_ITERATIONS = 10; + + /** Attempts per model call, including the first. */ + public static final int DEFAULT_MAX_LLM_RETRIES = 3; + + private final int maxIterations; + private final int maxLlmRetries; + private final boolean raw; + private final boolean parallelToolCalls; + private final Map tools; + private final AgentEvent.Listener onEvent; + private final CancellationToken cancellation; + private final Integer contextBudget; + private final Guardrails guardrails; + private final Steering steering; + private final Compaction compaction; + private final Function validator; + private final Ports.DurabilityPort durability; + private final Ports.PermissionPort permission; + private final Ports.PostCommitPort postCommit; + + private TurnOptions(Builder builder) { + this.maxIterations = builder.maxIterations; + this.maxLlmRetries = builder.maxLlmRetries; + this.raw = builder.raw; + this.parallelToolCalls = builder.parallelToolCalls; + this.tools = Map.copyOf(builder.tools); + this.onEvent = builder.onEvent; + this.cancellation = builder.cancellation; + this.contextBudget = builder.contextBudget; + this.guardrails = builder.guardrails; + this.steering = builder.steering; + this.compaction = builder.compaction; + this.validator = builder.validator; + this.durability = builder.durability; + this.permission = builder.permission; + this.postCommit = builder.postCommit; + } + + /** Options with every default in place. */ + public static TurnOptions defaults() { + return builder().build(); + } + + /** A new builder, pre-populated with the defaults. */ + public static Builder builder() { + return new Builder(); + } + + /** How many model calls this turn may make. */ + public int maxIterations() { + return maxIterations; + } + + /** How many attempts each model call gets. */ + public int maxLlmRetries() { + return maxLlmRetries; + } + + /** Whether to return the provider's raw response instead of the processed result. */ + public boolean raw() { + return raw; + } + + /** Whether the caller asked for tool calls to run concurrently. */ + public boolean parallelToolCalls() { + return parallelToolCalls; + } + + /** Caller-supplied tool implementations, keyed by tool name. */ + public Map tools() { + return tools; + } + + /** Where live events go, or null. */ + public AgentEvent.Listener onEvent() { + return onEvent; + } + + /** The token this turn watches, never null. */ + public CancellationToken cancellation() { + return cancellation == null ? CancellationToken.none() : cancellation; + } + + /** The character budget for the conversation, or null for no trimming. */ + public Integer contextBudget() { + return contextBudget; + } + + /** The policy hooks, or null. */ + public Guardrails guardrails() { + return guardrails; + } + + /** The mid-turn instruction queue, or null. */ + public Steering steering() { + return steering; + } + + /** The summarizer for trimmed messages, or null. */ + public Compaction compaction() { + return compaction; + } + + /** A final-output check returning an error message, or null when valid. */ + public Function validator() { + return validator; + } + + /** Where the durable journal is written, or null to keep the turn in memory. */ + public Ports.DurabilityPort durability() { + return durability; + } + + /** Who authorizes tool calls, or null to defer to guardrails. */ + public Ports.PermissionPort permission() { + return permission; + } + + /** A non-fatal effect to run after a successful commit, or null. */ + public Ports.PostCommitPort postCommit() { + return postCommit; + } + + /** Builds {@link TurnOptions}. */ + public static final class Builder { + private int maxIterations = DEFAULT_MAX_ITERATIONS; + private int maxLlmRetries = DEFAULT_MAX_LLM_RETRIES; + private boolean raw; + private boolean parallelToolCalls; + private final Map tools = new LinkedHashMap<>(); + private AgentEvent.Listener onEvent; + private CancellationToken cancellation; + private Integer contextBudget; + private Guardrails guardrails; + private Steering steering; + private Compaction compaction; + private Function validator; + private Ports.DurabilityPort durability; + private Ports.PermissionPort permission; + private Ports.PostCommitPort postCommit; + + private Builder() {} + + /** Cap the number of model calls. */ + public Builder maxIterations(int maxIterations) { + this.maxIterations = maxIterations; + return this; + } + + /** Cap the attempts per model call. */ + public Builder maxLlmRetries(int maxLlmRetries) { + this.maxLlmRetries = maxLlmRetries; + return this; + } + + /** Return the provider's raw response instead of the processed result. */ + public Builder raw(boolean raw) { + this.raw = raw; + return this; + } + + /** + * Ask for concurrent tool execution. + * + *

Rejected at turn start: the engine commits each tool effect durably in request order, and + * running them concurrently would make the journal — and therefore replay — non-deterministic. + */ + public Builder parallelToolCalls(boolean parallelToolCalls) { + this.parallelToolCalls = parallelToolCalls; + return this; + } + + /** Register one tool implementation. */ + public Builder tool(String name, ToolHandler handler) { + this.tools.put(name, handler); + return this; + } + + /** Register several tool implementations. */ + public Builder tools(Map tools) { + if (tools != null) { + this.tools.putAll(tools); + } + return this; + } + + /** Receive live events. */ + public Builder onEvent(AgentEvent.Listener onEvent) { + this.onEvent = onEvent; + return this; + } + + /** Watch a cancellation token. */ + public Builder cancellation(CancellationToken cancellation) { + this.cancellation = cancellation; + return this; + } + + /** Trim the conversation to this many characters before each model call. */ + public Builder contextBudget(Integer contextBudget) { + this.contextBudget = contextBudget; + return this; + } + + /** Apply policy hooks around model calls and tool dispatch. */ + public Builder guardrails(Guardrails guardrails) { + this.guardrails = guardrails; + return this; + } + + /** Inject mid-turn instructions from this queue. */ + public Builder steering(Steering steering) { + this.steering = steering; + return this; + } + + /** Summarize trimmed messages rather than dropping them mechanically. */ + public Builder compaction(Compaction compaction) { + this.compaction = compaction; + return this; + } + + /** Reject a final output by returning an error message from this function. */ + public Builder validator(Function validator) { + this.validator = validator; + return this; + } + + /** Persist the engine journal so the turn can be resumed. */ + public Builder durability(Ports.DurabilityPort durability) { + this.durability = durability; + return this; + } + + /** Authorize tool calls through a host-supplied policy. */ + public Builder permission(Ports.PermissionPort permission) { + this.permission = permission; + return this; + } + + /** Run a non-fatal effect after a successful commit. */ + public Builder postCommit(Ports.PostCommitPort postCommit) { + this.postCommit = postCommit; + return this; + } + + /** Build the options. */ + public TurnOptions build() { + return new TurnOptions(this); + } + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/ContextException.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/ContextException.java new file mode 100644 index 000000000..905e776a0 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/ContextException.java @@ -0,0 +1,22 @@ +package com.microsoft.prompty.engine; + +/** A failure raised while assembling the context for a model invocation. */ +public class ContextException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + private ContextException(String message, Throwable cause) { + super(message, cause); + } + + /** A source, transform, or packing strategy failed. */ + public static ContextException stage(String stage, String name, Throwable cause) { + return new ContextException( + stage + " '" + name + "' failed: " + (cause == null ? "" : cause.getMessage()), cause); + } + + /** A packing strategy produced a snapshot that breaks a replay or portability invariant. */ + public static ContextException invalidSnapshot(String message) { + return new ContextException("invalid context snapshot: " + message, null); + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/ContextPipeline.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/ContextPipeline.java new file mode 100644 index 000000000..9c066f1fa --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/ContextPipeline.java @@ -0,0 +1,222 @@ +package com.microsoft.prompty.engine; + +import com.microsoft.prompty.model.ContextCandidate; +import com.microsoft.prompty.model.ContextRequest; +import com.microsoft.prompty.model.InvocationContextDecision; +import com.microsoft.prompty.model.InvocationContextDisposition; +import com.microsoft.prompty.model.InvocationContextPortability; +import com.microsoft.prompty.model.InvocationContextState; +import com.microsoft.prompty.model.Message; +import com.microsoft.prompty.model.ModelInvocationContextSnapshot; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +/** + * Composes context sources, transforms, and a packing strategy into one immutable snapshot per model + * invocation. + * + *

Sources and transforms run in registration order. That ordering is load-bearing: a replayed run + * must reproduce the same candidate list and the same decision list, and it can only do so if every + * stage runs in a fixed sequence. + * + *

Every candidate that enters the pipeline leaves it accounted for. Candidates a transform drops, + * and candidates the packer silently ignores, both get an explicit {@code EXCLUDED} decision, so the + * snapshot records why a piece of context is missing rather than leaving it unexplained. + */ +public final class ContextPipeline { + + /** Supplies candidates such as history, recalled memory, files, or host state. */ + public interface Source { + String name(); + + List load(ContextRequest request); + } + + /** Filters, redacts, ranks, deduplicates, or enriches candidates. */ + public interface Transform { + String name(); + + List apply(ContextRequest request, List candidates); + } + + /** Selects and orders candidates under token, cost, and cache-affinity constraints. */ + public interface PackingStrategy { + String name(); + + ModelInvocationContextSnapshot pack(ContextRequest request, List candidates); + } + + private final List sources = new ArrayList<>(); + private final List transforms = new ArrayList<>(); + private final PackingStrategy packing; + + public ContextPipeline(PackingStrategy packing) { + this.packing = packing; + } + + /** A pipeline with no sources or transforms that appends candidates in order. */ + public static ContextPipeline appendOnly() { + return new ContextPipeline(new AppendPackingStrategy()); + } + + public ContextPipeline withSource(Source source) { + sources.add(source); + return this; + } + + public ContextPipeline withTransform(Transform transform) { + transforms.add(transform); + return this; + } + + /** Assemble and validate the snapshot for one model invocation. */ + public ModelInvocationContextSnapshot prepare(ContextRequest request) { + List candidates = new ArrayList<>(); + for (Source source : sources) { + List loaded; + try { + loaded = source.load(request); + } catch (ContextException e) { + throw ContextException.stage("context source", source.name(), e); + } + if (loaded != null) { + candidates.addAll(loaded); + } + } + + Set seen = new HashSet<>(); + for (ContextCandidate candidate : candidates) { + if (!seen.add(candidate.id)) { + throw ContextException.invalidSnapshot("duplicate context candidate id '" + candidate.id + "'"); + } + } + + List excluded = new ArrayList<>(); + for (Transform transform : transforms) { + List before = candidates; + try { + candidates = transform.apply(request, new ArrayList<>(before)); + } catch (ContextException e) { + throw ContextException.stage("context transform", transform.name(), e); + } + if (candidates == null) { + candidates = new ArrayList<>(); + } + Set retained = new LinkedHashSet<>(); + for (ContextCandidate candidate : candidates) { + retained.add(candidate.id); + } + for (ContextCandidate candidate : before) { + if (!retained.contains(candidate.id)) { + excluded.add( + decision( + candidate, + InvocationContextDisposition.EXCLUDED, + "excluded by context transform '" + transform.name() + "'", + null)); + } + } + } + + List packedCandidates = new ArrayList<>(candidates); + ModelInvocationContextSnapshot snapshot; + try { + snapshot = packing.pack(request, candidates); + } catch (ContextException e) { + throw ContextException.stage("packing strategy", packing.name(), e); + } + if (snapshot.decisions == null) { + snapshot.decisions = new ArrayList<>(); + } + + Set decided = new HashSet<>(); + for (InvocationContextDecision decision : snapshot.decisions) { + decided.add(decision.candidateId); + } + for (ContextCandidate candidate : packedCandidates) { + if (!decided.contains(candidate.id)) { + excluded.add( + decision( + candidate, + InvocationContextDisposition.EXCLUDED, + "excluded without an explicit decision by packing strategy '" + packing.name() + "'", + null)); + } + } + snapshot.decisions.addAll(excluded); + Snapshots.validateFor(snapshot, request); + return snapshot; + } + + private static InvocationContextDecision decision( + ContextCandidate candidate, + InvocationContextDisposition disposition, + String reason, + Integer rank) { + InvocationContextDecision decision = new InvocationContextDecision(); + decision.candidateId = candidate.id; + decision.disposition = disposition; + decision.reason = reason; + decision.rank = rank; + decision.metadata = candidate.metadata; + return decision; + } + + /** + * The deterministic baseline packer: keep the request's messages, then append every candidate's + * messages in order. + * + *

Production profiles replace this with token-aware, relevance-aware, or cache-affinity + * strategies without the engine changing at all — which is the point of packing being a port. + */ + public static final class AppendPackingStrategy implements PackingStrategy { + @Override + public String name() { + return "append"; + } + + @Override + public ModelInvocationContextSnapshot pack( + ContextRequest request, List candidates) { + List messages = new ArrayList<>(request.messages); + List decisions = new ArrayList<>(); + int rank = 0; + for (ContextCandidate candidate : candidates) { + if (candidate.messages != null) { + messages.addAll(candidate.messages); + } + decisions.add( + decision( + candidate, + InvocationContextDisposition.INCLUDED, + "included by append strategy", + rank)); + rank++; + } + + ModelInvocationContextSnapshot snapshot = new ModelInvocationContextSnapshot(); + snapshot.id = "context:" + request.invocationId; + snapshot.sessionId = request.sessionId; + snapshot.turnId = request.turnId; + snapshot.invocationId = request.invocationId; + snapshot.iteration = request.iteration; + snapshot.messages = messages; + snapshot.decisions = decisions; + snapshot.stablePrefixMessages = request.stablePrefixMessages; + + InvocationContextState state = new InvocationContextState(); + InvocationContextState requested = request.contextState; + state.portability = + requested == null ? InvocationContextPortability.PORTABLE : requested.portability; + state.delegatedState = + requested == null || requested.delegatedState == null + ? null + : new ArrayList<>(requested.delegatedState); + snapshot.contextState = state; + return snapshot; + } + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/DefaultPorts.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/DefaultPorts.java new file mode 100644 index 000000000..ada50d369 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/DefaultPorts.java @@ -0,0 +1,154 @@ +package com.microsoft.prompty.engine; + +import com.microsoft.prompty.CancellationToken; +import com.microsoft.prompty.Messages; +import com.microsoft.prompty.model.EngineCheckpoint; +import com.microsoft.prompty.model.EngineEvent; +import com.microsoft.prompty.model.EnginePermissionDecision; +import com.microsoft.prompty.model.FinalOutputPolicyRequest; +import com.microsoft.prompty.model.FinalOutputPolicyResult; +import com.microsoft.prompty.model.HostPolicyRequest; +import com.microsoft.prompty.model.HostPolicyResult; +import com.microsoft.prompty.model.Message; +import com.microsoft.prompty.model.ModelInvocationResponse; +import com.microsoft.prompty.model.ModelToolRequest; +import com.microsoft.prompty.model.ModelToolResult; +import com.microsoft.prompty.model.RetryPolicyRequest; +import com.microsoft.prompty.model.TurnCommit; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Neutral port implementations used when a host supplies nothing of its own. + * + *

Each is a genuine no-op rather than a stub that throws, so an engine wired entirely from these + * defaults runs a complete, correct turn — it simply keeps no journal, asks no permission, and + * leaves canonical state untouched. + */ +public final class DefaultPorts { + + private DefaultPorts() {} + + /** Approves every tool request. */ + public static final class AllowAllPermissions implements Ports.PermissionPort { + @Override + public EnginePermissionDecision authorize( + ModelToolRequest request, CancellationToken cancellation) { + EnginePermissionDecision decision = new EnginePermissionDecision(); + decision.approved = true; + decision.reason = "allow_all"; + return decision; + } + } + + /** Discards every event and checkpoint. */ + public static final class NoopDurability implements Ports.DurabilityPort { + @Override + public void append(EngineEvent event) { + // Intentionally empty: a host that wants durability supplies its own port. + } + + @Override + public void appendWithCheckpoint(List events, EngineCheckpoint checkpoint) { + // Intentionally empty. + } + } + + /** Runs no post-commit effect. */ + public static final class NoopPostCommit implements Ports.PostCommitPort { + @Override + public void afterCommit(String effectId, TurnCommit commit, CancellationToken cancellation) { + // Intentionally empty. + } + } + + /** Discards every stream chunk. */ + public static final class NoopModelStream implements Ports.ModelStreamPort { + @Override + public void emit(ModelStreamChunk chunk) { + // Intentionally empty. + } + } + + /** Returns canonical state and final output unchanged. */ + public static final class NoopHostPolicy implements Ports.HostPolicyPort { + @Override + public HostPolicyResult beforeModel(HostPolicyRequest request, CancellationToken cancellation) { + HostPolicyResult result = new HostPolicyResult(); + result.messages = request.messages; + result.stablePrefixMessages = request.stablePrefixMessages; + return result; + } + + @Override + public FinalOutputPolicyResult beforeCommit( + FinalOutputPolicyRequest request, CancellationToken cancellation) { + FinalOutputPolicyResult result = new FinalOutputPolicyResult(); + result.output = request.output; + return result; + } + } + + /** Retries immediately, with no delay. */ + public static final class NoopRetryPolicy implements Ports.RetryPolicyPort { + @Override + public void backoff(RetryPolicyRequest request, CancellationToken cancellation) { + // Intentionally empty: deterministic tests must not wait, and a host that wants + // exponential backoff supplies its own port. + } + } + + /** + * Provider-neutral tool formatting: keep the assistant messages, then append one tool message per + * request, in request order. + * + *

Ordering follows {@code response.toolRequests} rather than the order results arrived, so the + * conversation a resumed run rebuilds matches the one the original run sent. + */ + public static final class DefaultConversation implements Ports.ConversationPort { + @Override + public List formatToolExchange( + ModelInvocationResponse response, List results) { + List messages = new ArrayList<>(); + if (response.assistantMessages != null) { + messages.addAll(response.assistantMessages); + } + if (response.toolRequests != null) { + for (ModelToolRequest request : response.toolRequests) { + for (ModelToolResult result : results) { + if (request.id.equals(result.requestId)) { + messages.add(Messages.toolResult(request.id, ToolResults.modelText(result))); + break; + } + } + } + } + return messages; + } + } + + /** Wall-clock timestamps in ISO-8601. */ + public static final class SystemClock implements Ports.Clock { + @Override + public String now() { + return Instant.now().toString(); + } + } + + /** + * Monotonically numbered identifiers of the form {@code kind-1}, {@code kind-2}, …. + * + *

Unique within one generator instance, which is all the engine needs: identifiers scope a + * single run, and a host wanting globally unique values supplies its own generator. + */ + public static final class SequentialIds implements Ports.IdGenerator { + private final AtomicLong counter = new AtomicLong(); + + @Override + public String nextId(String kind) { + return kind + "-" + counter.incrementAndGet(); + } + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/HostPolicyException.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/HostPolicyException.java new file mode 100644 index 000000000..8c661dbe6 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/HostPolicyException.java @@ -0,0 +1,25 @@ +package com.microsoft.prompty.engine; + +/** + * A deterministic policy rejection raised by a {@link HostPolicyPort}. + * + *

Unlike a {@link PortException}, this is never retried. The host has made a decision — a + * guardrail denied the input, an output failed validation — and the engine records that decision as + * the turn's failure kind rather than treating it as a transient fault. + */ +public class HostPolicyException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + private final String errorKind; + + public HostPolicyException(String errorKind, String message) { + super(message); + this.errorKind = errorKind; + } + + /** The stable identifier the engine commits as the turn's {@code errorKind}. */ + public String errorKind() { + return errorKind; + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/ModelStreamChunk.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/ModelStreamChunk.java new file mode 100644 index 000000000..d8b45e99b --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/ModelStreamChunk.java @@ -0,0 +1,21 @@ +package com.microsoft.prompty.engine; + +/** + * An ephemeral chunk produced while a model invocation is in flight. + * + *

Stream chunks deliberately sit outside the durable event journal. They arrive at whatever rate + * the provider emits them, they carry no commit semantics, and a host that drops them still gets an + * identical committed turn. Only the completed {@code ModelInvocationResponse} participates in + * ordering, so a replayed turn need not reproduce chunk boundaries. + */ +public sealed interface ModelStreamChunk { + + /** Model-visible output text. */ + record Text(String value) implements ModelStreamChunk {} + + /** Reasoning text, where the provider exposes it separately from output. */ + record Thinking(String value) implements ModelStreamChunk {} + + /** A raw provider chunk, passed through for hosts that understand the provider's shape. */ + record Provider(Object value) implements ModelStreamChunk {} +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/PortException.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/PortException.java new file mode 100644 index 000000000..ed4de5500 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/PortException.java @@ -0,0 +1,77 @@ +package com.microsoft.prompty.engine; + +import java.util.Map; + +/** + * A failure reported by one of the effect ports the turn engine drives. + * + *

Two flags decide what the engine does next, and they mean very different things. {@link + * #outcomeUnknown} says the effect may or may not have happened — a request that timed out after the + * provider accepted it, say. The engine cannot retry that safely, because retrying might duplicate a + * side effect it cannot see, so it stops and commits a turn that asks the host to reconcile. {@link + * #configurationError} says the request itself is wrong, so no amount of retrying or model recovery + * will help; the engine fails the turn immediately rather than feeding the error back to the model. + * + *

A plain failure is neither: the engine retries it up to the request's attempt budget, and, for + * tools, hands the failure to the model as a tool result so it can adapt. + */ +public class PortException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + private final boolean outcomeUnknown; + private final boolean configurationError; + private final transient Map metadata; + + private PortException( + String message, + boolean outcomeUnknown, + boolean configurationError, + Map metadata, + Throwable cause) { + super(message, cause); + this.outcomeUnknown = outcomeUnknown; + this.configurationError = configurationError; + this.metadata = metadata; + } + + /** A retryable failure whose effect definitely did not occur. */ + public static PortException of(String message) { + return new PortException(message, false, false, null, null); + } + + /** A retryable failure whose effect definitely did not occur, wrapping a cause. */ + public static PortException of(String message, Throwable cause) { + return new PortException(message, false, false, null, cause); + } + + /** An effect that may have occurred, so the turn must stop and be reconciled by the host. */ + public static PortException indeterminate(String message) { + return new PortException(message, true, false, null, null); + } + + /** An indeterminate effect carrying provider-specific data the host needs to reconcile it. */ + public static PortException indeterminate(String message, Map metadata) { + return new PortException(message, true, false, metadata, null); + } + + /** A plan or binding error that neither a retry nor the model can recover from. */ + public static PortException configuration(String message) { + return new PortException(message, false, true, null, null); + } + + /** Whether the effect may have occurred, leaving the durable record ambiguous. */ + public boolean outcomeUnknown() { + return outcomeUnknown; + } + + /** Whether the request is malformed, making retries and model recovery pointless. */ + public boolean configurationError() { + return configurationError; + } + + /** Provider-specific reconciliation data, or null when the port supplied none. */ + public Map metadata() { + return metadata; + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/Ports.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/Ports.java new file mode 100644 index 000000000..2dfaf04dd --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/Ports.java @@ -0,0 +1,111 @@ +package com.microsoft.prompty.engine; + +import com.microsoft.prompty.CancellationToken; +import com.microsoft.prompty.model.EngineCheckpoint; +import com.microsoft.prompty.model.EngineEvent; +import com.microsoft.prompty.model.EnginePermissionDecision; +import com.microsoft.prompty.model.FinalOutputPolicyRequest; +import com.microsoft.prompty.model.FinalOutputPolicyResult; +import com.microsoft.prompty.model.HostPolicyRequest; +import com.microsoft.prompty.model.HostPolicyResult; +import com.microsoft.prompty.model.Message; +import com.microsoft.prompty.model.ModelInvocationRequest; +import com.microsoft.prompty.model.ModelInvocationResponse; +import com.microsoft.prompty.model.ModelToolRequest; +import com.microsoft.prompty.model.ModelToolResult; +import com.microsoft.prompty.model.RetryPolicyRequest; +import com.microsoft.prompty.model.TurnCommit; +import java.util.List; + +/** + * The effect ports the turn engine drives. + * + *

The engine itself performs no I/O. Every outward effect — invoking a model, running a tool, + * asking permission, writing the journal — goes through one of these interfaces, which is what lets + * the same loop run a live provider turn and a deterministic replay without behaving differently. + * + *

All ports are synchronous, matching the rest of this runtime: callers that need concurrency run + * a turn on its own (virtual) thread rather than threading futures through the state machine. + */ +public final class Ports { + + private Ports() {} + + /** Invokes a model for one prepared context snapshot. */ + public interface ModelPort { + ModelInvocationResponse invoke( + ModelInvocationRequest request, CancellationToken cancellation, ModelStreamPort stream); + } + + /** Receives ephemeral chunks while a model invocation is in flight. */ + public interface ModelStreamPort { + /** Deliver a chunk. A delivery failure must never change semantic execution. */ + void emit(ModelStreamChunk chunk); + } + + /** + * Lets the host inspect and rewrite canonical state at the two points where doing so is safe. + * + *

This is the seam guardrails, context trimming, and steering plug into. + */ + public interface HostPolicyPort { + HostPolicyResult beforeModel(HostPolicyRequest request, CancellationToken cancellation); + + FinalOutputPolicyResult beforeCommit( + FinalOutputPolicyRequest request, CancellationToken cancellation); + } + + /** Waits between failed model attempts. */ + public interface RetryPolicyPort { + void backoff(RetryPolicyRequest request, CancellationToken cancellation); + } + + /** + * Turns one completed model/tool batch into provider-valid conversation messages. + * + *

Providers disagree about this: OpenAI wants one message per tool result, Anthropic wants a + * single user message carrying every result. Delegating it keeps that difference out of the + * engine. + */ + public interface ConversationPort { + List formatToolExchange(ModelInvocationResponse response, List results); + } + + /** Decides whether a tool request may run. */ + public interface PermissionPort { + EnginePermissionDecision authorize(ModelToolRequest request, CancellationToken cancellation); + } + + /** Runs an authorized tool request. */ + public interface ToolPort { + ModelToolResult execute(ModelToolRequest request, CancellationToken cancellation); + } + + /** + * Persists the engine's event journal and checkpoints. + * + *

{@link #appendWithCheckpoint} must be atomic. The engine relies on an event and the + * checkpoint that includes it landing together; if they can diverge, a resumed run can replay an + * effect that was already committed. + */ + public interface DurabilityPort { + void append(EngineEvent event); + + void appendWithCheckpoint(List events, EngineCheckpoint checkpoint); + } + + /** Runs a non-fatal effect after a successful turn is committed. */ + public interface PostCommitPort { + void afterCommit(String effectId, TurnCommit commit, CancellationToken cancellation); + } + + /** Supplies timestamps. Replay substitutes a deterministic implementation. */ + public interface Clock { + String now(); + } + + /** Supplies identifiers. Replay substitutes a deterministic implementation. */ + public interface IdGenerator { + String nextId(String kind); + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/RetryPolicyException.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/RetryPolicyException.java new file mode 100644 index 000000000..891ea9b83 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/RetryPolicyException.java @@ -0,0 +1,35 @@ +package com.microsoft.prompty.engine; + +/** + * A failure raised while waiting out a retry backoff. + * + *

Cancellation during a backoff is not an error condition — the caller asked the turn to stop — + * so it is modelled separately from a genuine backoff failure and commits a cancelled turn rather + * than a failed one. + */ +public class RetryPolicyException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + private final boolean cancelled; + + private RetryPolicyException(String message, boolean cancelled, Throwable cause) { + super(message, cause); + this.cancelled = cancelled; + } + + /** The backoff was interrupted because the turn was cancelled. */ + public static RetryPolicyException cancelled() { + return new RetryPolicyException("retry backoff cancelled", true, null); + } + + /** The backoff itself failed. */ + public static RetryPolicyException failed(PortException cause) { + return new RetryPolicyException(cause.getMessage(), false, cause); + } + + /** Whether the backoff ended because the turn was cancelled rather than because it failed. */ + public boolean isCancelled() { + return cancelled; + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/Snapshots.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/Snapshots.java new file mode 100644 index 000000000..7249cb825 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/Snapshots.java @@ -0,0 +1,79 @@ +package com.microsoft.prompty.engine; + +import com.microsoft.prompty.model.ContextRequest; +import com.microsoft.prompty.model.InvocationContextPortability; +import com.microsoft.prompty.model.InvocationContextState; +import com.microsoft.prompty.model.ModelInvocationContextSnapshot; +import java.util.List; +import java.util.Objects; + +/** + * Snapshot invariants required for caching, portability, and replay. + * + *

These validators live here rather than on {@code ModelInvocationContextSnapshot} because that + * class is emitted from the shared schema and must not be hand-edited. + */ +public final class Snapshots { + + private Snapshots() {} + + /** + * Check the invariants a snapshot must hold regardless of which request produced it. + * + *

The stable prefix must actually exist in the snapshot, because it is what a provider caches + * against. Portability and delegated state must agree: a snapshot claiming to be portable while + * pointing at provider-held state cannot be replayed elsewhere, and one claiming delegation while + * naming no state cannot be resumed at all. + */ + public static void validate(ModelInvocationContextSnapshot snapshot) { + int prefix = snapshot.stablePrefixMessages == null ? 0 : snapshot.stablePrefixMessages; + int size = snapshot.messages == null ? 0 : snapshot.messages.size(); + if (prefix < 0 || prefix > size) { + throw ContextException.invalidSnapshot( + "stable prefix contains " + prefix + " messages but snapshot contains " + size); + } + InvocationContextState state = snapshot.contextState; + InvocationContextPortability portability = + state == null || state.portability == null + ? InvocationContextPortability.PORTABLE + : state.portability; + List delegated = state == null ? null : state.delegatedState; + boolean hasDelegated = delegated != null && !delegated.isEmpty(); + if (portability == InvocationContextPortability.PORTABLE && hasDelegated) { + throw ContextException.invalidSnapshot( + "portable snapshots cannot contain delegated provider state"); + } + if (portability == InvocationContextPortability.DELEGATED && !hasDelegated) { + throw ContextException.invalidSnapshot( + "delegated snapshots must identify provider-held state"); + } + } + + /** Check snapshot invariants and that the snapshot identifies the invocation it was built for. */ + public static void validateFor(ModelInvocationContextSnapshot snapshot, ContextRequest request) { + validate(snapshot); + if (!Objects.equals(snapshot.sessionId, request.sessionId) + || !Objects.equals(snapshot.turnId, request.turnId) + || !Objects.equals(snapshot.invocationId, request.invocationId) + || !Objects.equals(snapshot.iteration, request.iteration)) { + throw ContextException.invalidSnapshot( + "snapshot identity (" + + snapshot.sessionId + + "/" + + snapshot.turnId + + "/" + + snapshot.invocationId + + "/" + + snapshot.iteration + + ") does not match request (" + + request.sessionId + + "/" + + request.turnId + + "/" + + request.invocationId + + "/" + + request.iteration + + ")"); + } + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/ToolResults.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/ToolResults.java new file mode 100644 index 000000000..529ffb410 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/ToolResults.java @@ -0,0 +1,39 @@ +package com.microsoft.prompty.engine; + +import com.microsoft.prompty.model.ModelToolResult; +import com.microsoft.prompty.model.TypraJson; + +/** + * Renders a tool result as the text the model sees. + * + *

This lives beside the engine rather than on the generated {@code ModelToolResult} because the + * generated model layer is emitted and must not be hand-edited. + */ +public final class ToolResults { + + private ToolResults() {} + + /** + * The model-visible text for a result. + * + *

A string output is passed through verbatim — wrapping it in JSON quotes would change what the + * model reads. Anything else is serialized, and an absent output renders as empty rather than as + * the literal {@code "null"}. + * + *

Divergence from the Rust reference: Rust holds the output as {@code Option} and so + * distinguishes an absent output ({@code None} → {@code ""}) from a tool that explicitly returned + * JSON null ({@code Some(Value::Null)} → {@code "null"}). Java's field is a plain {@code Object}, + * where both cases are {@code null}, so both render as empty. Closing this would mean carrying a + * sentinel through the generated model layer, which is not worth it for a tool that returns a + * bare null. + */ + public static String modelText(ModelToolResult result) { + if (result == null || result.output == null) { + return ""; + } + if (result.output instanceof String text) { + return text; + } + return TypraJson.stringify(result.output); + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/TurnEngine.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/TurnEngine.java new file mode 100644 index 000000000..6f4576b61 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/TurnEngine.java @@ -0,0 +1,1052 @@ +package com.microsoft.prompty.engine; + +import com.microsoft.prompty.CancellationToken; +import com.microsoft.prompty.Messages; +import com.microsoft.prompty.model.ContextRequest; +import com.microsoft.prompty.model.EngineCheckpoint; +import com.microsoft.prompty.model.EngineEvent; +import com.microsoft.prompty.model.EngineEventKind; +import com.microsoft.prompty.model.EnginePermissionDecision; +import com.microsoft.prompty.model.EngineTurnStatus; +import com.microsoft.prompty.model.FinalOutputPolicyRequest; +import com.microsoft.prompty.model.FinalOutputPolicyResult; +import com.microsoft.prompty.model.HostPolicyRequest; +import com.microsoft.prompty.model.HostPolicyResult; +import com.microsoft.prompty.model.DelegatedStateReference; +import com.microsoft.prompty.model.InvocationContextPortability; +import com.microsoft.prompty.model.InvocationContextState; +import com.microsoft.prompty.model.Message; +import com.microsoft.prompty.model.ModelInvocationContextSnapshot; +import com.microsoft.prompty.model.ModelInvocationRequest; +import com.microsoft.prompty.model.ModelInvocationResponse; +import com.microsoft.prompty.model.ModelReconciliationState; +import com.microsoft.prompty.model.ModelToolOutcome; +import com.microsoft.prompty.model.ModelToolRequest; +import com.microsoft.prompty.model.ModelToolResult; +import com.microsoft.prompty.model.ResumeContext; +import com.microsoft.prompty.model.RetryPolicyRequest; +import com.microsoft.prompty.model.SaveContext; +import com.microsoft.prompty.model.TurnCommit; +import com.microsoft.prompty.model.TurnEngineResult; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * The canonical turn state machine. + * + *

One loop drives every kind of turn — live provider calls, deterministic replay, resumed + * checkpoints — because the engine itself performs no I/O. Every outward effect goes through a port, + * so swapping a live model for a canned one changes what happens without changing what the engine + * decides. + * + *

The loop advances through three mutually exclusive states per iteration: + * + *

    + *
  1. a completed tool batch waiting to be folded into the conversation, + *
  2. tool requests still waiting to run, or + *
  3. neither, meaning it is time to call the model again. + *
+ * + *

Splitting a tool round this way — one request per pass, each persisted before the next starts — + * is what makes an interrupted turn resumable at tool granularity rather than restarting the whole + * round. + */ +public final class TurnEngine { + + private final ContextPipeline context; + private final TurnEngineEffects effects; + + public TurnEngine(ContextPipeline context, TurnEngineEffects effects) { + this.context = context; + this.effects = effects; + } + + /** An engine over the append-only context pipeline. */ + public static TurnEngine of(TurnEngineEffects effects) { + return new TurnEngine(ContextPipeline.appendOnly(), effects); + } + + /** Resume an interrupted turn from a durable {@link ResumeContext}. */ + public TurnEngineResult resume(ResumeContext resume, CancellationToken cancellation) { + return run(TurnEngineRequest.fromResume(resume), cancellation); + } + + /** Run one turn to a committed result. */ + public TurnEngineResult run(TurnEngineRequest request, CancellationToken cancellation) { + validateRequest(request); + TurnState state = new TurnState(request); + // Rust consumes the request by value, so an engine-assigned run id is never visible to the + // caller. Java passes by reference, so the id is assigned onto the run state instead of + // written back — otherwise reusing one request for a second run would silently inherit the + // first run's identity. + if (state.runId == null || state.runId.isEmpty()) { + state.runId = effects.ids.nextId("run"); + } + + Map startPayload = new LinkedHashMap<>(); + startPayload.put("maxIterations", state.maxIterations); + startPayload.put("startIteration", state.iteration); + startPayload.put("inputs", state.inputs); + emit(state, EngineEventKind.TURN_STARTED, null, null, startPayload); + + if (state.modelReconciliationResolution != null) { + ModelInvocationResponse response = state.modelReconciliationResolution; + state.modelReconciliationResolution = null; + ModelReconciliationState reconciliation = state.modelReconciliation; + if (reconciliation == null) { + throw new TurnEngineException.InvalidRequest( + "model reconciliation response is missing durable reconciliation state"); + } + state.reconciliationRequired = false; + state.modelReconciliation = null; + String error = state.applyModelResponse(reconciliation.invocationId, response); + if (error != null) { + return commitFailed(state, "provider_state_error", error, cancellation); + } + persistModelReconciliation(state, reconciliation.invocationId, reconciliation, response); + } + + if (state.reconciliationResolution != null) { + ModelToolResult resolution = state.reconciliationResolution; + state.reconciliationResolution = null; + persistReconciliation(state, resolution); + } + + if (state.reconciliationRequired) { + return commitReconciliation( + state, + "effect_outcome_unknown", + "Checkpoint requires explicit effect reconciliation", + cancellation); + } + + if (state.finalOutputReady) { + if (cancellation.isCancelled()) { + return commitCancelled(state, cancellation); + } + state.output = state.pendingOutput; + return applyFinalPolicy(state, cancellation); + } + + while (state.iteration < state.maxIterations) { + if (cancellation.isCancelled()) { + return commitCancelled(state, cancellation); + } + + // (1) A tool batch is complete: fold it into the conversation and close the iteration. + if (state.pendingToolRequests.isEmpty() && state.pendingModelResponse != null) { + String invocationId = + state.activeInvocationId != null + ? state.activeInvocationId + : effects.ids.nextId("invocation"); + List results; + try { + results = finalizeToolExchange(state); + } catch (PortException e) { + return commitFailed(state, "conversation_format_error", e.getMessage(), cancellation); + } + persistToolExchange(state, invocationId, results); + state.activeInvocationId = null; + state.iteration++; + continue; + } + + // (2) Tools are still outstanding: run exactly one, then persist before touching the next. + if (!state.pendingToolRequests.isEmpty()) { + String invocationId = + state.activeInvocationId != null + ? state.activeInvocationId + : effects.ids.nextId("invocation"); + ModelToolRequest toolRequest = state.pendingToolRequests.remove(0); + ModelToolResult toolResult; + try { + toolResult = executeTool(state, invocationId, toolRequest, cancellation); + } catch (ToolCancelled e) { + return commitCancelled(state, cancellation); + } catch (ToolPermissionFailed e) { + return commitFailed(state, "permission_error", e.getMessage(), cancellation); + } catch (ToolConfigurationFailed e) { + return commitFailed(state, "tool_configuration_error", e.getMessage(), cancellation); + } + boolean outcomeUnknown = toolResult.outcome == ModelToolOutcome.INDETERMINATE; + state.toolResults.add(toolResult); + if (state.pendingModelResponse == null) { + // Recovery path for checkpoints written before the conversation batch became + // explicit state: without a held response there is nothing to fold later, so the + // result has to enter the conversation now. + state.messages.add( + Messages.toolResult(toolRequest.id, ToolResults.modelText(toolResult))); + } + persistToolResult(state, invocationId, toolRequest); + if (outcomeUnknown) { + return commitReconciliation( + state, + "effect_outcome_unknown", + "Tool effect outcome is unknown and requires reconciliation", + cancellation); + } + if (state.pendingToolRequests.isEmpty() && state.pendingModelResponse == null) { + state.activeInvocationId = null; + state.iteration++; + } + continue; + } + + // (3) Nothing outstanding: prepare context and invoke the model. + String invocationId = effects.ids.nextId("invocation"); + if (state.policyAppliedForIteration) { + // The policy already ran for this iteration and its rewrite was checkpointed, so + // rerunning it on resume would apply the same transformation twice. + state.policyAppliedForIteration = false; + } else { + HostPolicyRequest policyRequest = new HostPolicyRequest(); + policyRequest.sessionId = state.sessionId; + policyRequest.turnId = state.turnId; + policyRequest.iteration = state.iteration; + policyRequest.messages = new ArrayList<>(state.messages); + policyRequest.stablePrefixMessages = state.stablePrefixMessages; + policyRequest.inputs = state.inputs; + + HostPolicyResult policyResult; + try { + policyResult = effects.policy.beforeModel(policyRequest, cancellation); + } catch (HostPolicyException e) { + return commitFailed(state, e.errorKind(), e.getMessage(), cancellation); + } + if (cancellation.isCancelled()) { + return commitCancelled(state, cancellation); + } + List rewritten = + policyResult.messages == null ? new ArrayList<>() : policyResult.messages; + int rewrittenPrefix = + policyResult.stablePrefixMessages == null ? 0 : policyResult.stablePrefixMessages; + if (rewrittenPrefix < 0 || rewrittenPrefix > rewritten.size()) { + return commitFailed( + state, + "policy_error", + "host policy stable prefix exceeds rewritten message count", + cancellation); + } + boolean policyChanged = + !state.messages.equals(rewritten) || state.stablePrefixMessages != rewrittenPrefix; + if (policyChanged) { + state.messages = new ArrayList<>(rewritten); + state.stablePrefixMessages = rewrittenPrefix; + persistPolicyUpdate(state, invocationId, policyResult.metadata); + state.policyAppliedForIteration = false; + } + } + + ContextRequest contextRequest = new ContextRequest(); + contextRequest.sessionId = state.sessionId; + contextRequest.turnId = state.turnId; + contextRequest.invocationId = invocationId; + contextRequest.iteration = state.iteration; + contextRequest.messages = new ArrayList<>(state.messages); + contextRequest.stablePrefixMessages = + Math.min(state.stablePrefixMessages, state.messages.size()); + contextRequest.contextState = currentContextState(state); + contextRequest.inputs = state.inputs; + + ModelInvocationContextSnapshot snapshot; + try { + snapshot = context.prepare(contextRequest); + } catch (ContextException e) { + return commitFailed(state, "context_error", e.getMessage(), cancellation); + } + int iteration = state.iteration; + emit(state, EngineEventKind.CONTEXT_PREPARED, invocationId, iteration, save(snapshot)); + state.snapshots.add(snapshot); + + if (cancellation.isCancelled()) { + return commitCancelled(state, cancellation); + } + + ModelInvocationRequest modelRequest = new ModelInvocationRequest(); + modelRequest.context = snapshot; + state.activeInvocationId = invocationId; + + int attempt = 0; + ModelInvocationResponse modelResponse = null; + while (modelResponse == null) { + if (cancellation.isCancelled()) { + return commitCancelled(state, cancellation); + } + Map startedPayload = new LinkedHashMap<>(); + startedPayload.put("snapshotId", snapshot.id); + startedPayload.put("attempt", attempt); + startedPayload.put("messageCount", snapshot.messages == null ? 0 : snapshot.messages.size()); + emit( + state, + EngineEventKind.MODEL_INVOCATION_STARTED, + invocationId, + iteration, + startedPayload); + + try { + modelResponse = effects.model.invoke(modelRequest, cancellation, effects.stream); + } catch (PortException source) { + if (cancellation.isCancelled()) { + return commitCancelled(state, cancellation); + } + attempt++; + boolean outcomeUnknown = source.outcomeUnknown(); + boolean exhausted = outcomeUnknown || attempt >= state.maxModelAttempts; + String reason = source.getMessage(); + + Map failedPayload = new LinkedHashMap<>(); + failedPayload.put("attempt", attempt - 1); + failedPayload.put("exhausted", exhausted); + failedPayload.put("outcomeUnknown", outcomeUnknown); + failedPayload.put("message", reason); + emit( + state, + EngineEventKind.MODEL_INVOCATION_FAILED, + invocationId, + iteration, + failedPayload); + + if (outcomeUnknown) { + // The provider may have run the invocation. Retrying could duplicate it and + // committing could lose it, so the turn stops for the host to reconcile. + state.reconciliationRequired = true; + ModelReconciliationState reconciliation = new ModelReconciliationState(); + reconciliation.invocationId = invocationId; + reconciliation.request = modelRequest; + reconciliation.failedAttempt = attempt - 1; + reconciliation.message = reason; + reconciliation.metadata = source.metadata(); + state.modelReconciliation = reconciliation; + persistModelReconciliationRequired(state, invocationId); + return commitReconciliation(state, "model_outcome_unknown", reason, cancellation); + } + if (exhausted) { + return commitFailed(state, "model_error", reason, cancellation); + } + + RetryPolicyRequest retryRequest = new RetryPolicyRequest(); + retryRequest.failedAttempts = attempt; + retryRequest.nextAttempt = attempt + 1; + retryRequest.maxAttempts = state.maxModelAttempts; + retryRequest.reason = reason; + try { + effects.retry.backoff(retryRequest, cancellation); + } catch (RetryPolicyException e) { + if (e.isCancelled()) { + return commitCancelled(state, cancellation); + } + return commitFailed(state, "retry_policy_error", e.getMessage(), cancellation); + } + if (cancellation.isCancelled()) { + return commitCancelled(state, cancellation); + } + } + } + + state.modelReconciliation = null; + state.reconciliationRequired = false; + String error = state.applyModelResponse(invocationId, modelResponse); + if (error != null) { + return commitFailed(state, "provider_state_error", error, cancellation); + } + persistModelResponse(state, invocationId, modelResponse); + + if (cancellation.isCancelled()) { + return commitCancelled(state, cancellation); + } + + if (state.finalOutputReady) { + state.output = state.pendingOutput; + return applyFinalPolicy(state, cancellation); + } + } + + return commitFailed(state, "max_iterations", "Maximum model iterations reached", cancellation); + } + + private void validateRequest(TurnEngineRequest request) { + if (request.sessionId == null || request.sessionId.isEmpty()) { + throw new TurnEngineException.InvalidRequest("session_id is required"); + } + if (request.turnId == null || request.turnId.isEmpty()) { + throw new TurnEngineException.InvalidRequest("turn_id is required"); + } + if (request.maxModelAttempts <= 0) { + throw new TurnEngineException.InvalidRequest("max_model_attempts must be greater than zero"); + } + if (request.startIteration > request.maxIterations) { + throw new TurnEngineException.InvalidRequest("start_iteration must not exceed max_iterations"); + } + int messageCount = request.messages == null ? 0 : request.messages.size(); + if (request.stablePrefixMessages > messageCount) { + throw new TurnEngineException.InvalidRequest( + "stable_prefix_messages exceeds initial message count"); + } + if (request.portability == InvocationContextPortability.PORTABLE + && request.delegatedState != null + && !request.delegatedState.isEmpty()) { + throw new TurnEngineException.InvalidRequest( + "portable turns cannot begin with delegated provider state"); + } + } + + /** + * Fold a completed tool batch into the conversation. + * + *

Formatting only happens once every request in the batch has a result. A partially answered + * batch is put back untouched, because emitting half of one would leave the conversation with + * unmatched tool calls that most providers reject outright. + */ + private List finalizeToolExchange(TurnState state) { + ModelInvocationResponse response = state.pendingModelResponse; + if (response == null) { + return new ArrayList<>(); + } + state.pendingModelResponse = null; + List requests = + response.toolRequests == null ? new ArrayList<>() : response.toolRequests; + if (requests.isEmpty()) { + return new ArrayList<>(); + } + List results = new ArrayList<>(); + for (ModelToolRequest request : requests) { + for (ModelToolResult result : state.toolResults) { + if (Objects.equals(request.id, result.requestId)) { + results.add(result); + break; + } + } + } + if (results.size() != requests.size()) { + state.pendingModelResponse = response; + throw PortException.configuration("tool exchange is incomplete and cannot be formatted"); + } + List messages; + try { + messages = effects.conversation.formatToolExchange(response, results); + } catch (PortException e) { + state.pendingModelResponse = response; + throw e; + } + if (messages != null) { + state.messages.addAll(messages); + } + return results; + } + + /** + * Run one tool request end to end: ask, record the answer, execute, and classify the outcome. + * + *

A denial is not an engine failure — it is returned as a failed tool result so the model sees + * it and can adapt, which is what lets a host decline an action without aborting the turn. + */ + private ModelToolResult executeTool( + TurnState state, + String invocationId, + ModelToolRequest request, + CancellationToken cancellation) { + Map requestPayload = new LinkedHashMap<>(); + requestPayload.put("toolRequest", save(request)); + emit( + state, + EngineEventKind.PERMISSION_REQUESTED, + invocationId, + state.iteration, + requestPayload); + + EnginePermissionDecision decision; + try { + decision = effects.permission.authorize(request, cancellation); + } catch (PortException e) { + throw new ToolPermissionFailed(e); + } + + Map resolvedPayload = new LinkedHashMap<>(); + resolvedPayload.put("toolRequestId", request.id); + resolvedPayload.put("decision", save(decision)); + emit( + state, EngineEventKind.PERMISSION_RESOLVED, invocationId, state.iteration, resolvedPayload); + + if (decision.approved == null || !decision.approved) { + Object declaredKind = decision.metadata == null ? null : decision.metadata.get("errorKind"); + String errorKind = + declaredKind instanceof String text && !text.isEmpty() ? text : "permission_denied"; + ModelToolResult denied = new ModelToolResult(); + denied.requestId = request.id; + denied.name = request.name; + denied.outcome = ModelToolOutcome.FAILED; + denied.output = decision.reason == null ? "Permission denied" : decision.reason; + denied.errorKind = errorKind; + denied.metadata = decision.metadata; + return denied; + } + + if (cancellation.isCancelled()) { + throw new ToolCancelled(); + } + + Map startedPayload = new LinkedHashMap<>(); + startedPayload.put("toolRequest", save(request)); + emit( + state, + EngineEventKind.TOOL_EXECUTION_STARTED, + invocationId, + state.iteration, + startedPayload); + if (cancellation.isCancelled()) { + throw new ToolCancelled(); + } + + try { + return effects.tools.execute(request, cancellation); + } catch (PortException error) { + if (error.configurationError()) { + throw new ToolConfigurationFailed(error); + } + // A tool that simply failed is model-visible data, not an engine fault: the model is + // given the failure and gets to decide what to do about it. + ModelToolResult result = new ModelToolResult(); + result.requestId = request.id; + result.name = request.name; + result.outcome = + error.outcomeUnknown() ? ModelToolOutcome.INDETERMINATE : ModelToolOutcome.FAILED; + result.output = + error.outcomeUnknown() + ? "Tool '" + + request.name + + "' outcome is unknown and requires reconciliation: " + + error.getMessage() + : "Tool '" + request.name + "' failed: " + error.getMessage(); + result.errorKind = error.outcomeUnknown() ? "effect_outcome_unknown" : "tool_error"; + return result; + } + } + + private EngineCheckpoint persistPolicyUpdate( + TurnState state, String invocationId, Map metadata) { + long sequence = state.sequence + 1; + // Set before building the checkpoint so a resumed run knows the rewrite already happened. + state.policyAppliedForIteration = true; + EngineCheckpoint checkpoint = buildCheckpoint(state, sequence, true); + Map payload = new LinkedHashMap<>(); + payload.put("messages", saveMessages(state.messages)); + payload.put("stablePrefixMessages", state.stablePrefixMessages); + payload.put("metadata", metadata); + EngineEvent event = + buildEvent( + state, + sequence, + EngineEventKind.POLICY_APPLIED, + invocationId, + state.iteration, + payload); + appendWithCheckpoint( + state, "host policy", invocationId, checkpoint, List.of(event, checkpointEvent(state, checkpoint, invocationId))); + state.sequence = sequence + 1; + return checkpoint; + } + + private EngineCheckpoint persistToolExchange( + TurnState state, String invocationId, List results) { + long sequence = state.sequence; + List events = new ArrayList<>(); + for (ModelToolResult result : results) { + sequence++; + Map payload = new LinkedHashMap<>(); + payload.put("toolResult", save(result)); + events.add( + buildEvent( + state, + sequence, + EngineEventKind.TOOL_RESULT_COMMITTED, + invocationId, + state.iteration, + payload)); + } + sequence++; + Map conversationPayload = new LinkedHashMap<>(); + conversationPayload.put("messageCount", state.messages.size()); + events.add( + buildEvent( + state, + sequence, + EngineEventKind.CONVERSATION_UPDATED, + invocationId, + state.iteration, + conversationPayload)); + EngineCheckpoint checkpoint = buildCheckpoint(state, sequence, false); + events.add(checkpointEvent(state, checkpoint, invocationId)); + appendWithCheckpoint(state, "tool exchange", invocationId, checkpoint, events); + state.sequence = checkpoint.lastSequence + 1; + return checkpoint; + } + + private EngineCheckpoint persistModelReconciliationRequired( + TurnState state, String invocationId) { + long sequence = state.sequence + 1; + EngineCheckpoint checkpoint = buildCheckpoint(state, sequence, false); + EngineEvent event = + buildEvent( + state, + sequence, + EngineEventKind.MODEL_RECONCILIATION_REQUIRED, + invocationId, + state.iteration, + save(state.modelReconciliation)); + appendWithCheckpoint( + state, + "model reconciliation", + invocationId, + checkpoint, + List.of(event, checkpointEvent(state, checkpoint, invocationId))); + state.sequence = sequence + 1; + return checkpoint; + } + + private EngineCheckpoint persistModelReconciliation( + TurnState state, + String invocationId, + ModelReconciliationState reconciliation, + ModelInvocationResponse response) { + long sequence = state.sequence + 1; + EngineCheckpoint checkpoint = buildCheckpoint(state, sequence, false); + Map payload = new LinkedHashMap<>(); + payload.put("reconciliation", save(reconciliation)); + payload.put("hasOutput", response.output != null); + payload.put("toolRequests", response.toolRequests == null ? 0 : response.toolRequests.size()); + payload.put("metadata", response.metadata); + EngineEvent event = + buildEvent( + state, + sequence, + EngineEventKind.MODEL_INVOCATION_RECONCILED, + invocationId, + state.iteration, + payload); + appendWithCheckpoint( + state, + "model reconciliation resolution", + invocationId, + checkpoint, + List.of(event, checkpointEvent(state, checkpoint, invocationId))); + state.sequence = sequence + 1; + return checkpoint; + } + + private EngineCheckpoint persistModelResponse( + TurnState state, String invocationId, ModelInvocationResponse response) { + long sequence = state.sequence + 1; + EngineCheckpoint checkpoint = buildCheckpoint(state, sequence, false); + Map payload = new LinkedHashMap<>(); + payload.put("hasOutput", response.output != null); + payload.put("toolRequests", response.toolRequests == null ? 0 : response.toolRequests.size()); + payload.put( + "nextPortability", + response.nextContextState == null || response.nextContextState.portability == null + ? null + : response.nextContextState.portability.value); + payload.put( + "delegatedState", + response.nextContextState == null + ? null + : saveReferences(response.nextContextState.delegatedState)); + payload.put("metadata", response.metadata); + EngineEvent event = + buildEvent( + state, + sequence, + EngineEventKind.MODEL_INVOCATION_COMPLETED, + invocationId, + state.iteration, + payload); + appendWithCheckpoint( + state, + "model response", + invocationId, + checkpoint, + List.of(event, checkpointEvent(state, checkpoint, invocationId))); + state.sequence = sequence + 1; + return checkpoint; + } + + private EngineCheckpoint persistToolResult( + TurnState state, String invocationId, ModelToolRequest request) { + long sequence = state.sequence + 1; + EngineCheckpoint checkpoint = buildCheckpoint(state, sequence, false); + ModelToolResult result = state.toolResults.get(state.toolResults.size() - 1); + Map payload = new LinkedHashMap<>(); + payload.put("toolResult", save(result)); + EngineEvent event = + buildEvent( + state, + sequence, + EngineEventKind.TOOL_EXECUTION_COMPLETED, + invocationId, + state.iteration, + payload); + appendWithCheckpoint( + state, + "tool result", + request.id, + checkpoint, + List.of(event, checkpointEvent(state, checkpoint, invocationId))); + state.sequence = sequence + 1; + return checkpoint; + } + + private EngineCheckpoint persistReconciliation(TurnState state, ModelToolResult result) { + long sequence = state.sequence + 1; + EngineCheckpoint checkpoint = buildCheckpoint(state, sequence, false); + String invocationId = + state.activeInvocationId == null ? "reconciliation" : state.activeInvocationId; + Map payload = new LinkedHashMap<>(); + payload.put("toolResult", save(result)); + EngineEvent event = + buildEvent( + state, + sequence, + EngineEventKind.TOOL_RESULT_RECONCILED, + invocationId, + state.iteration, + payload); + appendWithCheckpoint( + state, + "tool reconciliation", + result.requestId, + checkpoint, + List.of(event, checkpointEvent(state, checkpoint, invocationId))); + state.sequence = sequence + 1; + return checkpoint; + } + + private void appendWithCheckpoint( + TurnState state, + String stage, + String requestId, + EngineCheckpoint checkpoint, + List events) { + try { + effects.durability.appendWithCheckpoint(events, checkpoint); + } catch (PortException source) { + throw new TurnEngineException.RecoveryRequired( + stage, requestId, checkpoint, new ArrayList<>(state.toolResults), source); + } + } + + private EngineEvent checkpointEvent( + TurnState state, EngineCheckpoint checkpoint, String invocationId) { + Map payload = new LinkedHashMap<>(); + payload.put("checkpointId", checkpoint.id); + payload.put("includedThroughSequence", checkpoint.lastSequence); + return buildEvent( + state, + checkpoint.lastSequence + 1, + EngineEventKind.CHECKPOINT_CREATED, + invocationId, + checkpoint.iteration, + payload); + } + + private EngineCheckpoint buildCheckpoint( + TurnState state, long lastSequence, boolean resumeSameIteration) { + EngineCheckpoint checkpoint = new EngineCheckpoint(); + checkpoint.id = effects.ids.nextId("checkpoint"); + checkpoint.sessionId = state.sessionId; + checkpoint.turnId = state.turnId; + checkpoint.runId = state.runId; + checkpoint.parentRunId = state.parentRunId; + checkpoint.delegationDepth = state.delegationDepth; + checkpoint.iteration = state.iteration; + checkpoint.lastSequence = lastSequence; + checkpoint.messages = new ArrayList<>(state.messages); + checkpoint.stablePrefixMessages = state.stablePrefixMessages; + checkpoint.inputs = state.inputs; + checkpoint.activeInvocationId = state.activeInvocationId; + checkpoint.pendingToolRequests = new ArrayList<>(state.pendingToolRequests); + checkpoint.completedToolResults = new ArrayList<>(state.toolResults); + checkpoint.completedModelIterations = state.completedModelIterations; + // An indeterminate result blocks resumption even if the flag has not been raised yet: + // the checkpoint is written before the engine reaches its own reconciliation branch. + boolean lastIsIndeterminate = + !state.toolResults.isEmpty() + && state.toolResults.get(state.toolResults.size() - 1).outcome + == ModelToolOutcome.INDETERMINATE; + checkpoint.reconciliationRequired = state.reconciliationRequired || lastIsIndeterminate; + checkpoint.modelReconciliation = state.modelReconciliation; + checkpoint.pendingOutput = state.pendingOutput; + checkpoint.finalOutputReady = state.finalOutputReady; + checkpoint.pendingModelResponse = state.pendingModelResponse; + checkpoint.resumeSameIteration = resumeSameIteration; + checkpoint.policyAppliedForIteration = state.policyAppliedForIteration; + checkpoint.contextState = currentContextState(state); + return checkpoint; + } + + private static InvocationContextState currentContextState(TurnState state) { + InvocationContextState contextState = new InvocationContextState(); + contextState.portability = state.portability; + contextState.delegatedState = new ArrayList<>(state.delegatedState); + return contextState; + } + + private TurnEngineResult applyFinalPolicy(TurnState state, CancellationToken cancellation) { + if (cancellation.isCancelled()) { + return commitCancelled(state, cancellation); + } + FinalOutputPolicyRequest request = new FinalOutputPolicyRequest(); + request.sessionId = state.sessionId; + request.turnId = state.turnId; + request.iteration = state.iteration; + request.messages = new ArrayList<>(state.messages); + request.output = state.output; + request.inputs = state.inputs; + + FinalOutputPolicyResult result; + try { + result = effects.policy.beforeCommit(request, cancellation); + } catch (HostPolicyException e) { + if (cancellation.isCancelled()) { + return commitCancelled(state, cancellation); + } + return commitFailed(state, e.errorKind(), e.getMessage(), cancellation); + } + if (cancellation.isCancelled()) { + return commitCancelled(state, cancellation); + } + state.output = result.output; + return commit(state, EngineTurnStatus.SUCCESS, EngineEventKind.TURN_COMMITTED, cancellation); + } + + private TurnEngineResult commitCancelled(TurnState state, CancellationToken cancellation) { + return commit(state, EngineTurnStatus.CANCELLED, EngineEventKind.TURN_CANCELLED, cancellation); + } + + private TurnEngineResult commitFailed( + TurnState state, String errorKind, String message, CancellationToken cancellation) { + state.output = errorOutput(errorKind, message); + return commit(state, EngineTurnStatus.FAILED, EngineEventKind.TURN_FAILED, cancellation); + } + + private TurnEngineResult commitReconciliation( + TurnState state, String errorKind, String message, CancellationToken cancellation) { + state.output = errorOutput(errorKind, message); + return commit( + state, + EngineTurnStatus.RECONCILIATION_REQUIRED, + EngineEventKind.TURN_RECONCILIATION_REQUIRED, + cancellation); + } + + private static Map errorOutput(String errorKind, String message) { + Map output = new LinkedHashMap<>(); + output.put("errorKind", errorKind); + output.put("message", message); + return output; + } + + /** + * Emit the terminal event, build the commit, and — only on success — run the post-commit effect. + * + *

Post-commit failures are deliberately non-fatal. The turn is already committed by the time it + * runs, so reporting a failure as the turn's outcome would misrepresent what happened; it is + * returned alongside the commit instead. + */ + private TurnEngineResult commit( + TurnState state, + EngineTurnStatus status, + EngineEventKind kind, + CancellationToken cancellation) { + int iteration = state.iteration; + Map terminalPayload = new LinkedHashMap<>(); + terminalPayload.put("status", status.value); + terminalPayload.put("output", state.output); + emit(state, kind, null, iteration, terminalPayload); + + TurnCommit commit = new TurnCommit(); + commit.sessionId = state.sessionId; + commit.turnId = state.turnId; + commit.status = status; + commit.output = state.output; + commit.messages = new ArrayList<>(state.messages); + commit.iterations = state.completedModelIterations; + commit.lastSequence = state.sequence; + commit.contextState = currentContextState(state); + commit.modelReconciliation = state.modelReconciliation; + + String postCommitError = null; + if (status == EngineTurnStatus.SUCCESS) { + // Length-prefixed so no pair of session and turn identifiers can collide into the + // same effect id, which is what makes the effect idempotent across resumes. + String effectId = + "post_commit:" + + commit.sessionId.length() + + ":" + + commit.sessionId + + ":" + + commit.turnId.length() + + ":" + + commit.turnId; + Map effectPayload = new LinkedHashMap<>(); + effectPayload.put("effectId", effectId); + try { + emit(state, EngineEventKind.POST_COMMIT_STARTED, null, iteration, effectPayload); + try { + effects.postCommit.afterCommit(effectId, commit, cancellation); + try { + emit(state, EngineEventKind.POST_COMMIT_COMPLETED, null, iteration, effectPayload); + } catch (TurnEngineException e) { + postCommitError = + "post-commit effect '" + + effectId + + "' completed, but its completion event could not be persisted: " + + e.getMessage(); + } + } catch (PortException source) { + String message = source.getMessage(); + Map failedPayload = new LinkedHashMap<>(); + failedPayload.put("effectId", effectId); + failedPayload.put("message", message); + try { + emit(state, EngineEventKind.POST_COMMIT_FAILED, null, iteration, failedPayload); + postCommitError = message; + } catch (TurnEngineException e) { + postCommitError = + message + + "; failure event for post-commit effect '" + + effectId + + "' could not be persisted: " + + e.getMessage(); + } + } + } catch (TurnEngineException e) { + postCommitError = + "post-commit effect '" + + effectId + + "' was not started because its start event could not be persisted: " + + e.getMessage(); + } + } + // Re-read: post-commit events advance the journal past where the commit was built. + commit.lastSequence = state.sequence; + + TurnEngineResult result = new TurnEngineResult(); + result.commit = commit; + result.snapshots = new ArrayList<>(state.snapshots); + result.toolResults = new ArrayList<>(state.toolResults); + result.postCommitError = postCommitError; + return result; + } + + private void emit( + TurnState state, + EngineEventKind kind, + String invocationId, + Integer iteration, + Object payload) { + long sequence = state.sequence + 1; + EngineEvent event = buildEvent(state, sequence, kind, invocationId, iteration, payload); + try { + effects.durability.append(event); + } catch (PortException source) { + throw new TurnEngineException.Port("event journal", source); + } + state.sequence = sequence; + } + + private EngineEvent buildEvent( + TurnState state, + long sequence, + EngineEventKind kind, + String invocationId, + Integer iteration, + Object payload) { + EngineEvent event = new EngineEvent(); + event.sequence = sequence; + event.id = effects.ids.nextId("event"); + event.timestamp = effects.clock.now(); + event.sessionId = state.sessionId; + event.turnId = state.turnId; + event.runId = state.runId; + event.parentRunId = state.parentRunId; + event.delegationDepth = state.delegationDepth; + event.invocationId = invocationId; + event.iteration = iteration; + event.kind = kind; + event.payload = payload; + return event; + } + + private static Map save(ModelInvocationContextSnapshot value) { + return value == null ? null : value.save(new SaveContext()); + } + + private static Map save(ModelToolRequest value) { + return value == null ? null : value.save(new SaveContext()); + } + + private static Map save(ModelToolResult value) { + return value == null ? null : value.save(new SaveContext()); + } + + private static Map save(EnginePermissionDecision value) { + return value == null ? null : value.save(new SaveContext()); + } + + private static Map save(ModelReconciliationState value) { + return value == null ? null : value.save(new SaveContext()); + } + + private static List> saveMessages(List values) { + if (values == null) { + return null; + } + SaveContext context = new SaveContext(); + List> saved = new ArrayList<>(values.size()); + for (Message value : values) { + saved.add(value.save(context)); + } + return saved; + } + + private static List> saveReferences(List values) { + if (values == null) { + return null; + } + SaveContext context = new SaveContext(); + List> saved = new ArrayList<>(values.size()); + for (DelegatedStateReference value : values) { + saved.add(value.save(context)); + } + return saved; + } + + /** The turn was cancelled part-way through a tool request. */ + private static final class ToolCancelled extends RuntimeException { + private static final long serialVersionUID = 1L; + + ToolCancelled() { + super(null, null, false, false); + } + } + + /** The permission port itself failed, as distinct from denying the request. */ + private static final class ToolPermissionFailed extends RuntimeException { + private static final long serialVersionUID = 1L; + + ToolPermissionFailed(PortException cause) { + super(cause.getMessage(), cause, false, false); + } + } + + /** The tool request is malformed, so neither retrying nor the model can recover. */ + private static final class ToolConfigurationFailed extends RuntimeException { + private static final long serialVersionUID = 1L; + + ToolConfigurationFailed(PortException cause) { + super(cause.getMessage(), cause, false, false); + } + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/TurnEngineEffects.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/TurnEngineEffects.java new file mode 100644 index 000000000..5121ac21b --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/TurnEngineEffects.java @@ -0,0 +1,78 @@ +package com.microsoft.prompty.engine; + +/** + * The bundle of effect ports one {@link TurnEngine} drives. + * + *

Every field defaults to a neutral implementation, so a host overrides only the effects it + * actually cares about. Wiring just a {@link Ports.ModelPort} is enough to run a real turn. + */ +public final class TurnEngineEffects { + + public Ports.ModelPort model; + public Ports.ModelStreamPort stream = new DefaultPorts.NoopModelStream(); + public Ports.HostPolicyPort policy = new DefaultPorts.NoopHostPolicy(); + public Ports.RetryPolicyPort retry = new DefaultPorts.NoopRetryPolicy(); + public Ports.ConversationPort conversation = new DefaultPorts.DefaultConversation(); + public Ports.PermissionPort permission = new DefaultPorts.AllowAllPermissions(); + public Ports.ToolPort tools; + public Ports.DurabilityPort durability = new DefaultPorts.NoopDurability(); + public Ports.PostCommitPort postCommit = new DefaultPorts.NoopPostCommit(); + public Ports.Clock clock = new DefaultPorts.SystemClock(); + public Ports.IdGenerator ids = new DefaultPorts.SequentialIds(); + + public static TurnEngineEffects of(Ports.ModelPort model) { + TurnEngineEffects effects = new TurnEngineEffects(); + effects.model = model; + return effects; + } + + public TurnEngineEffects withStream(Ports.ModelStreamPort stream) { + this.stream = stream; + return this; + } + + public TurnEngineEffects withPolicy(Ports.HostPolicyPort policy) { + this.policy = policy; + return this; + } + + public TurnEngineEffects withRetry(Ports.RetryPolicyPort retry) { + this.retry = retry; + return this; + } + + public TurnEngineEffects withConversation(Ports.ConversationPort conversation) { + this.conversation = conversation; + return this; + } + + public TurnEngineEffects withPermission(Ports.PermissionPort permission) { + this.permission = permission; + return this; + } + + public TurnEngineEffects withTools(Ports.ToolPort tools) { + this.tools = tools; + return this; + } + + public TurnEngineEffects withDurability(Ports.DurabilityPort durability) { + this.durability = durability; + return this; + } + + public TurnEngineEffects withPostCommit(Ports.PostCommitPort postCommit) { + this.postCommit = postCommit; + return this; + } + + public TurnEngineEffects withClock(Ports.Clock clock) { + this.clock = clock; + return this; + } + + public TurnEngineEffects withIds(Ports.IdGenerator ids) { + this.ids = ids; + return this; + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/TurnEngineException.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/TurnEngineException.java new file mode 100644 index 000000000..b7c886995 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/TurnEngineException.java @@ -0,0 +1,97 @@ +package com.microsoft.prompty.engine; + +import com.microsoft.prompty.model.EngineCheckpoint; +import com.microsoft.prompty.model.ModelToolResult; +import java.util.List; + +/** + * A failure that prevents the engine from producing a committed turn. + * + *

Most problems do not surface as one of these. A model that keeps failing, a tool that + * throws, a policy that rejects the input — those all produce a committed turn with a {@code failed} + * status, because the host still needs the journal and the conversation. These exceptions are for + * the cases where committing is itself impossible. + */ +public abstract class TurnEngineException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + protected TurnEngineException(String message, Throwable cause) { + super(message, cause); + } + + /** The request cannot start a turn at all. */ + public static final class InvalidRequest extends TurnEngineException { + private static final long serialVersionUID = 1L; + + public InvalidRequest(String message) { + super("invalid turn request: " + message, null); + } + } + + /** A port failed at a point where the engine cannot record a decision. */ + public static final class Port extends TurnEngineException { + private static final long serialVersionUID = 1L; + + private final String stage; + + public Port(String stage, PortException cause) { + super(stage + " failed: " + cause.getMessage(), cause); + this.stage = stage; + } + + public String stage() { + return stage; + } + } + + /** + * An effect succeeded but the durable record of it did not. + * + *

This is the dangerous case, so it carries everything a host needs to recover: the checkpoint + * that failed to persist and the tool results accumulated so far. Without them the effect is + * invisible to a resumed run, which would then execute it a second time. + */ + public static final class RecoveryRequired extends TurnEngineException { + private static final long serialVersionUID = 1L; + + private final String stage; + private final String requestId; + private final transient EngineCheckpoint checkpoint; + private final transient List toolResults; + + public RecoveryRequired( + String stage, + String requestId, + EngineCheckpoint checkpoint, + List toolResults, + PortException cause) { + super( + stage + " durability failed after effect '" + requestId + "': " + cause.getMessage(), + cause); + this.stage = stage; + this.requestId = requestId; + this.checkpoint = checkpoint; + this.toolResults = toolResults; + } + + public String stage() { + return stage; + } + + /** The effect whose durable record is missing. */ + public String requestId() { + return requestId; + } + + /** The checkpoint that could not be persisted; the host must store it to resume safely. */ + public EngineCheckpoint checkpoint() { + return checkpoint; + } + + /** Tool results accumulated up to the failure. */ + public List toolResults() { + return toolResults; + } + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/TurnEngineRequest.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/TurnEngineRequest.java new file mode 100644 index 000000000..82fc35555 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/TurnEngineRequest.java @@ -0,0 +1,355 @@ +package com.microsoft.prompty.engine; + +import com.microsoft.prompty.Messages; +import com.microsoft.prompty.model.DelegatedStateReference; +import com.microsoft.prompty.model.EngineCheckpoint; +import com.microsoft.prompty.model.InvocationContextPortability; +import com.microsoft.prompty.model.Message; +import com.microsoft.prompty.model.ModelInvocationResponse; +import com.microsoft.prompty.model.ModelReconciliationState; +import com.microsoft.prompty.model.ModelToolOutcome; +import com.microsoft.prompty.model.ModelToolRequest; +import com.microsoft.prompty.model.ModelToolResult; +import com.microsoft.prompty.model.ResumeContext; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * The input to one canonical turn engine run. + * + *

This carries not just what to run but where to run from, which is what makes a turn resumable. + * A fresh turn supplies messages and budgets; a resumed turn additionally supplies the iteration, + * journal sequence, pending tool queue, and pending model response recovered from a checkpoint, so + * the engine picks up exactly where the interrupted run stopped rather than repeating committed + * effects. + * + *

Hand-written rather than generated: this is a runtime-local entry shape, not a durable + * cross-runtime contract. The durable shapes it is built from — {@link EngineCheckpoint} and {@link + * ResumeContext} — are generated. + */ +public final class TurnEngineRequest { + + public String sessionId = ""; + public String turnId = ""; + + /** Stable identifier for this run. Empty means the engine assigns one at run start. */ + public String runId = ""; + + /** The parent run this run was delegated from, or null for a top-level run. */ + public String parentRunId; + + /** Zero-based delegation nesting depth; 0 for a top-level run. */ + public int delegationDepth; + + public List messages = new ArrayList<>(); + public Object inputs; + public int maxIterations = 10; + public int maxModelAttempts = 3; + + /** The iteration to execute first. Non-zero when resuming a checkpoint. */ + public int startIteration; + + /** The last committed event sequence before this run. */ + public long initialSequence; + + public int stablePrefixMessages; + public InvocationContextPortability portability = InvocationContextPortability.PORTABLE; + public List delegatedState = new ArrayList<>(); + public String activeInvocationId; + public List pendingToolRequests = new ArrayList<>(); + public List completedToolResults = new ArrayList<>(); + public int completedModelIterations; + public boolean reconciliationRequired; + public ModelReconciliationState modelReconciliation; + public Object pendingOutput; + public boolean finalOutputReady; + public ModelInvocationResponse pendingModelResponse; + public boolean policyAppliedForIteration; + public ModelToolResult reconciliationResolution; + public ModelInvocationResponse modelReconciliationResolution; + + /** + * A fresh turn over the supplied conversation. + * + *

The whole conversation starts as the stable prefix: nothing has been appended yet, so every + * message is eligible for provider-side prefix caching. + */ + public static TurnEngineRequest of(String sessionId, String turnId, List messages) { + TurnEngineRequest request = new TurnEngineRequest(); + request.sessionId = sessionId; + request.turnId = turnId; + request.messages = messages == null ? new ArrayList<>() : new ArrayList<>(messages); + request.stablePrefixMessages = request.messages.size(); + return request; + } + + /** Mark this run as delegated from a parent run, nesting one level deeper than the parent. */ + public TurnEngineRequest delegatedUnder(String parentRunId, int parentDelegationDepth) { + this.parentRunId = parentRunId; + this.delegationDepth = + parentDelegationDepth == Integer.MAX_VALUE + ? Integer.MAX_VALUE + : parentDelegationDepth + 1; + return this; + } + + /** Set the stable run identifier. An empty value lets the engine assign one. */ + public TurnEngineRequest withRunId(String runId) { + this.runId = runId; + return this; + } + + /** + * Resume from a checkpoint, continuing a journal whose tail may extend past it. + * + *

Choosing the iteration is the subtle part. A checkpoint that explicitly asks to resume in + * place does so. A checkpoint with nothing outstanding — no pending tools, no pending model + * response, no ready output, no reconciliation — finished its iteration, so the run advances. + * Anything else still has work inside the recorded iteration and resumes there. + */ + public static TurnEngineRequest resumeFrom( + EngineCheckpoint checkpoint, int maxIterations, long lastJournalSequence) { + return resumeFrom( + checkpoint, maxIterations, lastJournalSequence, isTrue(checkpoint.reconciliationRequired)); + } + + /** + * The resume body, parameterised on whether reconciliation is still outstanding. + * + *

The reconciliation entry points resolve the blocking effect first and only then work out + * where to resume, so they pass {@code false} here: a checkpoint whose sole outstanding item was + * the effect just resolved has in fact finished its iteration, and resuming in place would rerun + * it. + */ + private static TurnEngineRequest resumeFrom( + EngineCheckpoint checkpoint, + int maxIterations, + long lastJournalSequence, + boolean reconciliationRequired) { + TurnEngineRequest request = new TurnEngineRequest(); + request.sessionId = checkpoint.sessionId; + request.turnId = checkpoint.turnId; + request.runId = checkpoint.runId == null ? "" : checkpoint.runId; + request.parentRunId = checkpoint.parentRunId; + request.delegationDepth = orZero(checkpoint.delegationDepth); + request.messages = + checkpoint.messages == null ? new ArrayList<>() : new ArrayList<>(checkpoint.messages); + request.stablePrefixMessages = orZero(checkpoint.stablePrefixMessages); + request.inputs = checkpoint.inputs; + request.maxIterations = maxIterations; + request.maxModelAttempts = 3; + + int iteration = orZero(checkpoint.iteration); + boolean nothingOutstanding = + isEmpty(checkpoint.pendingToolRequests) + && checkpoint.pendingModelResponse == null + && !isTrue(checkpoint.finalOutputReady) + && !reconciliationRequired; + if (isTrue(checkpoint.resumeSameIteration)) { + request.startIteration = iteration; + } else if (nothingOutstanding) { + request.startIteration = iteration + 1; + } else { + request.startIteration = iteration; + } + + long checkpointSequence = checkpoint.lastSequence == null ? 0L : checkpoint.lastSequence; + request.initialSequence = Math.max(lastJournalSequence, checkpointSequence); + if (checkpoint.contextState != null) { + request.portability = + checkpoint.contextState.portability == null + ? InvocationContextPortability.PORTABLE + : checkpoint.contextState.portability; + request.delegatedState = + checkpoint.contextState.delegatedState == null + ? new ArrayList<>() + : new ArrayList<>(checkpoint.contextState.delegatedState); + } + request.activeInvocationId = checkpoint.activeInvocationId; + request.pendingToolRequests = copyOrEmpty(checkpoint.pendingToolRequests); + request.completedToolResults = copyOrEmpty(checkpoint.completedToolResults); + request.completedModelIterations = orZero(checkpoint.completedModelIterations); + request.reconciliationRequired = reconciliationRequired; + request.modelReconciliation = checkpoint.modelReconciliation; + request.pendingOutput = checkpoint.pendingOutput; + request.finalOutputReady = isTrue(checkpoint.finalOutputReady); + request.pendingModelResponse = checkpoint.pendingModelResponse; + request.policyAppliedForIteration = isTrue(checkpoint.policyAppliedForIteration); + return request; + } + + /** + * Resume after the host resolves an indeterminate tool effect. + * + *

The resolved result replaces the indeterminate one in the checkpoint, and, when the batch was + * already folded into the conversation, the tool message the model will read is rewritten too — + * otherwise the model would keep seeing the "outcome unknown" text the engine wrote when it gave + * up on the effect. + */ + public static TurnEngineRequest resumeAfterReconciliation( + EngineCheckpoint checkpoint, + int maxIterations, + long lastJournalSequence, + ModelToolResult resolvedResult) { + if (!isTrue(checkpoint.reconciliationRequired)) { + throw new TurnEngineException.InvalidRequest("checkpoint does not require reconciliation"); + } + if (checkpoint.modelReconciliation != null) { + throw new TurnEngineException.InvalidRequest( + "checkpoint requires model reconciliation, not tool reconciliation"); + } + if (resolvedResult.outcome == ModelToolOutcome.INDETERMINATE) { + throw new TurnEngineException.InvalidRequest( + "resolved tool result must have a determinate outcome"); + } + + List results = copyOrEmpty(checkpoint.completedToolResults); + int index = -1; + for (int i = 0; i < results.size(); i++) { + if (Objects.equals(results.get(i).requestId, resolvedResult.requestId)) { + index = i; + break; + } + } + if (index < 0) { + throw new TurnEngineException.InvalidRequest( + "checkpoint does not contain indeterminate tool request '" + + resolvedResult.requestId + + "'"); + } + if (results.get(index).outcome != ModelToolOutcome.INDETERMINATE) { + throw new TurnEngineException.InvalidRequest( + "tool request '" + resolvedResult.requestId + "' is already determinate"); + } + results.set(index, resolvedResult); + + List messages = copyOrEmpty(checkpoint.messages); + if (checkpoint.pendingModelResponse == null) { + int messageIndex = -1; + for (int i = 0; i < messages.size(); i++) { + Map metadata = Messages.metadata(messages.get(i)); + Object toolCallId = metadata == null ? null : metadata.get(Messages.TOOL_CALL_ID); + if (resolvedResult.requestId.equals(toolCallId)) { + messageIndex = i; + break; + } + } + if (messageIndex < 0) { + throw new TurnEngineException.InvalidRequest( + "checkpoint is missing the tool result message for '" + + resolvedResult.requestId + + "'"); + } + messages.set( + messageIndex, + Messages.toolResult(resolvedResult.requestId, ToolResults.modelText(resolvedResult))); + } + + TurnEngineRequest request = + resumeFrom(checkpoint, maxIterations, lastJournalSequence, false); + request.messages = messages; + request.completedToolResults = results; + request.reconciliationResolution = resolvedResult; + return request; + } + + /** Resume after the host resolves an indeterminate model invocation. */ + public static TurnEngineRequest resumeAfterModelReconciliation( + EngineCheckpoint checkpoint, + int maxIterations, + long lastJournalSequence, + ModelInvocationResponse resolvedResponse) { + if (!isTrue(checkpoint.reconciliationRequired)) { + throw new TurnEngineException.InvalidRequest("checkpoint does not require reconciliation"); + } + ModelReconciliationState reconciliation = checkpoint.modelReconciliation; + if (reconciliation == null) { + throw new TurnEngineException.InvalidRequest( + "checkpoint requires tool reconciliation, not model reconciliation"); + } + if (!Objects.equals(checkpoint.activeInvocationId, reconciliation.invocationId)) { + throw new TurnEngineException.InvalidRequest( + "model reconciliation identity does not match the active invocation"); + } + + TurnEngineRequest request = resumeFrom(checkpoint, maxIterations, lastJournalSequence); + request.startIteration = orZero(checkpoint.iteration); + request.reconciliationRequired = false; + request.modelReconciliationResolution = resolvedResponse; + return request; + } + + /** + * Resume from the durable {@link ResumeContext} record. + * + *

Unlike {@link #resumeFrom}, this threads the attempt budget from the persisted record rather + * than defaulting it, so a resumed run keeps the retry policy the original run was given. + */ + public static TurnEngineRequest fromResume(ResumeContext resume) { + TurnEngineRequest request = + resumeFrom(resume.checkpoint, maxIterationsOf(resume), resumeSequence(resume)); + applyResumeAttempts(request, resume); + return request; + } + + /** Resume from a {@link ResumeContext} after the host resolves an indeterminate tool effect. */ + public static TurnEngineRequest fromResumeAfterReconciliation( + ResumeContext resume, ModelToolResult resolvedResult) { + TurnEngineRequest request = + resumeAfterReconciliation( + resume.checkpoint, maxIterationsOf(resume), resumeSequence(resume), resolvedResult); + applyResumeAttempts(request, resume); + return request; + } + + /** Resume from a {@link ResumeContext} after the host resolves an indeterminate invocation. */ + public static TurnEngineRequest fromResumeAfterModelReconciliation( + ResumeContext resume, ModelInvocationResponse resolvedResponse) { + TurnEngineRequest request = + resumeAfterModelReconciliation( + resume.checkpoint, maxIterationsOf(resume), resumeSequence(resume), resolvedResponse); + applyResumeAttempts(request, resume); + return request; + } + + private static void applyResumeAttempts(TurnEngineRequest request, ResumeContext resume) { + if (resume.maxModelAttempts != null && resume.maxModelAttempts > 0) { + request.maxModelAttempts = resume.maxModelAttempts; + } + } + + private static int maxIterationsOf(ResumeContext resume) { + return Math.max(resume.maxIterations == null ? 0 : resume.maxIterations, 0); + } + + /** + * The journal position a resumed run continues from: the further of the checkpoint's own sequence + * and any journal tail written after it. + */ + private static long resumeSequence(ResumeContext resume) { + long journal = resume.lastJournalSequence == null ? 0L : resume.lastJournalSequence; + long checkpoint = + resume.checkpoint == null || resume.checkpoint.lastSequence == null + ? 0L + : resume.checkpoint.lastSequence; + return Math.max(Math.max(journal, checkpoint), 0L); + } + + private static boolean isTrue(Boolean value) { + return value != null && value; + } + + private static int orZero(Integer value) { + return value == null ? 0 : value; + } + + private static boolean isEmpty(List list) { + return list == null || list.isEmpty(); + } + + private static List copyOrEmpty(List list) { + return list == null ? new ArrayList<>() : new ArrayList<>(list); + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/TurnState.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/TurnState.java new file mode 100644 index 000000000..7cfe1f3a1 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/TurnState.java @@ -0,0 +1,153 @@ +package com.microsoft.prompty.engine; + +import com.microsoft.prompty.model.DelegatedStateReference; +import com.microsoft.prompty.model.InvocationContextPortability; +import com.microsoft.prompty.model.Message; +import com.microsoft.prompty.model.ModelInvocationContextSnapshot; +import com.microsoft.prompty.model.ModelInvocationResponse; +import com.microsoft.prompty.model.ModelReconciliationState; +import com.microsoft.prompty.model.ModelToolRequest; +import com.microsoft.prompty.model.ModelToolResult; +import java.util.ArrayList; +import java.util.List; + +/** + * Mutable state for one engine run. + * + *

Package-private on purpose: every field here is either recoverable from a checkpoint or + * derivable from the journal, so nothing outside the engine should be reading it directly. + */ +final class TurnState { + + String sessionId; + String turnId; + String runId; + String parentRunId; + int delegationDepth; + List messages; + Object inputs; + int maxIterations; + int maxModelAttempts; + int stablePrefixMessages; + InvocationContextPortability portability; + List delegatedState; + String activeInvocationId; + List pendingToolRequests; + boolean reconciliationRequired; + ModelReconciliationState modelReconciliation; + int completedModelIterations; + Object pendingOutput; + boolean finalOutputReady; + ModelInvocationResponse pendingModelResponse; + boolean policyAppliedForIteration; + ModelToolResult reconciliationResolution; + ModelInvocationResponse modelReconciliationResolution; + int iteration; + long sequence; + Object output; + final List snapshots = new ArrayList<>(); + List toolResults; + + TurnState(TurnEngineRequest request) { + sessionId = request.sessionId; + turnId = request.turnId; + runId = request.runId; + parentRunId = request.parentRunId; + delegationDepth = request.delegationDepth; + messages = new ArrayList<>(request.messages); + inputs = request.inputs; + maxIterations = request.maxIterations; + maxModelAttempts = request.maxModelAttempts; + stablePrefixMessages = request.stablePrefixMessages; + portability = + request.portability == null ? InvocationContextPortability.PORTABLE : request.portability; + delegatedState = + request.delegatedState == null ? new ArrayList<>() : new ArrayList<>(request.delegatedState); + activeInvocationId = request.activeInvocationId; + pendingToolRequests = + request.pendingToolRequests == null + ? new ArrayList<>() + : new ArrayList<>(request.pendingToolRequests); + iteration = request.startIteration; + sequence = request.initialSequence; + toolResults = + request.completedToolResults == null + ? new ArrayList<>() + : new ArrayList<>(request.completedToolResults); + reconciliationRequired = request.reconciliationRequired; + modelReconciliation = request.modelReconciliation; + completedModelIterations = request.completedModelIterations; + pendingOutput = request.pendingOutput; + finalOutputReady = request.finalOutputReady; + pendingModelResponse = request.pendingModelResponse; + policyAppliedForIteration = request.policyAppliedForIteration; + reconciliationResolution = request.reconciliationResolution; + modelReconciliationResolution = request.modelReconciliationResolution; + } + + /** + * Fold a model response into canonical state. + * + *

Returns a message describing why the response is unusable, or null on success. It is a + * message rather than an exception because the caller turns it into a committed failed turn, not + * into a thrown error. + * + *

The response is held back rather than appended when it requested tools: the assistant + * message and its tool results have to enter the conversation together, or a provider that + * validates pairing will reject the next request. + */ + String applyModelResponse(String invocationId, ModelInvocationResponse response) { + completedModelIterations++; + List toolRequests = + response.toolRequests == null ? new ArrayList<>() : new ArrayList<>(response.toolRequests); + if (toolRequests.isEmpty()) { + if (response.assistantMessages != null) { + messages.addAll(response.assistantMessages); + } + pendingModelResponse = null; + } else { + pendingModelResponse = response; + } + String error = applyProviderState(response); + if (error != null) { + return error; + } + activeInvocationId = invocationId; + pendingToolRequests = toolRequests; + pendingOutput = response.output; + finalOutputReady = pendingToolRequests.isEmpty(); + return null; + } + + /** + * Adopt the provider's view of where conversation state now lives. + * + *

A response that names a next state replaces ours outright. A response that stays silent while + * we are portable clears any stale references, because portable state by definition holds none. + * + *

The two rejections below are what keep a turn replayable: portable state pointing at + * provider-held data cannot be moved, and delegated state naming no data cannot be resumed. + */ + private String applyProviderState(ModelInvocationResponse response) { + if (response.nextContextState != null) { + portability = + response.nextContextState.portability == null + ? InvocationContextPortability.PORTABLE + : response.nextContextState.portability; + delegatedState = + response.nextContextState.delegatedState == null + ? new ArrayList<>() + : new ArrayList<>(response.nextContextState.delegatedState); + } else if (portability == InvocationContextPortability.PORTABLE) { + delegatedState.clear(); + } + + if (portability == InvocationContextPortability.PORTABLE && !delegatedState.isEmpty()) { + return "portable provider state cannot retain delegated references"; + } + if (portability == InvocationContextPortability.DELEGATED && delegatedState.isEmpty()) { + return "delegated provider state requires at least one reference"; + } + return null; + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/harness/AllowAllPermissionResolver.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/harness/AllowAllPermissionResolver.java new file mode 100644 index 000000000..1c66859d2 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/harness/AllowAllPermissionResolver.java @@ -0,0 +1,20 @@ +package com.microsoft.prompty.harness; + +import com.microsoft.prompty.model.PermissionDecision; +import com.microsoft.prompty.model.PermissionRequest; +import com.microsoft.prompty.model.PermissionResolver; + +/** + * Resolves every permission request as approved. + * + *

The right resolver for a batch job or a sandbox, and the wrong one for anything touching a + * user's machine. It records {@code allow_all} as the reason so a journal shows that nothing + * actually adjudicated the request. + */ +public final class AllowAllPermissionResolver implements PermissionResolver { + + @Override + public PermissionDecision request(PermissionRequest request) { + return Decisions.of(request, true, "allow_all"); + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/harness/CollectingEventSink.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/harness/CollectingEventSink.java new file mode 100644 index 000000000..70862cbbc --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/harness/CollectingEventSink.java @@ -0,0 +1,46 @@ +package com.microsoft.prompty.harness; + +import com.microsoft.prompty.model.EventSink; +import com.microsoft.prompty.model.SessionEvent; +import com.microsoft.prompty.model.TurnEvent; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Captures emitted turn and session events in memory. + * + *

The obvious sink for a test, but also the one a host reaches for while wiring an agent up: it + * makes the engine's event stream inspectable without a file or a subscriber. + */ +public final class CollectingEventSink implements EventSink { + + private final List turnEvents = Collections.synchronizedList(new ArrayList<>()); + private final List sessionEvents = Collections.synchronizedList(new ArrayList<>()); + + @Override + public Boolean emitTurn(TurnEvent turnEvent) { + turnEvents.add(turnEvent); + return true; + } + + @Override + public Boolean emitSession(SessionEvent sessionEvent) { + sessionEvents.add(sessionEvent); + return true; + } + + /** The turn events emitted so far, in order. */ + public List turnEvents() { + synchronized (turnEvents) { + return List.copyOf(turnEvents); + } + } + + /** The session events emitted so far, in order. */ + public List sessionEvents() { + synchronized (sessionEvents) { + return List.copyOf(sessionEvents); + } + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/harness/Decisions.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/harness/Decisions.java new file mode 100644 index 000000000..e33dfe2a0 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/harness/Decisions.java @@ -0,0 +1,22 @@ +package com.microsoft.prompty.harness; + +import com.microsoft.prompty.model.PermissionDecision; +import com.microsoft.prompty.model.PermissionRequest; + +/** Builds the decision a resolver hands back, echoing the request it answers. */ +final class Decisions { + + private Decisions() {} + + static PermissionDecision of(PermissionRequest request, boolean approved, String reason) { + PermissionDecision decision = new PermissionDecision(); + decision.requestId = request.requestId; + decision.toolCallId = request.toolCallId; + // Echoing the permission back matters: a decision that names no permission cannot be audited + // against the request that produced it. + decision.permission = request.permission; + decision.approved = approved; + decision.reason = reason; + return decision; + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/harness/DenyAllPermissionResolver.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/harness/DenyAllPermissionResolver.java new file mode 100644 index 000000000..2343b83b7 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/harness/DenyAllPermissionResolver.java @@ -0,0 +1,19 @@ +package com.microsoft.prompty.harness; + +import com.microsoft.prompty.model.PermissionDecision; +import com.microsoft.prompty.model.PermissionRequest; +import com.microsoft.prompty.model.PermissionResolver; + +/** + * Resolves every permission request as denied. + * + *

Useful for proving that a turn survives refusal — the denial becomes a tool result the model + * can react to, not an error that ends the turn. + */ +public final class DenyAllPermissionResolver implements PermissionResolver { + + @Override + public PermissionDecision request(PermissionRequest request) { + return Decisions.of(request, false, "deny_all"); + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/harness/FunctionHostToolExecutor.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/harness/FunctionHostToolExecutor.java new file mode 100644 index 000000000..99692d369 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/harness/FunctionHostToolExecutor.java @@ -0,0 +1,83 @@ +package com.microsoft.prompty.harness; + +import com.microsoft.prompty.model.HostToolExecutor; +import com.microsoft.prompty.model.HostToolRequest; +import com.microsoft.prompty.model.HostToolResult; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.BiFunction; + +/** + * Dispatches host tool requests to registered local functions. + * + *

A handler that throws, and a tool name nobody registered, both come back as an unsuccessful + * {@link HostToolResult} rather than an exception. That is deliberate: the model asked for + * something and is owed an answer it can read and react to. Throwing would end the turn over a + * failure the model could have recovered from, and would leave the tool call unanswered in the + * conversation, which most providers reject outright on the next request. + */ +public final class FunctionHostToolExecutor implements HostToolExecutor { + + /** A tool implementation: arguments and the originating request in, a JSON-shaped result out. */ + public interface Handler extends BiFunction, HostToolRequest, Object> {} + + private final Map handlers; + + public FunctionHostToolExecutor() { + this(Map.of()); + } + + public FunctionHostToolExecutor(Map handlers) { + this.handlers = Map.copyOf(handlers); + } + + /** A copy of this executor with {@code handler} registered under {@code name}. */ + public FunctionHostToolExecutor with(String name, Handler handler) { + Map merged = new LinkedHashMap<>(handlers); + merged.put(name, handler); + return new FunctionHostToolExecutor(merged); + } + + @Override + public HostToolResult execute(HostToolRequest request) { + long started = System.nanoTime(); + Handler handler = handlers.get(request.toolName); + if (handler == null) { + return failure( + request, + started, + "not_found", + "No host tool registered for '" + request.toolName + "'"); + } + + Map arguments = request.arguments == null ? Map.of() : request.arguments; + try { + Object result = handler.apply(arguments, request); + HostToolResult success = base(request, started); + success.success = true; + success.result = result; + return success; + } catch (RuntimeException e) { + String message = e.getMessage() == null ? e.toString() : e.getMessage(); + return failure(request, started, "exception", message); + } + } + + private static HostToolResult failure( + HostToolRequest request, long started, String errorKind, String message) { + HostToolResult result = base(request, started); + result.success = false; + result.result = new LinkedHashMap<>(Map.of("message", message)); + result.errorKind = errorKind; + return result; + } + + private static HostToolResult base(HostToolRequest request, long started) { + HostToolResult result = new HostToolResult(); + result.requestId = request.requestId; + result.toolCallId = request.toolCallId; + result.toolName = request.toolName; + result.durationMs = (System.nanoTime() - started) / 1_000_000.0; + return result; + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/harness/InMemoryCheckpointStore.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/harness/InMemoryCheckpointStore.java new file mode 100644 index 000000000..03fbbdffd --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/harness/InMemoryCheckpointStore.java @@ -0,0 +1,51 @@ +package com.microsoft.prompty.harness; + +import com.microsoft.prompty.model.Checkpoint; +import com.microsoft.prompty.model.CheckpointStore; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Stores checkpoints in memory, keyed by session and checkpoint identifier. + * + *

Listing is sorted by checkpoint id rather than by insertion, so a caller resuming a session + * sees the same order whichever store implementation the host swapped in. + */ +public final class InMemoryCheckpointStore implements CheckpointStore { + + private record Key(String sessionId, String checkpointId) {} + + private final Map checkpoints = new LinkedHashMap<>(); + + @Override + public synchronized Checkpoint save(Checkpoint checkpoint) { + if (checkpoint.sessionId == null) { + throw new IllegalArgumentException("Checkpoint session_id is required"); + } + if (checkpoint.id == null) { + throw new IllegalArgumentException("Checkpoint id is required"); + } + checkpoints.put(new Key(checkpoint.sessionId, checkpoint.id), checkpoint); + return checkpoint; + } + + @Override + public synchronized Checkpoint load(String sessionId, String checkpointId) { + return checkpoints.get(new Key(sessionId, checkpointId)); + } + + @Override + public synchronized List listCheckpoints(String sessionId) { + List found = new ArrayList<>(); + for (Map.Entry entry : checkpoints.entrySet()) { + if (entry.getKey().sessionId().equals(sessionId)) { + found.add(entry.getValue()); + } + } + found.sort(Comparator.comparing(checkpoint -> checkpoint.id, Comparator.nullsFirst(Comparator.naturalOrder()))); + return found; + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/harness/JsonlEventJournalWriter.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/harness/JsonlEventJournalWriter.java new file mode 100644 index 000000000..c55b4e7ee --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/harness/JsonlEventJournalWriter.java @@ -0,0 +1,110 @@ +package com.microsoft.prompty.harness; + +import com.microsoft.prompty.model.EventJournalWriter; +import com.microsoft.prompty.model.SaveContext; +import com.microsoft.prompty.model.SessionEvent; +import com.microsoft.prompty.model.SessionSummary; +import com.microsoft.prompty.model.TurnEvent; +import com.microsoft.prompty.model.TypraJson; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Appends replayable journal records as newline-delimited JSON. + * + *

One record per line means a journal is still readable after a crash mid-write, and a replay + * can be graded by comparing lines rather than parsing a whole document that may never have been + * closed. + * + *

Every method reports success rather than throwing. Journalling is observation: a turn that + * cannot be written down still happened, and failing the turn because of it would trade a + * recoverable gap in the record for an unrecoverable loss of work. + */ +public final class JsonlEventJournalWriter implements EventJournalWriter { + + private final Path path; + private final Object lock = new Object(); + private boolean closed; + + public JsonlEventJournalWriter(Path path) { + this.path = path; + Path parent = path.getParent(); + if (parent != null) { + try { + Files.createDirectories(parent); + } catch (IOException ignored) { + // Reported by the first append that fails; there is nothing useful to do here. + } + } + } + + /** The file this writer appends to. */ + public Path path() { + return path; + } + + @Override + public Boolean appendTurn(TurnEvent turnEvent) { + Map record = new LinkedHashMap<>(); + record.put("kind", "turn"); + record.put("event", turnEvent.save(new SaveContext())); + return write(record); + } + + @Override + public Boolean appendSession(SessionEvent sessionEvent) { + Map record = new LinkedHashMap<>(); + record.put("kind", "session"); + record.put("event", sessionEvent.save(new SaveContext())); + return write(record); + } + + @Override + public Boolean close(SessionSummary summary) { + synchronized (lock) { + if (closed) { + return false; + } + if (summary != null) { + Map record = new LinkedHashMap<>(); + record.put("kind", "summary"); + record.put("summary", summary.save(new SaveContext())); + if (!append(record)) { + return false; + } + } + closed = true; + return true; + } + } + + private boolean write(Map record) { + synchronized (lock) { + if (closed) { + return false; + } + return append(record); + } + } + + private boolean append(Map record) { + try { + // Always LF, never the platform separator: Rust's writeln! emits \n, and the journal is + // compared across runtimes. + Files.writeString( + path, + TypraJson.stringify(record) + "\n", + StandardCharsets.UTF_8, + StandardOpenOption.CREATE, + StandardOpenOption.APPEND); + return true; + } catch (IOException e) { + return false; + } + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/harness/ReferenceReplayVerifier.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/harness/ReferenceReplayVerifier.java new file mode 100644 index 000000000..5c45d25b6 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/harness/ReferenceReplayVerifier.java @@ -0,0 +1,61 @@ +package com.microsoft.prompty.harness; + +import com.microsoft.prompty.model.ReplayJournalRecord; +import com.microsoft.prompty.model.ReplayMismatch; +import com.microsoft.prompty.model.ReplayVerificationRequest; +import com.microsoft.prompty.model.ReplayVerificationResult; +import com.microsoft.prompty.model.ReplayVerificationStatus; +import com.microsoft.prompty.model.SaveContext; +import com.microsoft.prompty.model.TypraJson; +import java.util.ArrayList; +import java.util.List; + +/** + * Verifies normalized replay journal records. + * + *

Comparison is positional and reports every divergence rather than stopping at the + * first. A replay that drifts usually drifts once and then stays shifted, so a report naming only + * the first mismatch tends to describe the symptom rather than the cause; seeing the whole shape of + * the divergence is what tells you whether a record was inserted, dropped, or merely changed. + * + *

Records are compared by their saved shape, not by identity, so a record rebuilt from a journal + * file compares equal to the one the engine emitted. + */ +public final class ReferenceReplayVerifier { + + public ReplayVerificationResult verify(ReplayVerificationRequest request) { + List expected = request.expected == null ? List.of() : request.expected; + List actual = request.actual == null ? List.of() : request.actual; + int max = Math.max(expected.size(), actual.size()); + + List mismatches = new ArrayList<>(); + for (int index = 0; index < max; index++) { + ReplayJournalRecord expectedRecord = index < expected.size() ? expected.get(index) : null; + ReplayJournalRecord actualRecord = index < actual.size() ? actual.get(index) : null; + if (comparable(expectedRecord).equals(comparable(actualRecord))) { + continue; + } + ReplayMismatch mismatch = new ReplayMismatch(); + mismatch.index = index; + mismatch.expected = expectedRecord; + mismatch.actual = actualRecord; + mismatch.message = + expectedRecord == null + ? "Unexpected extra replay record" + : actualRecord == null ? "Missing replay record" : "Replay record mismatch"; + mismatches.add(mismatch); + } + + ReplayVerificationResult result = new ReplayVerificationResult(); + result.status = + mismatches.isEmpty() ? ReplayVerificationStatus.PASSED : ReplayVerificationStatus.FAILED; + result.expectedCount = expected.size(); + result.actualCount = actual.size(); + result.mismatches = mismatches; + return result; + } + + private static String comparable(ReplayJournalRecord record) { + return record == null ? "\u0000absent" : TypraJson.stringify(record.save(new SaveContext())); + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/harness/ReferenceTurnRunner.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/harness/ReferenceTurnRunner.java new file mode 100644 index 000000000..91fc2c251 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/harness/ReferenceTurnRunner.java @@ -0,0 +1,730 @@ +package com.microsoft.prompty.harness; + +import com.microsoft.prompty.CancellationToken; +import com.microsoft.prompty.engine.ContextPipeline; +import com.microsoft.prompty.engine.PortException; +import com.microsoft.prompty.engine.Ports; +import com.microsoft.prompty.engine.TurnEngine; +import com.microsoft.prompty.engine.TurnEngineEffects; +import com.microsoft.prompty.engine.TurnEngineRequest; +import com.microsoft.prompty.model.Checkpoint; +import com.microsoft.prompty.model.CheckpointStore; +import com.microsoft.prompty.model.EngineCheckpoint; +import com.microsoft.prompty.model.EngineEvent; +import com.microsoft.prompty.model.EngineEventKind; +import com.microsoft.prompty.model.EnginePermissionDecision; +import com.microsoft.prompty.model.EngineTurnStatus; +import com.microsoft.prompty.model.EventJournalWriter; +import com.microsoft.prompty.model.EventSink; +import com.microsoft.prompty.model.HostToolExecutor; +import com.microsoft.prompty.model.HostToolRequest; +import com.microsoft.prompty.model.HostToolResult; +import com.microsoft.prompty.model.LoadContext; +import com.microsoft.prompty.model.ModelInvocationRequest; +import com.microsoft.prompty.model.ModelInvocationResponse; +import com.microsoft.prompty.model.ModelToolOutcome; +import com.microsoft.prompty.model.ModelToolRequest; +import com.microsoft.prompty.model.ModelToolResult; +import com.microsoft.prompty.model.PermissionDecision; +import com.microsoft.prompty.model.PermissionRequest; +import com.microsoft.prompty.model.PermissionResolver; +import com.microsoft.prompty.model.RunTurnRequest; +import com.microsoft.prompty.model.RunTurnResult; +import com.microsoft.prompty.model.RunTurnStatus; +import com.microsoft.prompty.model.SaveContext; +import com.microsoft.prompty.model.SessionEvent; +import com.microsoft.prompty.model.SessionEventType; +import com.microsoft.prompty.model.SessionSummary; +import com.microsoft.prompty.model.SessionSummaryStatus; +import com.microsoft.prompty.model.TurnEvent; +import com.microsoft.prompty.model.TurnEventType; +import com.microsoft.prompty.model.TurnModelRequest; +import com.microsoft.prompty.model.TurnModelResponse; +import com.microsoft.prompty.model.TurnOptions; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Function; + +/** + * Runs one turn through the canonical engine against the host-facing protocol types. + * + *

The engine speaks in ports; a host speaks in event sinks, journals, checkpoint stores, + * permission resolvers, and tool executors. This runner is the translation between the two, and it + * exists so that a host implementing the five documented protocols gets the real engine — the same + * loop, the same durability ordering, the same cancellation semantics — rather than a simplified + * one written for convenience. + * + *

It is also what the shared replay vectors grade. Because the clock and id generator are + * injected, a run produces a byte-identical journal every time, which is what makes cross-runtime + * comparison meaningful. + */ +public final class ReferenceTurnRunner { + + /** Invokes the model for one iteration. */ + public interface ModelCallback extends Function {} + + private final EventSink eventSink; + private final EventJournalWriter journal; + private final CheckpointStore checkpointStore; + private final PermissionResolver permissionResolver; + private final HostToolExecutor hostToolExecutor; + private final ModelCallback invokeModel; + private final Ports.Clock clock; + private final Ports.IdGenerator ids; + + public ReferenceTurnRunner( + EventSink eventSink, + EventJournalWriter journal, + CheckpointStore checkpointStore, + PermissionResolver permissionResolver, + HostToolExecutor hostToolExecutor, + ModelCallback invokeModel, + Ports.Clock clock, + Ports.IdGenerator ids) { + this.eventSink = eventSink; + this.journal = journal; + this.checkpointStore = checkpointStore; + this.permissionResolver = permissionResolver; + this.hostToolExecutor = hostToolExecutor; + this.invokeModel = invokeModel; + this.clock = clock; + this.ids = ids; + } + + /** Run one turn to completion. */ + public RunTurnResult run(RunTurnRequest request) { + TurnOptions options = request.options == null ? new TurnOptions() : request.options; + int maxIterations = Math.max(0, options.maxIterations == null ? 10 : options.maxIterations); + Map inputs = request.inputs == null ? Map.of() : request.inputs; + + State state = new State(); + Durability durability = new Durability(state, options, inputs); + + TurnEngineEffects effects = + TurnEngineEffects.of(new Model(state, options, inputs)) + .withPermission(new Permission(state)) + .withTools(new Tools(state)) + .withDurability(durability) + .withClock(clock) + .withIds(ids); + + TurnEngineRequest engineRequest = + TurnEngineRequest.of(request.sessionId, request.turnId, List.of()); + engineRequest.inputs = inputs; + engineRequest.maxIterations = maxIterations; + // A zero-iteration turn has nothing left to decide, so the engine must not wait for a model to + // declare the output final. + engineRequest.finalOutputReady = maxIterations == 0; + // The reference model callback is a plain function: a second attempt would call it again with + // the same arguments and get the same answer, so retrying only hides the failure. + engineRequest.maxModelAttempts = 1; + + var engineResult = new TurnEngine(ContextPipeline.appendOnly(), effects).run(engineRequest, new CancellationToken()); + + if (state.adapterFailure != null) { + throw new IllegalStateException(state.adapterFailure); + } + + RunTurnStatus status = + switch (engineResult.commit.status) { + case SUCCESS -> RunTurnStatus.SUCCESS; + case CANCELLED -> RunTurnStatus.CANCELLED; + default -> RunTurnStatus.ERROR; + }; + + Object output = engineResult.commit.output; + if (status == RunTurnStatus.ERROR && "max_iterations".equals(errorKind(output))) { + // The engine reports the reason it stopped; a caller wants the message it can show. The + // reason survives on the error event either way. + output = new LinkedHashMap<>(Map.of("message", "Maximum turn iterations reached")); + } + + SessionSummary summary = new SessionSummary(); + summary.sessionId = request.sessionId; + summary.status = + status == RunTurnStatus.SUCCESS ? SessionSummaryStatus.SUCCESS : SessionSummaryStatus.ERROR; + summary.turns = 1; + summary.checkpoints = state.checkpoints.size(); + journal.close(summary); + + RunTurnResult result = new RunTurnResult(); + result.sessionId = request.sessionId; + result.turnId = request.turnId; + result.status = status; + result.output = output; + result.iterations = engineResult.commit.iterations; + result.toolResults = List.copyOf(state.allResults); + result.checkpoints = List.copyOf(state.checkpoints); + return result; + } + + /** State shared between the port adapters for the duration of one run. */ + private static final class State { + /** Results the model has not yet been shown. Drained on the next invocation. */ + final List pendingResults = new ArrayList<>(); + + /** Every result produced during the turn, for the caller. */ + final List allResults = new ArrayList<>(); + + final List checkpoints = new ArrayList<>(); + + /** Permission requests projected from events, keyed by engine tool request id. */ + final Map permissionRequests = new ConcurrentHashMap<>(); + + /** + * The first adapter failure, if any. + * + *

Held rather than thrown so the engine can finish unwinding: a port that throws mid-turn + * would skip the durability writes that explain what went wrong. + */ + volatile String adapterFailure; + + synchronized void fail(String message) { + if (adapterFailure == null) { + adapterFailure = message; + } + } + + synchronized List drainPending() { + List drained = List.copyOf(pendingResults); + pendingResults.clear(); + return drained; + } + + synchronized void record(HostToolResult result) { + pendingResults.add(result); + allResults.add(result); + } + + synchronized HostToolResult pendingFor(String requestId) { + for (HostToolResult result : pendingResults) { + if (requestId.equals(result.requestId)) { + return result; + } + } + return null; + } + } + + private final class Model implements Ports.ModelPort { + private final State state; + private final TurnOptions options; + private final Map inputs; + + Model(State state, TurnOptions options, Map inputs) { + this.state = state; + this.options = options; + this.inputs = inputs; + } + + @Override + public ModelInvocationResponse invoke( + ModelInvocationRequest request, CancellationToken cancellation, Ports.ModelStreamPort stream) { + TurnModelRequest modelRequest = new TurnModelRequest(); + modelRequest.sessionId = request.context.sessionId; + modelRequest.turnId = request.context.turnId; + modelRequest.iteration = request.context.iteration; + modelRequest.inputs = inputs; + modelRequest.options = options; + modelRequest.toolResults = state.drainPending(); + + TurnModelResponse response; + try { + response = invokeModel.apply(modelRequest); + } catch (RuntimeException e) { + String message = "reference model callback: " + describe(e); + state.fail(message); + throw PortException.of(message); + } + + ModelInvocationResponse invocation = new ModelInvocationResponse(); + invocation.output = response.output; + invocation.assistantMessages = List.of(); + + List toolRequests = new ArrayList<>(); + List hosts = response.toolRequests == null ? List.of() : response.toolRequests; + for (int index = 0; index < hosts.size(); index++) { + HostToolRequest host = hosts.get(index); + ModelToolRequest tool = new ModelToolRequest(); + tool.id = + firstNonNull( + host.requestId, + host.toolCallId, + "reference-tool-" + request.context.iteration + "-" + index); + tool.name = host.toolName; + tool.arguments = host.arguments; + // The engine works in its own tool vocabulary, so the host request rides along in metadata + // rather than being reconstructed later from fields the engine may have normalized. + tool.metadata = new LinkedHashMap<>(Map.of("hostToolRequest", host.save(new SaveContext()))); + toolRequests.add(tool); + } + invocation.toolRequests = toolRequests; + invocation.metadata = + new LinkedHashMap<>(Map.of("referenceResponse", response.save(new SaveContext()))); + return invocation; + } + } + + private final class Permission implements Ports.PermissionPort { + private final State state; + + Permission(State state) { + this.state = state; + } + + @Override + public EnginePermissionDecision authorize( + ModelToolRequest request, CancellationToken cancellation) { + PermissionRequest permission = state.permissionRequests.get(request.id); + if (permission == null) { + throw PortException.configuration( + "permission request '" + request.id + "' was not projected"); + } + PermissionDecision decision; + try { + decision = permissionResolver.request(permission); + } catch (RuntimeException e) { + String message = "reference permission resolver: " + describe(e); + state.fail(message); + throw PortException.of(message); + } + + if (!Boolean.TRUE.equals(decision.approved)) { + // A refusal is an answer, not an error: the model asked to run something and is told no, in + // the same shape a tool failure would take, so it can adapt rather than stall. + HostToolRequest host = hostRequest(request); + HostToolResult denied = new HostToolResult(); + denied.requestId = host.requestId == null ? request.id : host.requestId; + denied.toolCallId = host.toolCallId; + denied.toolName = host.toolName; + denied.success = false; + denied.result = + new LinkedHashMap<>( + Map.of("message", decision.reason == null ? "Permission denied" : decision.reason)); + denied.errorKind = "permission_denied"; + state.record(denied); + } + + EnginePermissionDecision engineDecision = new EnginePermissionDecision(); + engineDecision.approved = decision.approved; + engineDecision.reason = decision.reason; + engineDecision.metadata = + new LinkedHashMap<>(Map.of("permissionDecision", decision.save(new SaveContext()))); + return engineDecision; + } + } + + private final class Tools implements Ports.ToolPort { + private final State state; + + Tools(State state) { + this.state = state; + } + + @Override + public ModelToolResult execute(ModelToolRequest request, CancellationToken cancellation) { + HostToolRequest host = hostRequest(request); + HostToolResult result; + try { + result = hostToolExecutor.execute(host); + } catch (RuntimeException e) { + String message = "reference host tool executor: " + describe(e); + state.fail(message); + throw PortException.configuration(message); + } + state.record(result); + + ModelToolResult engineResult = new ModelToolResult(); + engineResult.requestId = request.id; + engineResult.name = request.name; + engineResult.outcome = + Boolean.TRUE.equals(result.success) ? ModelToolOutcome.SUCCESS : ModelToolOutcome.FAILED; + engineResult.output = result.result; + engineResult.errorKind = result.errorKind; + engineResult.metadata = + new LinkedHashMap<>(Map.of("hostToolResult", result.save(new SaveContext()))); + return engineResult; + } + } + + private final class Durability implements Ports.DurabilityPort { + private final State state; + private final TurnOptions options; + private final Map inputs; + + Durability(State state, TurnOptions options, Map inputs) { + this.state = state; + this.options = options; + this.inputs = inputs; + } + + @Override + public void append(EngineEvent event) { + project(event); + } + + @Override + public void appendWithCheckpoint(List events, EngineCheckpoint checkpoint) { + for (EngineEvent event : events) { + project(event); + if (event.kind == EngineEventKind.MODEL_INVOCATION_COMPLETED + || event.kind == EngineEventKind.MODEL_INVOCATION_RECONCILED) { + saveModelCheckpoint(event); + } + } + + if (shouldRecordMessagesUpdated(events, checkpoint)) { + List results = new ArrayList<>(); + synchronized (state) { + for (HostToolResult result : state.pendingResults) { + results.add(result.save(new SaveContext())); + } + } + recordTurn( + TurnEventType.MESSAGES_UPDATED, + checkpoint.turnId, + checkpoint.iteration == null ? 0 : checkpoint.iteration, + new LinkedHashMap<>(Map.of("toolResults", results))); + } + } + + private void project(EngineEvent event) { + int iteration = event.iteration == null ? 0 : event.iteration; + Map payload = asMap(event.payload); + + switch (event.kind) { + case TURN_STARTED -> { + recordSession( + SessionEventType.SESSION_START, + event.sessionId, + event.turnId, + map("sessionId", event.sessionId, "schemaVersion", "1")); + Map turnStart = new LinkedHashMap<>(); + turnStart.put("inputs", payload.getOrDefault("inputs", inputs)); + turnStart.put("maxIterations", payload.get("maxIterations")); + recordTurn(TurnEventType.TURN_START, event.turnId, 0, turnStart); + } + case MODEL_INVOCATION_STARTED -> + recordTurn( + TurnEventType.LLM_START, + event.turnId, + iteration, + map("attempt", payload.get("attempt"))); + case MODEL_INVOCATION_COMPLETED, MODEL_INVOCATION_RECONCILED -> + recordTurn(TurnEventType.LLM_COMPLETE, event.turnId, iteration, new LinkedHashMap<>()); + case PERMISSION_REQUESTED -> { + ModelToolRequest toolRequest = + ModelToolRequest.load(payload.get("toolRequest"), new LoadContext()); + HostToolRequest host = hostRequest(toolRequest); + PermissionRequest permission = buildPermissionRequest(host); + state.permissionRequests.put(toolRequest.id, permission); + recordTurn( + TurnEventType.PERMISSION_REQUESTED, + event.turnId, + iteration, + permission.save(new SaveContext())); + } + case PERMISSION_RESOLVED -> { + Object decision = + asMap(asMap(asMap(payload.get("decision")).get("metadata")).get("permissionDecision")); + recordTurn(TurnEventType.PERMISSION_COMPLETED, event.turnId, iteration, decision); + } + case TOOL_EXECUTION_STARTED -> { + ModelToolRequest toolRequest = + ModelToolRequest.load(payload.get("toolRequest"), new LoadContext()); + recordTurn( + TurnEventType.TOOL_EXECUTION_START, + event.turnId, + iteration, + hostRequest(toolRequest).save(new SaveContext())); + } + case TOOL_EXECUTION_COMPLETED -> { + ModelToolResult toolResult = + ModelToolResult.load(payload.get("toolResult"), new LoadContext()); + HostToolResult host = hostResultOrNull(toolResult); + if (host == null) { + // A result the host never produced has nothing to report; the engine's own event + // already records that the execution finished. + return; + } + recordTurn( + TurnEventType.TOOL_EXECUTION_COMPLETE, + event.turnId, + iteration, + host.save(new SaveContext())); + } + case TOOL_RESULT_COMMITTED -> { + ModelToolResult toolResult = + ModelToolResult.load(payload.get("toolResult"), new LoadContext()); + HostToolResult host = hostResult(toolResult); + recordTurn( + TurnEventType.TOOL_RESULT, event.turnId, iteration, host.save(new SaveContext())); + } + case TURN_COMMITTED, TURN_FAILED, TURN_CANCELLED, TURN_RECONCILIATION_REQUIRED -> + projectTurnEnd(event, payload); + default -> { + // The remaining engine events are internal bookkeeping with no host-facing counterpart. + } + } + } + + private void projectTurnEnd(EngineEvent event, Map payload) { + if (state.adapterFailure != null) { + // The run is already doomed and the caller will be told why. Writing a tidy end record here + // would claim the turn concluded normally. + return; + } + int iterations = state.checkpoints.size(); + RunTurnStatus status = + switch (event.kind) { + case TURN_COMMITTED -> RunTurnStatus.SUCCESS; + case TURN_CANCELLED -> RunTurnStatus.CANCELLED; + default -> RunTurnStatus.ERROR; + }; + + Object output = payload.get("output"); + Object errorPayload = output; + if ("max_iterations".equals(errorKind(output))) { + errorPayload = + map("errorKind", "max_iterations", "message", "Maximum turn iterations reached"); + output = map("message", "Maximum turn iterations reached"); + } + + if (event.kind == EngineEventKind.TURN_FAILED) { + recordTurn(TurnEventType.ERROR, event.turnId, iterations, errorPayload); + } + recordTurn( + TurnEventType.TURN_END, + event.turnId, + iterations, + map("iterations", iterations, "status", status.value, "response", output)); + recordSession( + SessionEventType.SESSION_END, + event.sessionId, + event.turnId, + map("sessionId", event.sessionId, "status", status.value, "reason", "turn_complete")); + } + + private void saveModelCheckpoint(EngineEvent event) { + int iteration = event.iteration == null ? 0 : event.iteration; + Map payload = asMap(event.payload); + TurnModelResponse response = + TurnModelResponse.load( + asMap(payload.get("metadata")).get("referenceResponse"), new LoadContext()); + + Map checkpointState = new LinkedHashMap<>(); + checkpointState.put("iteration", iteration); + checkpointState.put("output", response.output); + List toolRequests = new ArrayList<>(); + if (response.toolRequests != null) { + for (HostToolRequest request : response.toolRequests) { + toolRequests.add(request.save(new SaveContext())); + } + } + checkpointState.put("toolRequests", toolRequests); + if (response.checkpointState != null) { + // The model gets the last word on its own resumable state; the fields above are what the + // runner needs to reconstruct a turn, not a claim about what the model considers durable. + checkpointState.putAll(response.checkpointState); + } + + Checkpoint checkpoint = new Checkpoint(); + checkpoint.id = event.turnId + "-checkpoint-" + iteration; + checkpoint.sessionId = event.sessionId; + checkpoint.turnId = event.turnId; + checkpoint.checkpointNumber = iteration + 1; + checkpoint.title = "Turn " + event.turnId + " iteration " + iteration; + checkpoint.state = checkpointState; + checkpoint.createdAt = clock.now(); + + Checkpoint saved; + try { + saved = checkpointStore.save(checkpoint); + } catch (RuntimeException e) { + throw PortException.of("reference checkpoint store: " + describe(e)); + } + synchronized (state) { + state.checkpoints.add(saved); + } + recordSession( + SessionEventType.CHECKPOINT_CREATED, + event.sessionId, + event.turnId, + map("checkpointId", saved.id, "checkpointNumber", saved.checkpointNumber)); + } + + private PermissionRequest buildPermissionRequest(HostToolRequest request) { + PermissionRequest permission = new PermissionRequest(); + permission.requestId = + request.requestId == null ? ids.nextId("permission") : request.requestId + "-permission"; + permission.toolCallId = request.toolCallId; + permission.permission = "tool.execute"; + permission.target = request.toolName; + permission.details = request.save(new SaveContext()); + return permission; + } + + private HostToolResult hostResult(ModelToolResult result) { + HostToolResult host = hostResultOrNull(result); + if (host != null) { + return host; + } + // A denied request never reached the executor, so the engine result carries no host metadata; + // the refusal we already recorded is the answer. + HostToolResult pending = state.pendingFor(result.requestId); + if (pending == null) { + throw PortException.of("engine tool result is missing hostToolResult metadata"); + } + return pending; + } + + private void recordTurn(TurnEventType type, String turnId, int iteration, Object payload) { + TurnEvent event = new TurnEvent(); + event.id = ids.nextId("turn-event"); + event.type = type; + event.timestamp = clock.now(); + event.turnId = turnId; + event.iteration = iteration; + event.payload = asMap(payload); + eventSink.emitTurn(event); + journal.appendTurn(event); + } + + private void recordSession( + SessionEventType type, String sessionId, String turnId, Object payload) { + SessionEvent event = new SessionEvent(); + event.id = ids.nextId("session-event"); + event.type = type; + event.timestamp = clock.now(); + event.sessionId = sessionId; + event.turnId = turnId; + event.payload = asMap(payload); + eventSink.emitSession(event); + journal.appendSession(event); + } + } + + /** A clock that always reports the same instant, so a replay is byte-identical. */ + public static Ports.Clock fixedClock(String timestamp) { + return () -> timestamp; + } + + /** An id generator that numbers each kind from one, so a replay is byte-identical. */ + public static Ports.IdGenerator sequentialIds() { + AtomicLong counter = new AtomicLong(); + return kind -> kind + "-" + counter.incrementAndGet(); + } + + /** + * Whether a batch of engine events concluded something the host should be told its messages + * changed for. + * + *

Two independent reasons, because they mean different things. A conversation update says the + * message list moved. A finished tool round says every request the model made has an answer and + * nothing is outstanding — which is the moment a host can safely render or persist the thread. + * + *

Today's engine emits a conversation update alongside every tool commit, so the second arm + * never decides the outcome on its own. It is kept because deriving "the round is over" from "the + * engine happened to also say the messages changed" would break silently the day the engine + * batches those events differently, and a host that had been notified would simply stop being + * notified. + */ + static boolean shouldRecordMessagesUpdated(List events, EngineCheckpoint checkpoint) { + if (anyKind(events, EngineEventKind.CONVERSATION_UPDATED)) { + return true; + } + boolean toolRoundEnded = + anyKind(events, EngineEventKind.TOOL_EXECUTION_COMPLETED) + || anyKind(events, EngineEventKind.TOOL_RESULT_COMMITTED); + return toolRoundEnded + && isEmpty(checkpoint.pendingToolRequests) + && checkpoint.pendingModelResponse == null; + } + + private static HostToolRequest hostRequest(ModelToolRequest request) { + Object raw = request.metadata == null ? null : request.metadata.get("hostToolRequest"); + if (raw == null) { + throw PortException.configuration("engine tool request is missing hostToolRequest metadata"); + } + return HostToolRequest.load(raw, new LoadContext()); + } + + private static HostToolResult hostResultOrNull(ModelToolResult result) { + Object raw = result.metadata == null ? null : result.metadata.get("hostToolResult"); + return raw == null ? null : HostToolResult.load(raw, new LoadContext()); + } + + private static String errorKind(Object output) { + Object kind = asMap(output).get("errorKind"); + return kind instanceof String text ? text : null; + } + + private static boolean anyKind(List events, EngineEventKind kind) { + for (EngineEvent event : events) { + if (event.kind == kind) { + return true; + } + } + return false; + } + + private static boolean isEmpty(List values) { + return values == null || values.isEmpty(); + } + + @SuppressWarnings("unchecked") + private static Map asMap(Object value) { + if (value instanceof Map map) { + return (Map) map; + } + return new LinkedHashMap<>(); + } + + private static Map map(Object... pairs) { + Map result = new LinkedHashMap<>(); + for (int i = 0; i < pairs.length; i += 2) { + result.put(String.valueOf(pairs[i]), pairs[i + 1]); + } + return result; + } + + /** + * Returns the first non-null value. Rust chains {@code Option::or_else}, which only falls through + * on {@code None} — an explicitly empty id is used verbatim rather than skipped. + */ + private static String firstNonNull(String... values) { + for (String value : values) { + if (value != null) { + return value; + } + } + return ""; + } + + private static String describe(Throwable error) { + return error.getMessage() == null ? error.toString() : error.getMessage(); + } + + /** Builder-free convenience for the common wiring. */ + public static ReferenceTurnRunner of( + EventSink eventSink, + EventJournalWriter journal, + CheckpointStore checkpointStore, + PermissionResolver permissionResolver, + HostToolExecutor hostToolExecutor, + ModelCallback invokeModel) { + return new ReferenceTurnRunner( + eventSink, + journal, + checkpointStore, + permissionResolver, + hostToolExecutor, + invokeModel, + new com.microsoft.prompty.engine.DefaultPorts.SystemClock(), + sequentialIds()); + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AiResourceInfo.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AiResourceInfo.java new file mode 100644 index 000000000..260768528 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AiResourceInfo.java @@ -0,0 +1,147 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class AiResourceInfo { + public static final String SHORTHAND_PROPERTY = null; + + public String name = ""; + public String kind = ""; + public String endpoint = ""; + public String location = ""; + public String resourceGroup = ""; + public String serviceUrl = null; + + public AiResourceInfo() { } + + @SuppressWarnings("unchecked") + public static AiResourceInfo load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new AiResourceInfo()); + } + AiResourceInfo result = new AiResourceInfo(); + AiResourceInfo.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(AiResourceInfo result, Map map, LoadContext ctx) { + if (map.containsKey("name") && map.get("name") != null) { + result.name = String.valueOf(map.get("name")); + } + if (map.containsKey("kind") && map.get("kind") != null) { + result.kind = String.valueOf(map.get("kind")); + } + if (map.containsKey("endpoint") && map.get("endpoint") != null) { + result.endpoint = String.valueOf(map.get("endpoint")); + } + if (map.containsKey("location") && map.get("location") != null) { + result.location = String.valueOf(map.get("location")); + } + if (map.containsKey("resourceGroup") && map.get("resourceGroup") != null) { + result.resourceGroup = String.valueOf(map.get("resourceGroup")); + } + if (map.containsKey("serviceUrl") && map.get("serviceUrl") != null) { + result.serviceUrl = String.valueOf(map.get("serviceUrl")); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + AiResourceInfo obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.name != null) result.put("name", serializeScalar(obj.name)); + if (obj.kind != null) result.put("kind", serializeScalar(obj.kind)); + if (obj.endpoint != null) result.put("endpoint", serializeScalar(obj.endpoint)); + if (obj.location != null) result.put("location", serializeScalar(obj.location)); + if (obj.resourceGroup != null) result.put("resourceGroup", serializeScalar(obj.resourceGroup)); + if (obj.serviceUrl != null) result.put("serviceUrl", serializeScalar(obj.serviceUrl)); + return ctx.processDict(result); + } + + public Map toWire(String provider) { + Map result = new LinkedHashMap<>(); + String target = provider == null ? "" : provider; + { + String wireName = "name"; + boolean include = target.isEmpty(); + if (target.equals("foundry")) { wireName = "name"; include = true; } + if (include && this.name != null) result.put(wireName, serializeScalar(this.name)); + } + { + String wireName = "kind"; + boolean include = target.isEmpty(); + if (target.equals("foundry")) { wireName = "kind"; include = true; } + if (include && this.kind != null) result.put(wireName, serializeScalar(this.kind)); + } + { + String wireName = "endpoint"; + boolean include = target.isEmpty(); + if (target.equals("foundry")) { wireName = "endpoint"; include = true; } + if (include && this.endpoint != null) result.put(wireName, serializeScalar(this.endpoint)); + } + { + String wireName = "location"; + boolean include = target.isEmpty(); + if (target.equals("foundry")) { wireName = "location"; include = true; } + if (include && this.location != null) result.put(wireName, serializeScalar(this.location)); + } + { + String wireName = "resourceGroup"; + boolean include = target.isEmpty(); + if (target.equals("foundry")) { wireName = "resource_group"; include = true; } + if (include && this.resourceGroup != null) result.put(wireName, serializeScalar(this.resourceGroup)); + } + { + String wireName = "serviceUrl"; + boolean include = target.isEmpty(); + if (target.equals("foundry")) { wireName = "foundry_url"; include = true; } + if (include && this.serviceUrl != null) result.put(wireName, serializeScalar(this.serviceUrl)); + } + return result; + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static AiResourceInfo fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static AiResourceInfo fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static AiResourceInfo fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static AiResourceInfo fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnonymousConnection.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnonymousConnection.java new file mode 100644 index 000000000..86f156e67 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnonymousConnection.java @@ -0,0 +1,83 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class AnonymousConnection extends Connection { + public static final String SHORTHAND_PROPERTY = null; + + public String endpoint = ""; + + public AnonymousConnection() { + this.kind = "anonymous"; + } + + @SuppressWarnings("unchecked") + public static AnonymousConnection load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new AnonymousConnection()); + } + AnonymousConnection result = new AnonymousConnection(); + AnonymousConnection.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(AnonymousConnection result, Map map, LoadContext ctx) { + Connection.loadBaseInto(result, map, ctx); + if (map.containsKey("endpoint") && map.get("endpoint") != null) { + result.endpoint = String.valueOf(map.get("endpoint")); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + AnonymousConnection obj = ctx.processObject(this); + Map result = super.save(ctx); + if (obj.endpoint != null) result.put("endpoint", serializeScalar(obj.endpoint)); + return result; + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static AnonymousConnection fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static AnonymousConnection fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static AnonymousConnection fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static AnonymousConnection fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnthropicImageBlock.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnthropicImageBlock.java new file mode 100644 index 000000000..8ebcd0aa8 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnthropicImageBlock.java @@ -0,0 +1,85 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class AnthropicImageBlock { + public static final String SHORTHAND_PROPERTY = null; + + public String type = "image"; + public AnthropicImageSource source = null; + + public AnthropicImageBlock() { } + + @SuppressWarnings("unchecked") + public static AnthropicImageBlock load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new AnthropicImageBlock()); + } + AnthropicImageBlock result = new AnthropicImageBlock(); + AnthropicImageBlock.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(AnthropicImageBlock result, Map map, LoadContext ctx) { + if (map.containsKey("type") && map.get("type") != null) { + result.type = String.valueOf(map.get("type")); + } + if (map.containsKey("source") && map.get("source") != null) { + result.source = AnthropicImageSource.load(map.get("source"), ctx); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + AnthropicImageBlock obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.type != null) result.put("type", serializeScalar(obj.type)); + if (obj.source != null) result.put("source", obj.source.save(ctx)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static AnthropicImageBlock fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static AnthropicImageBlock fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static AnthropicImageBlock fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static AnthropicImageBlock fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnthropicImageSource.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnthropicImageSource.java new file mode 100644 index 000000000..332be2f74 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnthropicImageSource.java @@ -0,0 +1,90 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class AnthropicImageSource { + public static final String SHORTHAND_PROPERTY = null; + + public String type = "base64"; + public String media_type = ""; + public String data = ""; + + public AnthropicImageSource() { } + + @SuppressWarnings("unchecked") + public static AnthropicImageSource load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new AnthropicImageSource()); + } + AnthropicImageSource result = new AnthropicImageSource(); + AnthropicImageSource.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(AnthropicImageSource result, Map map, LoadContext ctx) { + if (map.containsKey("type") && map.get("type") != null) { + result.type = String.valueOf(map.get("type")); + } + if (map.containsKey("media_type") && map.get("media_type") != null) { + result.media_type = String.valueOf(map.get("media_type")); + } + if (map.containsKey("data") && map.get("data") != null) { + result.data = String.valueOf(map.get("data")); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + AnthropicImageSource obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.type != null) result.put("type", serializeScalar(obj.type)); + if (obj.media_type != null) result.put("media_type", serializeScalar(obj.media_type)); + if (obj.data != null) result.put("data", serializeScalar(obj.data)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static AnthropicImageSource fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static AnthropicImageSource fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static AnthropicImageSource fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static AnthropicImageSource fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnthropicMessagesRequest.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnthropicMessagesRequest.java new file mode 100644 index 000000000..9c2243587 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnthropicMessagesRequest.java @@ -0,0 +1,134 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class AnthropicMessagesRequest { + public static final String SHORTHAND_PROPERTY = null; + + public String model = ""; + public List messages = new ArrayList<>(); + public Integer max_tokens = 0; + public String system = null; + public Float temperature = null; + public Float top_p = null; + public Integer top_k = null; + public List stop_sequences = null; + public List tools = null; + + public AnthropicMessagesRequest() { } + + @SuppressWarnings("unchecked") + public static AnthropicMessagesRequest load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new AnthropicMessagesRequest()); + } + AnthropicMessagesRequest result = new AnthropicMessagesRequest(); + AnthropicMessagesRequest.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(AnthropicMessagesRequest result, Map map, LoadContext ctx) { + if (map.containsKey("model") && map.get("model") != null) { + result.model = String.valueOf(map.get("model")); + } + if (map.containsKey("messages") && map.get("messages") != null) { + result.messages = ModelCollections.loadList( + map.get("messages"), "messages", AnthropicWireMessage.SHORTHAND_PROPERTY, AnthropicWireMessage::load, ctx); + } + if (map.containsKey("max_tokens") && map.get("max_tokens") != null) { + result.max_tokens = (map.get("max_tokens") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("max_tokens")))); + } + if (map.containsKey("system") && map.get("system") != null) { + result.system = String.valueOf(map.get("system")); + } + if (map.containsKey("temperature") && map.get("temperature") != null) { + result.temperature = (map.get("temperature") instanceof Number n ? n.floatValue() : Float.parseFloat(String.valueOf(map.get("temperature")))); + } + if (map.containsKey("top_p") && map.get("top_p") != null) { + result.top_p = (map.get("top_p") instanceof Number n ? n.floatValue() : Float.parseFloat(String.valueOf(map.get("top_p")))); + } + if (map.containsKey("top_k") && map.get("top_k") != null) { + result.top_k = (map.get("top_k") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("top_k")))); + } + if (map.containsKey("stop_sequences") && map.get("stop_sequences") != null) { + result.stop_sequences = new ArrayList<>(); + if (map.get("stop_sequences") instanceof Iterable values) { + for (Object item : values) { + result.stop_sequences.add(String.valueOf(item)); + } + } + } + if (map.containsKey("tools") && map.get("tools") != null) { + result.tools = ModelCollections.loadList( + map.get("tools"), "tools", AnthropicToolDefinition.SHORTHAND_PROPERTY, AnthropicToolDefinition::load, ctx); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + AnthropicMessagesRequest obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.model != null) result.put("model", serializeScalar(obj.model)); + if (obj.messages != null) { + List items = new ArrayList<>(); + for (AnthropicWireMessage item : obj.messages) items.add(item.save(ctx)); + result.put("messages", items); + } + if (obj.max_tokens != null) result.put("max_tokens", serializeScalar(obj.max_tokens)); + if (obj.system != null) result.put("system", serializeScalar(obj.system)); + if (obj.temperature != null) result.put("temperature", serializeScalar(obj.temperature)); + if (obj.top_p != null) result.put("top_p", serializeScalar(obj.top_p)); + if (obj.top_k != null) result.put("top_k", serializeScalar(obj.top_k)); + if (obj.stop_sequences != null) result.put("stop_sequences", new ArrayList<>(obj.stop_sequences)); + if (obj.tools != null) { + result.put("tools", ModelCollections.saveList( + obj.tools, AnthropicToolDefinition.SHORTHAND_PROPERTY, item -> item.save(ctx), ctx)); + } + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static AnthropicMessagesRequest fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static AnthropicMessagesRequest fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static AnthropicMessagesRequest fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static AnthropicMessagesRequest fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnthropicMessagesResponse.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnthropicMessagesResponse.java new file mode 100644 index 000000000..89a3301fb --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnthropicMessagesResponse.java @@ -0,0 +1,115 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class AnthropicMessagesResponse { + public static final String SHORTHAND_PROPERTY = null; + + public String id = ""; + public String type = "message"; + public String role = "assistant"; + public List content = new ArrayList<>(); + public String model = ""; + public String stop_reason = ""; + public AnthropicUsage usage = null; + + public AnthropicMessagesResponse() { } + + @SuppressWarnings("unchecked") + public static AnthropicMessagesResponse load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new AnthropicMessagesResponse()); + } + AnthropicMessagesResponse result = new AnthropicMessagesResponse(); + AnthropicMessagesResponse.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(AnthropicMessagesResponse result, Map map, LoadContext ctx) { + if (map.containsKey("id") && map.get("id") != null) { + result.id = String.valueOf(map.get("id")); + } + if (map.containsKey("type") && map.get("type") != null) { + result.type = String.valueOf(map.get("type")); + } + if (map.containsKey("role") && map.get("role") != null) { + result.role = String.valueOf(map.get("role")); + } + if (map.containsKey("content") && map.get("content") != null) { + result.content = new ArrayList<>(); + if (map.get("content") instanceof Iterable values) { + for (Object item : values) { + result.content.add(item); + } + } + } + if (map.containsKey("model") && map.get("model") != null) { + result.model = String.valueOf(map.get("model")); + } + if (map.containsKey("stop_reason") && map.get("stop_reason") != null) { + result.stop_reason = String.valueOf(map.get("stop_reason")); + } + if (map.containsKey("usage") && map.get("usage") != null) { + result.usage = AnthropicUsage.load(map.get("usage"), ctx); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + AnthropicMessagesResponse obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.id != null) result.put("id", serializeScalar(obj.id)); + if (obj.type != null) result.put("type", serializeScalar(obj.type)); + if (obj.role != null) result.put("role", serializeScalar(obj.role)); + if (obj.content != null) result.put("content", new ArrayList<>(obj.content)); + if (obj.model != null) result.put("model", serializeScalar(obj.model)); + if (obj.stop_reason != null) result.put("stop_reason", serializeScalar(obj.stop_reason)); + if (obj.usage != null) result.put("usage", obj.usage.save(ctx)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static AnthropicMessagesResponse fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static AnthropicMessagesResponse fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static AnthropicMessagesResponse fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static AnthropicMessagesResponse fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnthropicTextBlock.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnthropicTextBlock.java new file mode 100644 index 000000000..f1b876c13 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnthropicTextBlock.java @@ -0,0 +1,85 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class AnthropicTextBlock { + public static final String SHORTHAND_PROPERTY = null; + + public String type = "text"; + public String text = ""; + + public AnthropicTextBlock() { } + + @SuppressWarnings("unchecked") + public static AnthropicTextBlock load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new AnthropicTextBlock()); + } + AnthropicTextBlock result = new AnthropicTextBlock(); + AnthropicTextBlock.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(AnthropicTextBlock result, Map map, LoadContext ctx) { + if (map.containsKey("type") && map.get("type") != null) { + result.type = String.valueOf(map.get("type")); + } + if (map.containsKey("text") && map.get("text") != null) { + result.text = String.valueOf(map.get("text")); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + AnthropicTextBlock obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.type != null) result.put("type", serializeScalar(obj.type)); + if (obj.text != null) result.put("text", serializeScalar(obj.text)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static AnthropicTextBlock fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static AnthropicTextBlock fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static AnthropicTextBlock fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static AnthropicTextBlock fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnthropicToolDefinition.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnthropicToolDefinition.java new file mode 100644 index 000000000..ac893ca15 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnthropicToolDefinition.java @@ -0,0 +1,92 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class AnthropicToolDefinition { + public static final String SHORTHAND_PROPERTY = null; + + public String name = ""; + public String description = null; + public Map input_schema = new LinkedHashMap<>(); + + public AnthropicToolDefinition() { } + + @SuppressWarnings("unchecked") + public static AnthropicToolDefinition load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new AnthropicToolDefinition()); + } + AnthropicToolDefinition result = new AnthropicToolDefinition(); + AnthropicToolDefinition.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(AnthropicToolDefinition result, Map map, LoadContext ctx) { + if (map.containsKey("name") && map.get("name") != null) { + result.name = String.valueOf(map.get("name")); + } + if (map.containsKey("description") && map.get("description") != null) { + result.description = String.valueOf(map.get("description")); + } + if (map.containsKey("input_schema") && map.get("input_schema") != null) { + if (map.get("input_schema") instanceof Map dict) { + result.input_schema = copyMap(dict); + } + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + AnthropicToolDefinition obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.name != null) result.put("name", serializeScalar(obj.name)); + if (obj.description != null) result.put("description", serializeScalar(obj.description)); + if (obj.input_schema != null) result.put("input_schema", serializeScalar(obj.input_schema)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static AnthropicToolDefinition fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static AnthropicToolDefinition fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static AnthropicToolDefinition fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static AnthropicToolDefinition fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnthropicToolResultBlock.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnthropicToolResultBlock.java new file mode 100644 index 000000000..44da4c94b --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnthropicToolResultBlock.java @@ -0,0 +1,90 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class AnthropicToolResultBlock { + public static final String SHORTHAND_PROPERTY = null; + + public String type = "tool_result"; + public String tool_use_id = ""; + public String content = ""; + + public AnthropicToolResultBlock() { } + + @SuppressWarnings("unchecked") + public static AnthropicToolResultBlock load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new AnthropicToolResultBlock()); + } + AnthropicToolResultBlock result = new AnthropicToolResultBlock(); + AnthropicToolResultBlock.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(AnthropicToolResultBlock result, Map map, LoadContext ctx) { + if (map.containsKey("type") && map.get("type") != null) { + result.type = String.valueOf(map.get("type")); + } + if (map.containsKey("tool_use_id") && map.get("tool_use_id") != null) { + result.tool_use_id = String.valueOf(map.get("tool_use_id")); + } + if (map.containsKey("content") && map.get("content") != null) { + result.content = String.valueOf(map.get("content")); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + AnthropicToolResultBlock obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.type != null) result.put("type", serializeScalar(obj.type)); + if (obj.tool_use_id != null) result.put("tool_use_id", serializeScalar(obj.tool_use_id)); + if (obj.content != null) result.put("content", serializeScalar(obj.content)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static AnthropicToolResultBlock fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static AnthropicToolResultBlock fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static AnthropicToolResultBlock fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static AnthropicToolResultBlock fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnthropicToolUseBlock.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnthropicToolUseBlock.java new file mode 100644 index 000000000..772996529 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnthropicToolUseBlock.java @@ -0,0 +1,97 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class AnthropicToolUseBlock { + public static final String SHORTHAND_PROPERTY = null; + + public String type = "tool_use"; + public String id = ""; + public String name = ""; + public Map input = new LinkedHashMap<>(); + + public AnthropicToolUseBlock() { } + + @SuppressWarnings("unchecked") + public static AnthropicToolUseBlock load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new AnthropicToolUseBlock()); + } + AnthropicToolUseBlock result = new AnthropicToolUseBlock(); + AnthropicToolUseBlock.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(AnthropicToolUseBlock result, Map map, LoadContext ctx) { + if (map.containsKey("type") && map.get("type") != null) { + result.type = String.valueOf(map.get("type")); + } + if (map.containsKey("id") && map.get("id") != null) { + result.id = String.valueOf(map.get("id")); + } + if (map.containsKey("name") && map.get("name") != null) { + result.name = String.valueOf(map.get("name")); + } + if (map.containsKey("input") && map.get("input") != null) { + if (map.get("input") instanceof Map dict) { + result.input = copyMap(dict); + } + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + AnthropicToolUseBlock obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.type != null) result.put("type", serializeScalar(obj.type)); + if (obj.id != null) result.put("id", serializeScalar(obj.id)); + if (obj.name != null) result.put("name", serializeScalar(obj.name)); + if (obj.input != null) result.put("input", serializeScalar(obj.input)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static AnthropicToolUseBlock fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static AnthropicToolUseBlock fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static AnthropicToolUseBlock fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static AnthropicToolUseBlock fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnthropicUsage.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnthropicUsage.java new file mode 100644 index 000000000..57af7e8c6 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnthropicUsage.java @@ -0,0 +1,85 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class AnthropicUsage { + public static final String SHORTHAND_PROPERTY = null; + + public Integer input_tokens = 0; + public Integer output_tokens = 0; + + public AnthropicUsage() { } + + @SuppressWarnings("unchecked") + public static AnthropicUsage load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new AnthropicUsage()); + } + AnthropicUsage result = new AnthropicUsage(); + AnthropicUsage.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(AnthropicUsage result, Map map, LoadContext ctx) { + if (map.containsKey("input_tokens") && map.get("input_tokens") != null) { + result.input_tokens = (map.get("input_tokens") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("input_tokens")))); + } + if (map.containsKey("output_tokens") && map.get("output_tokens") != null) { + result.output_tokens = (map.get("output_tokens") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("output_tokens")))); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + AnthropicUsage obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.input_tokens != null) result.put("input_tokens", serializeScalar(obj.input_tokens)); + if (obj.output_tokens != null) result.put("output_tokens", serializeScalar(obj.output_tokens)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static AnthropicUsage fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static AnthropicUsage fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static AnthropicUsage fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static AnthropicUsage fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnthropicWireMessage.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnthropicWireMessage.java new file mode 100644 index 000000000..544c0aab6 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnthropicWireMessage.java @@ -0,0 +1,90 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class AnthropicWireMessage { + public static final String SHORTHAND_PROPERTY = null; + + public String role = ""; + public List content = new ArrayList<>(); + + public AnthropicWireMessage() { } + + @SuppressWarnings("unchecked") + public static AnthropicWireMessage load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new AnthropicWireMessage()); + } + AnthropicWireMessage result = new AnthropicWireMessage(); + AnthropicWireMessage.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(AnthropicWireMessage result, Map map, LoadContext ctx) { + if (map.containsKey("role") && map.get("role") != null) { + result.role = String.valueOf(map.get("role")); + } + if (map.containsKey("content") && map.get("content") != null) { + result.content = new ArrayList<>(); + if (map.get("content") instanceof Iterable values) { + for (Object item : values) { + result.content.add(item); + } + } + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + AnthropicWireMessage obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.role != null) result.put("role", serializeScalar(obj.role)); + if (obj.content != null) result.put("content", new ArrayList<>(obj.content)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static AnthropicWireMessage fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static AnthropicWireMessage fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static AnthropicWireMessage fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static AnthropicWireMessage fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ApiKeyConnection.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ApiKeyConnection.java new file mode 100644 index 000000000..1a16fd19b --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ApiKeyConnection.java @@ -0,0 +1,88 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class ApiKeyConnection extends Connection { + public static final String SHORTHAND_PROPERTY = null; + + public String endpoint = ""; + public String apiKey = ""; + + public ApiKeyConnection() { + this.kind = "key"; + } + + @SuppressWarnings("unchecked") + public static ApiKeyConnection load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new ApiKeyConnection()); + } + ApiKeyConnection result = new ApiKeyConnection(); + ApiKeyConnection.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(ApiKeyConnection result, Map map, LoadContext ctx) { + Connection.loadBaseInto(result, map, ctx); + if (map.containsKey("endpoint") && map.get("endpoint") != null) { + result.endpoint = String.valueOf(map.get("endpoint")); + } + if (map.containsKey("apiKey") && map.get("apiKey") != null) { + result.apiKey = String.valueOf(map.get("apiKey")); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + ApiKeyConnection obj = ctx.processObject(this); + Map result = super.save(ctx); + if (obj.endpoint != null) result.put("endpoint", serializeScalar(obj.endpoint)); + if (obj.apiKey != null) result.put("apiKey", serializeScalar(obj.apiKey)); + return result; + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static ApiKeyConnection fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static ApiKeyConnection fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static ApiKeyConnection fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static ApiKeyConnection fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ArrayProperty.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ArrayProperty.java new file mode 100644 index 000000000..3f2d13d2d --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ArrayProperty.java @@ -0,0 +1,83 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class ArrayProperty extends Property { + public static final String SHORTHAND_PROPERTY = null; + + public Property items = null; + + public ArrayProperty() { + this.kind = "array"; + } + + @SuppressWarnings("unchecked") + public static ArrayProperty load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new ArrayProperty()); + } + ArrayProperty result = new ArrayProperty(); + ArrayProperty.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(ArrayProperty result, Map map, LoadContext ctx) { + Property.loadBaseInto(result, map, ctx); + if (map.containsKey("items") && map.get("items") != null) { + result.items = Property.load(map.get("items"), ctx); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + ArrayProperty obj = ctx.processObject(this); + Map result = super.save(ctx); + if (obj.items != null) result.put("items", obj.items.save(ctx)); + return result; + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static ArrayProperty fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static ArrayProperty fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static ArrayProperty fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static ArrayProperty fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AudioPart.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AudioPart.java new file mode 100644 index 000000000..75a2e0f49 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AudioPart.java @@ -0,0 +1,88 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class AudioPart extends ContentPart { + public static final String SHORTHAND_PROPERTY = null; + + public String source = ""; + public String mediaType = null; + + public AudioPart() { + this.kind = "audio"; + } + + @SuppressWarnings("unchecked") + public static AudioPart load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new AudioPart()); + } + AudioPart result = new AudioPart(); + AudioPart.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(AudioPart result, Map map, LoadContext ctx) { + ContentPart.loadBaseInto(result, map, ctx); + if (map.containsKey("source") && map.get("source") != null) { + result.source = String.valueOf(map.get("source")); + } + if (map.containsKey("mediaType") && map.get("mediaType") != null) { + result.mediaType = String.valueOf(map.get("mediaType")); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + AudioPart obj = ctx.processObject(this); + Map result = super.save(ctx); + if (obj.source != null) result.put("source", serializeScalar(obj.source)); + if (obj.mediaType != null) result.put("mediaType", serializeScalar(obj.mediaType)); + return result; + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static AudioPart fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static AudioPart fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static AudioPart fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static AudioPart fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AuthenticationMode.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AuthenticationMode.java new file mode 100644 index 000000000..6bc094a4d --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AuthenticationMode.java @@ -0,0 +1,18 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +public enum AuthenticationMode { + USER("user"), + SYSTEM("system"), + ; + + public final String value; + AuthenticationMode(String value) { this.value = value; } + public static AuthenticationMode fromValue(String value) { + for (AuthenticationMode item : values()) { + if (item.value.equals(value)) return item; + } + return valueOf(value.toUpperCase().replace("-", "_")); + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AuthorizationCodeFlow.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AuthorizationCodeFlow.java new file mode 100644 index 000000000..336f95713 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AuthorizationCodeFlow.java @@ -0,0 +1,103 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class AuthorizationCodeFlow { + public static final String SHORTHAND_PROPERTY = null; + + public String authUrl = ""; + public String codeVerifier = ""; + + public AuthorizationCodeFlow() { } + + @SuppressWarnings("unchecked") + public static AuthorizationCodeFlow load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new AuthorizationCodeFlow()); + } + AuthorizationCodeFlow result = new AuthorizationCodeFlow(); + AuthorizationCodeFlow.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(AuthorizationCodeFlow result, Map map, LoadContext ctx) { + if (map.containsKey("authUrl") && map.get("authUrl") != null) { + result.authUrl = String.valueOf(map.get("authUrl")); + } + if (map.containsKey("codeVerifier") && map.get("codeVerifier") != null) { + result.codeVerifier = String.valueOf(map.get("codeVerifier")); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + AuthorizationCodeFlow obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.authUrl != null) result.put("authUrl", serializeScalar(obj.authUrl)); + if (obj.codeVerifier != null) result.put("codeVerifier", serializeScalar(obj.codeVerifier)); + return ctx.processDict(result); + } + + public Map toWire(String provider) { + Map result = new LinkedHashMap<>(); + String target = provider == null ? "" : provider; + { + String wireName = "authUrl"; + boolean include = target.isEmpty(); + if (target.equals("foundry")) { wireName = "auth_url"; include = true; } + if (include && this.authUrl != null) result.put(wireName, serializeScalar(this.authUrl)); + } + { + String wireName = "codeVerifier"; + boolean include = target.isEmpty(); + if (target.equals("foundry")) { wireName = "code_verifier"; include = true; } + if (include && this.codeVerifier != null) result.put(wireName, serializeScalar(this.codeVerifier)); + } + return result; + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static AuthorizationCodeFlow fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static AuthorizationCodeFlow fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static AuthorizationCodeFlow fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static AuthorizationCodeFlow fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Binding.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Binding.java new file mode 100644 index 000000000..690930c87 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Binding.java @@ -0,0 +1,90 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class Binding { + public static final String SHORTHAND_PROPERTY = "input"; + + public String name = ""; + public String input = ""; + + public Binding() { } + + @SuppressWarnings("unchecked") + public static Binding load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (data instanceof String) { + Binding result = new Binding(); + result.input = String.valueOf(data); + return ctx.processOutput(result); + } + if (!(data instanceof Map map)) { + return ctx.processOutput(new Binding()); + } + Binding result = new Binding(); + Binding.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(Binding result, Map map, LoadContext ctx) { + if (map.containsKey("name") && map.get("name") != null) { + result.name = String.valueOf(map.get("name")); + } + if (map.containsKey("input") && map.get("input") != null) { + result.input = String.valueOf(map.get("input")); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + Binding obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.name != null) result.put("name", serializeScalar(obj.name)); + if (obj.input != null) result.put("input", serializeScalar(obj.input)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static Binding fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static Binding fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static Binding fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static Binding fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Checkpoint.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Checkpoint.java new file mode 100644 index 000000000..8137866fd --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Checkpoint.java @@ -0,0 +1,134 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class Checkpoint { + public static final String SHORTHAND_PROPERTY = null; + + public String id = null; + public String sessionId = null; + public String turnId = null; + public Integer checkpointNumber = null; + public String title = ""; + public String overview = null; + public Map state = null; + public String summary = null; + public Map metadata = null; + public String createdAt = null; + public RedactionMetadata redaction = null; + + public Checkpoint() { } + + @SuppressWarnings("unchecked") + public static Checkpoint load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new Checkpoint()); + } + Checkpoint result = new Checkpoint(); + Checkpoint.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(Checkpoint result, Map map, LoadContext ctx) { + if (map.containsKey("id") && map.get("id") != null) { + result.id = String.valueOf(map.get("id")); + } + if (map.containsKey("sessionId") && map.get("sessionId") != null) { + result.sessionId = String.valueOf(map.get("sessionId")); + } + if (map.containsKey("turnId") && map.get("turnId") != null) { + result.turnId = String.valueOf(map.get("turnId")); + } + if (map.containsKey("checkpointNumber") && map.get("checkpointNumber") != null) { + result.checkpointNumber = (map.get("checkpointNumber") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("checkpointNumber")))); + } + if (map.containsKey("title") && map.get("title") != null) { + result.title = String.valueOf(map.get("title")); + } + if (map.containsKey("overview") && map.get("overview") != null) { + result.overview = String.valueOf(map.get("overview")); + } + if (map.containsKey("state") && map.get("state") != null) { + if (map.get("state") instanceof Map dict) { + result.state = copyMap(dict); + } + } + if (map.containsKey("summary") && map.get("summary") != null) { + result.summary = String.valueOf(map.get("summary")); + } + if (map.containsKey("metadata") && map.get("metadata") != null) { + if (map.get("metadata") instanceof Map dict) { + result.metadata = copyMap(dict); + } + } + if (map.containsKey("createdAt") && map.get("createdAt") != null) { + result.createdAt = String.valueOf(map.get("createdAt")); + } + if (map.containsKey("redaction") && map.get("redaction") != null) { + result.redaction = RedactionMetadata.load(map.get("redaction"), ctx); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + Checkpoint obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.id != null) result.put("id", serializeScalar(obj.id)); + if (obj.sessionId != null) result.put("sessionId", serializeScalar(obj.sessionId)); + if (obj.turnId != null) result.put("turnId", serializeScalar(obj.turnId)); + if (obj.checkpointNumber != null) result.put("checkpointNumber", serializeScalar(obj.checkpointNumber)); + if (obj.title != null) result.put("title", serializeScalar(obj.title)); + if (obj.overview != null) result.put("overview", serializeScalar(obj.overview)); + if (obj.state != null) result.put("state", serializeScalar(obj.state)); + if (obj.summary != null) result.put("summary", serializeScalar(obj.summary)); + if (obj.metadata != null) result.put("metadata", serializeScalar(obj.metadata)); + if (obj.createdAt != null) result.put("createdAt", serializeScalar(obj.createdAt)); + if (obj.redaction != null) result.put("redaction", obj.redaction.save(ctx)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static Checkpoint fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static Checkpoint fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static Checkpoint fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static Checkpoint fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/CheckpointStore.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/CheckpointStore.java new file mode 100644 index 000000000..25ce37de7 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/CheckpointStore.java @@ -0,0 +1,14 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public interface CheckpointStore { + Checkpoint save(Checkpoint checkpoint); + Checkpoint load(String sessionId, String checkpointId); + List listCheckpoints(String sessionId); +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/CompactionCompletePayload.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/CompactionCompletePayload.java new file mode 100644 index 000000000..c3cfbdd97 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/CompactionCompletePayload.java @@ -0,0 +1,90 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class CompactionCompletePayload { + public static final String SHORTHAND_PROPERTY = null; + + public Integer removed = 0; + public Integer remaining = 0; + public Integer summaryLength = null; + + public CompactionCompletePayload() { } + + @SuppressWarnings("unchecked") + public static CompactionCompletePayload load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new CompactionCompletePayload()); + } + CompactionCompletePayload result = new CompactionCompletePayload(); + CompactionCompletePayload.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(CompactionCompletePayload result, Map map, LoadContext ctx) { + if (map.containsKey("removed") && map.get("removed") != null) { + result.removed = (map.get("removed") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("removed")))); + } + if (map.containsKey("remaining") && map.get("remaining") != null) { + result.remaining = (map.get("remaining") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("remaining")))); + } + if (map.containsKey("summaryLength") && map.get("summaryLength") != null) { + result.summaryLength = (map.get("summaryLength") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("summaryLength")))); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + CompactionCompletePayload obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.removed != null) result.put("removed", serializeScalar(obj.removed)); + if (obj.remaining != null) result.put("remaining", serializeScalar(obj.remaining)); + if (obj.summaryLength != null) result.put("summaryLength", serializeScalar(obj.summaryLength)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static CompactionCompletePayload fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static CompactionCompletePayload fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static CompactionCompletePayload fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static CompactionCompletePayload fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/CompactionConfig.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/CompactionConfig.java new file mode 100644 index 000000000..71866205c --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/CompactionConfig.java @@ -0,0 +1,92 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class CompactionConfig { + public static final String SHORTHAND_PROPERTY = null; + + public String strategy = null; + public Integer budget = null; + public Map options = null; + + public CompactionConfig() { } + + @SuppressWarnings("unchecked") + public static CompactionConfig load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new CompactionConfig()); + } + CompactionConfig result = new CompactionConfig(); + CompactionConfig.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(CompactionConfig result, Map map, LoadContext ctx) { + if (map.containsKey("strategy") && map.get("strategy") != null) { + result.strategy = String.valueOf(map.get("strategy")); + } + if (map.containsKey("budget") && map.get("budget") != null) { + result.budget = (map.get("budget") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("budget")))); + } + if (map.containsKey("options") && map.get("options") != null) { + if (map.get("options") instanceof Map dict) { + result.options = copyMap(dict); + } + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + CompactionConfig obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.strategy != null) result.put("strategy", serializeScalar(obj.strategy)); + if (obj.budget != null) result.put("budget", serializeScalar(obj.budget)); + if (obj.options != null) result.put("options", serializeScalar(obj.options)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static CompactionConfig fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static CompactionConfig fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static CompactionConfig fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static CompactionConfig fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/CompactionFailedPayload.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/CompactionFailedPayload.java new file mode 100644 index 000000000..0c8032657 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/CompactionFailedPayload.java @@ -0,0 +1,80 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class CompactionFailedPayload { + public static final String SHORTHAND_PROPERTY = null; + + public String message = ""; + + public CompactionFailedPayload() { } + + @SuppressWarnings("unchecked") + public static CompactionFailedPayload load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new CompactionFailedPayload()); + } + CompactionFailedPayload result = new CompactionFailedPayload(); + CompactionFailedPayload.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(CompactionFailedPayload result, Map map, LoadContext ctx) { + if (map.containsKey("message") && map.get("message") != null) { + result.message = String.valueOf(map.get("message")); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + CompactionFailedPayload obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.message != null) result.put("message", serializeScalar(obj.message)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static CompactionFailedPayload fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static CompactionFailedPayload fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static CompactionFailedPayload fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static CompactionFailedPayload fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/CompactionStartPayload.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/CompactionStartPayload.java new file mode 100644 index 000000000..15c989c56 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/CompactionStartPayload.java @@ -0,0 +1,80 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class CompactionStartPayload { + public static final String SHORTHAND_PROPERTY = null; + + public Integer droppedCount = 0; + + public CompactionStartPayload() { } + + @SuppressWarnings("unchecked") + public static CompactionStartPayload load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new CompactionStartPayload()); + } + CompactionStartPayload result = new CompactionStartPayload(); + CompactionStartPayload.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(CompactionStartPayload result, Map map, LoadContext ctx) { + if (map.containsKey("droppedCount") && map.get("droppedCount") != null) { + result.droppedCount = (map.get("droppedCount") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("droppedCount")))); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + CompactionStartPayload obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.droppedCount != null) result.put("droppedCount", serializeScalar(obj.droppedCount)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static CompactionStartPayload fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static CompactionStartPayload fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static CompactionStartPayload fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static CompactionStartPayload fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Connection.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Connection.java new file mode 100644 index 000000000..b9d3648de --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Connection.java @@ -0,0 +1,106 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public abstract class Connection { + public static final String SHORTHAND_PROPERTY = null; + + public String kind = ""; + public AuthenticationMode authenticationMode = null; + public String usageDescription = null; + + public Connection() { } + + @SuppressWarnings("unchecked") + public static Connection load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (data instanceof Map dispatchMap) { + Object discriminator = dispatchMap.get("kind"); + if (discriminator != null) { + switch (String.valueOf(discriminator).toLowerCase(java.util.Locale.ROOT)) { + case "reference": + return ReferenceConnection.load(data, ctx); + case "remote": + return RemoteConnection.load(data, ctx); + case "key": + return ApiKeyConnection.load(data, ctx); + case "anonymous": + return AnonymousConnection.load(data, ctx); + case "oauth": + return OAuthConnection.load(data, ctx); + case "foundry": + return FoundryConnection.load(data, ctx); + default: + break; + } + } + } + throw new IllegalArgumentException("Cannot instantiate abstract Connection; expected a matching 'kind' discriminator."); + } + + static void loadBaseInto(Connection result, Map map, LoadContext ctx) { + if (map.containsKey("kind") && map.get("kind") != null) { + result.kind = String.valueOf(map.get("kind")); + } + if (map.containsKey("authenticationMode") && map.get("authenticationMode") != null) { + result.authenticationMode = AuthenticationMode.fromValue(String.valueOf(map.get("authenticationMode"))); + } + if (map.containsKey("usageDescription") && map.get("usageDescription") != null) { + result.usageDescription = String.valueOf(map.get("usageDescription")); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + Connection obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.kind != null) result.put("kind", serializeScalar(obj.kind)); + if (obj.authenticationMode != null) result.put("authenticationMode", obj.authenticationMode.value); + if (obj.usageDescription != null) result.put("usageDescription", serializeScalar(obj.usageDescription)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static Connection fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static Connection fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static Connection fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static Connection fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ContentPart.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ContentPart.java new file mode 100644 index 000000000..bb3aac2c6 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ContentPart.java @@ -0,0 +1,92 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public abstract class ContentPart { + public static final String SHORTHAND_PROPERTY = null; + + public String kind = ""; + + public ContentPart() { } + + @SuppressWarnings("unchecked") + public static ContentPart load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (data instanceof Map dispatchMap) { + Object discriminator = dispatchMap.get("kind"); + if (discriminator != null) { + switch (String.valueOf(discriminator).toLowerCase(java.util.Locale.ROOT)) { + case "text": + return TextPart.load(data, ctx); + case "image": + return ImagePart.load(data, ctx); + case "file": + return FilePart.load(data, ctx); + case "audio": + return AudioPart.load(data, ctx); + default: + break; + } + } + } + throw new IllegalArgumentException("Cannot instantiate abstract ContentPart; expected a matching 'kind' discriminator."); + } + + static void loadBaseInto(ContentPart result, Map map, LoadContext ctx) { + if (map.containsKey("kind") && map.get("kind") != null) { + result.kind = String.valueOf(map.get("kind")); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + ContentPart obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.kind != null) result.put("kind", serializeScalar(obj.kind)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static ContentPart fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static ContentPart fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static ContentPart fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static ContentPart fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ContextCandidate.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ContextCandidate.java new file mode 100644 index 000000000..49e77a969 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ContextCandidate.java @@ -0,0 +1,102 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class ContextCandidate { + public static final String SHORTHAND_PROPERTY = null; + + public String id = ""; + public String source = ""; + public List messages = new ArrayList<>(); + public Map metadata = null; + + public ContextCandidate() { } + + @SuppressWarnings("unchecked") + public static ContextCandidate load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new ContextCandidate()); + } + ContextCandidate result = new ContextCandidate(); + ContextCandidate.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(ContextCandidate result, Map map, LoadContext ctx) { + if (map.containsKey("id") && map.get("id") != null) { + result.id = String.valueOf(map.get("id")); + } + if (map.containsKey("source") && map.get("source") != null) { + result.source = String.valueOf(map.get("source")); + } + if (map.containsKey("messages") && map.get("messages") != null) { + result.messages = ModelCollections.loadList( + map.get("messages"), "messages", Message.SHORTHAND_PROPERTY, Message::load, ctx); + } + if (map.containsKey("metadata") && map.get("metadata") != null) { + if (map.get("metadata") instanceof Map dict) { + result.metadata = copyMap(dict); + } + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + ContextCandidate obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.id != null) result.put("id", serializeScalar(obj.id)); + if (obj.source != null) result.put("source", serializeScalar(obj.source)); + if (obj.messages != null) { + List items = new ArrayList<>(); + for (Message item : obj.messages) items.add(item.save(ctx)); + result.put("messages", items); + } + if (obj.metadata != null) result.put("metadata", serializeScalar(obj.metadata)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static ContextCandidate fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static ContextCandidate fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static ContextCandidate fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static ContextCandidate fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ContextRequest.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ContextRequest.java new file mode 100644 index 000000000..7ccb8738d --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ContextRequest.java @@ -0,0 +1,120 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class ContextRequest { + public static final String SHORTHAND_PROPERTY = null; + + public String sessionId = ""; + public String turnId = ""; + public String invocationId = ""; + public Integer iteration = 0; + public List messages = new ArrayList<>(); + public Integer stablePrefixMessages = 0; + public InvocationContextState contextState = null; + public Object inputs = null; + + public ContextRequest() { } + + @SuppressWarnings("unchecked") + public static ContextRequest load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new ContextRequest()); + } + ContextRequest result = new ContextRequest(); + ContextRequest.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(ContextRequest result, Map map, LoadContext ctx) { + if (map.containsKey("sessionId") && map.get("sessionId") != null) { + result.sessionId = String.valueOf(map.get("sessionId")); + } + if (map.containsKey("turnId") && map.get("turnId") != null) { + result.turnId = String.valueOf(map.get("turnId")); + } + if (map.containsKey("invocationId") && map.get("invocationId") != null) { + result.invocationId = String.valueOf(map.get("invocationId")); + } + if (map.containsKey("iteration") && map.get("iteration") != null) { + result.iteration = (map.get("iteration") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("iteration")))); + } + if (map.containsKey("messages") && map.get("messages") != null) { + result.messages = ModelCollections.loadList( + map.get("messages"), "messages", Message.SHORTHAND_PROPERTY, Message::load, ctx); + } + if (map.containsKey("stablePrefixMessages") && map.get("stablePrefixMessages") != null) { + result.stablePrefixMessages = (map.get("stablePrefixMessages") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("stablePrefixMessages")))); + } + if (map.containsKey("contextState") && map.get("contextState") != null) { + result.contextState = InvocationContextState.load(map.get("contextState"), ctx); + } + if (map.containsKey("inputs") && map.get("inputs") != null) { + result.inputs = map.get("inputs"); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + ContextRequest obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.sessionId != null) result.put("sessionId", serializeScalar(obj.sessionId)); + if (obj.turnId != null) result.put("turnId", serializeScalar(obj.turnId)); + if (obj.invocationId != null) result.put("invocationId", serializeScalar(obj.invocationId)); + if (obj.iteration != null) result.put("iteration", serializeScalar(obj.iteration)); + if (obj.messages != null) { + List items = new ArrayList<>(); + for (Message item : obj.messages) items.add(item.save(ctx)); + result.put("messages", items); + } + if (obj.stablePrefixMessages != null) result.put("stablePrefixMessages", serializeScalar(obj.stablePrefixMessages)); + if (obj.contextState != null) result.put("contextState", obj.contextState.save(ctx)); + if (obj.inputs != null) result.put("inputs", serializeScalar(obj.inputs)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static ContextRequest fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static ContextRequest fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static ContextRequest fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static ContextRequest fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/CustomTool.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/CustomTool.java new file mode 100644 index 000000000..18dbabb21 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/CustomTool.java @@ -0,0 +1,90 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class CustomTool extends Tool { + public static final String SHORTHAND_PROPERTY = null; + + public Connection connection = null; + public Map options = new LinkedHashMap<>(); + + public CustomTool() { + this.kind = "*"; + } + + @SuppressWarnings("unchecked") + public static CustomTool load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new CustomTool()); + } + CustomTool result = new CustomTool(); + CustomTool.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(CustomTool result, Map map, LoadContext ctx) { + Tool.loadBaseInto(result, map, ctx); + if (map.containsKey("connection") && map.get("connection") != null) { + result.connection = Connection.load(map.get("connection"), ctx); + } + if (map.containsKey("options") && map.get("options") != null) { + if (map.get("options") instanceof Map dict) { + result.options = copyMap(dict); + } + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + CustomTool obj = ctx.processObject(this); + Map result = super.save(ctx); + if (obj.connection != null) result.put("connection", obj.connection.save(ctx)); + if (obj.options != null) result.put("options", serializeScalar(obj.options)); + return result; + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static CustomTool fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static CustomTool fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static CustomTool fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static CustomTool fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/DelegatedStateReference.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/DelegatedStateReference.java new file mode 100644 index 000000000..009e6d25b --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/DelegatedStateReference.java @@ -0,0 +1,97 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class DelegatedStateReference { + public static final String SHORTHAND_PROPERTY = null; + + public String provider = ""; + public String kind = ""; + public String id = ""; + public Map metadata = null; + + public DelegatedStateReference() { } + + @SuppressWarnings("unchecked") + public static DelegatedStateReference load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new DelegatedStateReference()); + } + DelegatedStateReference result = new DelegatedStateReference(); + DelegatedStateReference.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(DelegatedStateReference result, Map map, LoadContext ctx) { + if (map.containsKey("provider") && map.get("provider") != null) { + result.provider = String.valueOf(map.get("provider")); + } + if (map.containsKey("kind") && map.get("kind") != null) { + result.kind = String.valueOf(map.get("kind")); + } + if (map.containsKey("id") && map.get("id") != null) { + result.id = String.valueOf(map.get("id")); + } + if (map.containsKey("metadata") && map.get("metadata") != null) { + if (map.get("metadata") instanceof Map dict) { + result.metadata = copyMap(dict); + } + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + DelegatedStateReference obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.provider != null) result.put("provider", serializeScalar(obj.provider)); + if (obj.kind != null) result.put("kind", serializeScalar(obj.kind)); + if (obj.id != null) result.put("id", serializeScalar(obj.id)); + if (obj.metadata != null) result.put("metadata", serializeScalar(obj.metadata)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static DelegatedStateReference fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static DelegatedStateReference fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static DelegatedStateReference fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static DelegatedStateReference fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/DeviceAuthorization.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/DeviceAuthorization.java new file mode 100644 index 000000000..16b3b3558 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/DeviceAuthorization.java @@ -0,0 +1,147 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class DeviceAuthorization { + public static final String SHORTHAND_PROPERTY = null; + + public String deviceCode = ""; + public String userCode = ""; + public String verificationUri = ""; + public Long expiresIn = 0L; + public Long interval = 0L; + public String message = ""; + + public DeviceAuthorization() { } + + @SuppressWarnings("unchecked") + public static DeviceAuthorization load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new DeviceAuthorization()); + } + DeviceAuthorization result = new DeviceAuthorization(); + DeviceAuthorization.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(DeviceAuthorization result, Map map, LoadContext ctx) { + if (map.containsKey("deviceCode") && map.get("deviceCode") != null) { + result.deviceCode = String.valueOf(map.get("deviceCode")); + } + if (map.containsKey("userCode") && map.get("userCode") != null) { + result.userCode = String.valueOf(map.get("userCode")); + } + if (map.containsKey("verificationUri") && map.get("verificationUri") != null) { + result.verificationUri = String.valueOf(map.get("verificationUri")); + } + if (map.containsKey("expiresIn") && map.get("expiresIn") != null) { + result.expiresIn = (map.get("expiresIn") instanceof Number n ? n.longValue() : Long.parseLong(String.valueOf(map.get("expiresIn")))); + } + if (map.containsKey("interval") && map.get("interval") != null) { + result.interval = (map.get("interval") instanceof Number n ? n.longValue() : Long.parseLong(String.valueOf(map.get("interval")))); + } + if (map.containsKey("message") && map.get("message") != null) { + result.message = String.valueOf(map.get("message")); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + DeviceAuthorization obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.deviceCode != null) result.put("deviceCode", serializeScalar(obj.deviceCode)); + if (obj.userCode != null) result.put("userCode", serializeScalar(obj.userCode)); + if (obj.verificationUri != null) result.put("verificationUri", serializeScalar(obj.verificationUri)); + if (obj.expiresIn != null) result.put("expiresIn", serializeScalar(obj.expiresIn)); + if (obj.interval != null) result.put("interval", serializeScalar(obj.interval)); + if (obj.message != null) result.put("message", serializeScalar(obj.message)); + return ctx.processDict(result); + } + + public Map toWire(String provider) { + Map result = new LinkedHashMap<>(); + String target = provider == null ? "" : provider; + { + String wireName = "deviceCode"; + boolean include = target.isEmpty(); + if (target.equals("foundry")) { wireName = "device_code"; include = true; } + if (include && this.deviceCode != null) result.put(wireName, serializeScalar(this.deviceCode)); + } + { + String wireName = "userCode"; + boolean include = target.isEmpty(); + if (target.equals("foundry")) { wireName = "user_code"; include = true; } + if (include && this.userCode != null) result.put(wireName, serializeScalar(this.userCode)); + } + { + String wireName = "verificationUri"; + boolean include = target.isEmpty(); + if (target.equals("foundry")) { wireName = "verification_uri"; include = true; } + if (include && this.verificationUri != null) result.put(wireName, serializeScalar(this.verificationUri)); + } + { + String wireName = "expiresIn"; + boolean include = target.isEmpty(); + if (target.equals("foundry")) { wireName = "expires_in"; include = true; } + if (include && this.expiresIn != null) result.put(wireName, serializeScalar(this.expiresIn)); + } + { + String wireName = "interval"; + boolean include = target.isEmpty(); + if (target.equals("foundry")) { wireName = "interval"; include = true; } + if (include && this.interval != null) result.put(wireName, serializeScalar(this.interval)); + } + { + String wireName = "message"; + boolean include = target.isEmpty(); + if (target.equals("foundry")) { wireName = "message"; include = true; } + if (include && this.message != null) result.put(wireName, serializeScalar(this.message)); + } + return result; + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static DeviceAuthorization fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static DeviceAuthorization fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static DeviceAuthorization fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static DeviceAuthorization fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/DoneEventPayload.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/DoneEventPayload.java new file mode 100644 index 000000000..0bbdb8bd7 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/DoneEventPayload.java @@ -0,0 +1,90 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class DoneEventPayload { + public static final String SHORTHAND_PROPERTY = null; + + public Object response = 0; + public List messages = new ArrayList<>(); + + public DoneEventPayload() { } + + @SuppressWarnings("unchecked") + public static DoneEventPayload load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new DoneEventPayload()); + } + DoneEventPayload result = new DoneEventPayload(); + DoneEventPayload.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(DoneEventPayload result, Map map, LoadContext ctx) { + if (map.containsKey("response") && map.get("response") != null) { + result.response = map.get("response"); + } + if (map.containsKey("messages") && map.get("messages") != null) { + result.messages = ModelCollections.loadList( + map.get("messages"), "messages", Message.SHORTHAND_PROPERTY, Message::load, ctx); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + DoneEventPayload obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.response != null) result.put("response", serializeScalar(obj.response)); + if (obj.messages != null) { + List items = new ArrayList<>(); + for (Message item : obj.messages) items.add(item.save(ctx)); + result.put("messages", items); + } + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static DoneEventPayload fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static DoneEventPayload fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static DoneEventPayload fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static DoneEventPayload fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/EngineCheckpoint.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/EngineCheckpoint.java new file mode 100644 index 000000000..e4857ae3e --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/EngineCheckpoint.java @@ -0,0 +1,210 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class EngineCheckpoint { + public static final String SHORTHAND_PROPERTY = null; + + public String id = ""; + public String sessionId = ""; + public String turnId = ""; + public String runId = ""; + public String parentRunId = null; + public Integer delegationDepth = 0; + public Integer iteration = 0; + public Long lastSequence = 0L; + public List messages = new ArrayList<>(); + public Integer stablePrefixMessages = 0; + public Object inputs = null; + public String activeInvocationId = null; + public List pendingToolRequests = null; + public List completedToolResults = null; + public Integer completedModelIterations = 0; + public Boolean reconciliationRequired = false; + public ModelReconciliationState modelReconciliation = null; + public Object pendingOutput = null; + public Boolean finalOutputReady = false; + public ModelInvocationResponse pendingModelResponse = null; + public Boolean resumeSameIteration = false; + public Boolean policyAppliedForIteration = false; + public InvocationContextState contextState = null; + public Map metadata = null; + + public EngineCheckpoint() { } + + @SuppressWarnings("unchecked") + public static EngineCheckpoint load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new EngineCheckpoint()); + } + EngineCheckpoint result = new EngineCheckpoint(); + EngineCheckpoint.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(EngineCheckpoint result, Map map, LoadContext ctx) { + if (map.containsKey("id") && map.get("id") != null) { + result.id = String.valueOf(map.get("id")); + } + if (map.containsKey("sessionId") && map.get("sessionId") != null) { + result.sessionId = String.valueOf(map.get("sessionId")); + } + if (map.containsKey("turnId") && map.get("turnId") != null) { + result.turnId = String.valueOf(map.get("turnId")); + } + if (map.containsKey("runId") && map.get("runId") != null) { + result.runId = String.valueOf(map.get("runId")); + } + if (map.containsKey("parentRunId") && map.get("parentRunId") != null) { + result.parentRunId = String.valueOf(map.get("parentRunId")); + } + if (map.containsKey("delegationDepth") && map.get("delegationDepth") != null) { + result.delegationDepth = (map.get("delegationDepth") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("delegationDepth")))); + } + if (map.containsKey("iteration") && map.get("iteration") != null) { + result.iteration = (map.get("iteration") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("iteration")))); + } + if (map.containsKey("lastSequence") && map.get("lastSequence") != null) { + result.lastSequence = (map.get("lastSequence") instanceof Number n ? n.longValue() : Long.parseLong(String.valueOf(map.get("lastSequence")))); + } + if (map.containsKey("messages") && map.get("messages") != null) { + result.messages = ModelCollections.loadList( + map.get("messages"), "messages", Message.SHORTHAND_PROPERTY, Message::load, ctx); + } + if (map.containsKey("stablePrefixMessages") && map.get("stablePrefixMessages") != null) { + result.stablePrefixMessages = (map.get("stablePrefixMessages") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("stablePrefixMessages")))); + } + if (map.containsKey("inputs") && map.get("inputs") != null) { + result.inputs = map.get("inputs"); + } + if (map.containsKey("activeInvocationId") && map.get("activeInvocationId") != null) { + result.activeInvocationId = String.valueOf(map.get("activeInvocationId")); + } + if (map.containsKey("pendingToolRequests") && map.get("pendingToolRequests") != null) { + result.pendingToolRequests = ModelCollections.loadList( + map.get("pendingToolRequests"), "pendingToolRequests", ModelToolRequest.SHORTHAND_PROPERTY, ModelToolRequest::load, ctx); + } + if (map.containsKey("completedToolResults") && map.get("completedToolResults") != null) { + result.completedToolResults = ModelCollections.loadList( + map.get("completedToolResults"), "completedToolResults", ModelToolResult.SHORTHAND_PROPERTY, ModelToolResult::load, ctx); + } + if (map.containsKey("completedModelIterations") && map.get("completedModelIterations") != null) { + result.completedModelIterations = (map.get("completedModelIterations") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("completedModelIterations")))); + } + if (map.containsKey("reconciliationRequired") && map.get("reconciliationRequired") != null) { + result.reconciliationRequired = (map.get("reconciliationRequired") instanceof Boolean b ? b : Boolean.parseBoolean(String.valueOf(map.get("reconciliationRequired")))); + } + if (map.containsKey("modelReconciliation") && map.get("modelReconciliation") != null) { + result.modelReconciliation = ModelReconciliationState.load(map.get("modelReconciliation"), ctx); + } + if (map.containsKey("pendingOutput") && map.get("pendingOutput") != null) { + result.pendingOutput = map.get("pendingOutput"); + } + if (map.containsKey("finalOutputReady") && map.get("finalOutputReady") != null) { + result.finalOutputReady = (map.get("finalOutputReady") instanceof Boolean b ? b : Boolean.parseBoolean(String.valueOf(map.get("finalOutputReady")))); + } + if (map.containsKey("pendingModelResponse") && map.get("pendingModelResponse") != null) { + result.pendingModelResponse = ModelInvocationResponse.load(map.get("pendingModelResponse"), ctx); + } + if (map.containsKey("resumeSameIteration") && map.get("resumeSameIteration") != null) { + result.resumeSameIteration = (map.get("resumeSameIteration") instanceof Boolean b ? b : Boolean.parseBoolean(String.valueOf(map.get("resumeSameIteration")))); + } + if (map.containsKey("policyAppliedForIteration") && map.get("policyAppliedForIteration") != null) { + result.policyAppliedForIteration = (map.get("policyAppliedForIteration") instanceof Boolean b ? b : Boolean.parseBoolean(String.valueOf(map.get("policyAppliedForIteration")))); + } + if (map.containsKey("contextState") && map.get("contextState") != null) { + result.contextState = InvocationContextState.load(map.get("contextState"), ctx); + } + if (map.containsKey("metadata") && map.get("metadata") != null) { + if (map.get("metadata") instanceof Map dict) { + result.metadata = copyMap(dict); + } + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + EngineCheckpoint obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.id != null) result.put("id", serializeScalar(obj.id)); + if (obj.sessionId != null) result.put("sessionId", serializeScalar(obj.sessionId)); + if (obj.turnId != null) result.put("turnId", serializeScalar(obj.turnId)); + if (obj.runId != null) result.put("runId", serializeScalar(obj.runId)); + if (obj.parentRunId != null) result.put("parentRunId", serializeScalar(obj.parentRunId)); + if (obj.delegationDepth != null) result.put("delegationDepth", serializeScalar(obj.delegationDepth)); + if (obj.iteration != null) result.put("iteration", serializeScalar(obj.iteration)); + if (obj.lastSequence != null) result.put("lastSequence", serializeScalar(obj.lastSequence)); + if (obj.messages != null) { + List items = new ArrayList<>(); + for (Message item : obj.messages) items.add(item.save(ctx)); + result.put("messages", items); + } + if (obj.stablePrefixMessages != null) result.put("stablePrefixMessages", serializeScalar(obj.stablePrefixMessages)); + if (obj.inputs != null) result.put("inputs", serializeScalar(obj.inputs)); + if (obj.activeInvocationId != null) result.put("activeInvocationId", serializeScalar(obj.activeInvocationId)); + if (obj.pendingToolRequests != null) { + result.put("pendingToolRequests", ModelCollections.saveList( + obj.pendingToolRequests, ModelToolRequest.SHORTHAND_PROPERTY, item -> item.save(ctx), ctx)); + } + if (obj.completedToolResults != null) { + result.put("completedToolResults", ModelCollections.saveList( + obj.completedToolResults, ModelToolResult.SHORTHAND_PROPERTY, item -> item.save(ctx), ctx)); + } + if (obj.completedModelIterations != null) result.put("completedModelIterations", serializeScalar(obj.completedModelIterations)); + if (obj.reconciliationRequired != null) result.put("reconciliationRequired", serializeScalar(obj.reconciliationRequired)); + if (obj.modelReconciliation != null) result.put("modelReconciliation", obj.modelReconciliation.save(ctx)); + if (obj.pendingOutput != null) result.put("pendingOutput", serializeScalar(obj.pendingOutput)); + if (obj.finalOutputReady != null) result.put("finalOutputReady", serializeScalar(obj.finalOutputReady)); + if (obj.pendingModelResponse != null) result.put("pendingModelResponse", obj.pendingModelResponse.save(ctx)); + if (obj.resumeSameIteration != null) result.put("resumeSameIteration", serializeScalar(obj.resumeSameIteration)); + if (obj.policyAppliedForIteration != null) result.put("policyAppliedForIteration", serializeScalar(obj.policyAppliedForIteration)); + if (obj.contextState != null) result.put("contextState", obj.contextState.save(ctx)); + if (obj.metadata != null) result.put("metadata", serializeScalar(obj.metadata)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static EngineCheckpoint fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static EngineCheckpoint fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static EngineCheckpoint fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static EngineCheckpoint fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/EngineEvent.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/EngineEvent.java new file mode 100644 index 000000000..19be4eb17 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/EngineEvent.java @@ -0,0 +1,135 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class EngineEvent { + public static final String SHORTHAND_PROPERTY = null; + + public Long sequence = 0L; + public String id = ""; + public String timestamp = ""; + public String sessionId = ""; + public String turnId = ""; + public String runId = ""; + public String parentRunId = null; + public Integer delegationDepth = 0; + public String invocationId = null; + public Integer iteration = null; + public EngineEventKind kind = EngineEventKind.TURN_STARTED; + public Object payload = null; + + public EngineEvent() { } + + @SuppressWarnings("unchecked") + public static EngineEvent load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new EngineEvent()); + } + EngineEvent result = new EngineEvent(); + EngineEvent.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(EngineEvent result, Map map, LoadContext ctx) { + if (map.containsKey("sequence") && map.get("sequence") != null) { + result.sequence = (map.get("sequence") instanceof Number n ? n.longValue() : Long.parseLong(String.valueOf(map.get("sequence")))); + } + if (map.containsKey("id") && map.get("id") != null) { + result.id = String.valueOf(map.get("id")); + } + if (map.containsKey("timestamp") && map.get("timestamp") != null) { + result.timestamp = String.valueOf(map.get("timestamp")); + } + if (map.containsKey("sessionId") && map.get("sessionId") != null) { + result.sessionId = String.valueOf(map.get("sessionId")); + } + if (map.containsKey("turnId") && map.get("turnId") != null) { + result.turnId = String.valueOf(map.get("turnId")); + } + if (map.containsKey("runId") && map.get("runId") != null) { + result.runId = String.valueOf(map.get("runId")); + } + if (map.containsKey("parentRunId") && map.get("parentRunId") != null) { + result.parentRunId = String.valueOf(map.get("parentRunId")); + } + if (map.containsKey("delegationDepth") && map.get("delegationDepth") != null) { + result.delegationDepth = (map.get("delegationDepth") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("delegationDepth")))); + } + if (map.containsKey("invocationId") && map.get("invocationId") != null) { + result.invocationId = String.valueOf(map.get("invocationId")); + } + if (map.containsKey("iteration") && map.get("iteration") != null) { + result.iteration = (map.get("iteration") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("iteration")))); + } + if (map.containsKey("kind") && map.get("kind") != null) { + result.kind = EngineEventKind.fromValue(String.valueOf(map.get("kind"))); + } + if (map.containsKey("payload") && map.get("payload") != null) { + result.payload = map.get("payload"); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + EngineEvent obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.sequence != null) result.put("sequence", serializeScalar(obj.sequence)); + if (obj.id != null) result.put("id", serializeScalar(obj.id)); + if (obj.timestamp != null) result.put("timestamp", serializeScalar(obj.timestamp)); + if (obj.sessionId != null) result.put("sessionId", serializeScalar(obj.sessionId)); + if (obj.turnId != null) result.put("turnId", serializeScalar(obj.turnId)); + if (obj.runId != null) result.put("runId", serializeScalar(obj.runId)); + if (obj.parentRunId != null) result.put("parentRunId", serializeScalar(obj.parentRunId)); + if (obj.delegationDepth != null) result.put("delegationDepth", serializeScalar(obj.delegationDepth)); + if (obj.invocationId != null) result.put("invocationId", serializeScalar(obj.invocationId)); + if (obj.iteration != null) result.put("iteration", serializeScalar(obj.iteration)); + result.put("kind", obj.kind.value); + if (obj.payload != null) result.put("payload", serializeScalar(obj.payload)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static EngineEvent fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static EngineEvent fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static EngineEvent fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static EngineEvent fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/EngineEventKind.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/EngineEventKind.java new file mode 100644 index 000000000..65e6f1e08 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/EngineEventKind.java @@ -0,0 +1,39 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +public enum EngineEventKind { + TURN_STARTED("turn_started"), + POLICY_APPLIED("policy_applied"), + CONTEXT_PREPARED("context_prepared"), + MODEL_INVOCATION_STARTED("model_invocation_started"), + MODEL_INVOCATION_COMPLETED("model_invocation_completed"), + MODEL_INVOCATION_FAILED("model_invocation_failed"), + MODEL_RECONCILIATION_REQUIRED("model_reconciliation_required"), + MODEL_INVOCATION_RECONCILED("model_invocation_reconciled"), + PERMISSION_REQUESTED("permission_requested"), + PERMISSION_RESOLVED("permission_resolved"), + TOOL_EXECUTION_STARTED("tool_execution_started"), + TOOL_EXECUTION_COMPLETED("tool_execution_completed"), + TOOL_RESULT_COMMITTED("tool_result_committed"), + TOOL_RESULT_RECONCILED("tool_result_reconciled"), + CONVERSATION_UPDATED("conversation_updated"), + CHECKPOINT_CREATED("checkpoint_created"), + TURN_COMMITTED("turn_committed"), + TURN_CANCELLED("turn_cancelled"), + TURN_FAILED("turn_failed"), + TURN_RECONCILIATION_REQUIRED("turn_reconciliation_required"), + POST_COMMIT_STARTED("post_commit_started"), + POST_COMMIT_COMPLETED("post_commit_completed"), + POST_COMMIT_FAILED("post_commit_failed"), + ; + + public final String value; + EngineEventKind(String value) { this.value = value; } + public static EngineEventKind fromValue(String value) { + for (EngineEventKind item : values()) { + if (item.value.equals(value)) return item; + } + return valueOf(value.toUpperCase().replace("-", "_")); + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/EnginePermissionDecision.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/EnginePermissionDecision.java new file mode 100644 index 000000000..55e7b447e --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/EnginePermissionDecision.java @@ -0,0 +1,92 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class EnginePermissionDecision { + public static final String SHORTHAND_PROPERTY = null; + + public Boolean approved = false; + public String reason = null; + public Map metadata = null; + + public EnginePermissionDecision() { } + + @SuppressWarnings("unchecked") + public static EnginePermissionDecision load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new EnginePermissionDecision()); + } + EnginePermissionDecision result = new EnginePermissionDecision(); + EnginePermissionDecision.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(EnginePermissionDecision result, Map map, LoadContext ctx) { + if (map.containsKey("approved") && map.get("approved") != null) { + result.approved = (map.get("approved") instanceof Boolean b ? b : Boolean.parseBoolean(String.valueOf(map.get("approved")))); + } + if (map.containsKey("reason") && map.get("reason") != null) { + result.reason = String.valueOf(map.get("reason")); + } + if (map.containsKey("metadata") && map.get("metadata") != null) { + if (map.get("metadata") instanceof Map dict) { + result.metadata = copyMap(dict); + } + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + EnginePermissionDecision obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.approved != null) result.put("approved", serializeScalar(obj.approved)); + if (obj.reason != null) result.put("reason", serializeScalar(obj.reason)); + if (obj.metadata != null) result.put("metadata", serializeScalar(obj.metadata)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static EnginePermissionDecision fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static EnginePermissionDecision fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static EnginePermissionDecision fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static EnginePermissionDecision fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/EngineTurnStatus.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/EngineTurnStatus.java new file mode 100644 index 000000000..dfc8d6fbe --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/EngineTurnStatus.java @@ -0,0 +1,20 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +public enum EngineTurnStatus { + SUCCESS("success"), + FAILED("failed"), + CANCELLED("cancelled"), + RECONCILIATION_REQUIRED("reconciliation_required"), + ; + + public final String value; + EngineTurnStatus(String value) { this.value = value; } + public static EngineTurnStatus fromValue(String value) { + for (EngineTurnStatus item : values()) { + if (item.value.equals(value)) return item; + } + return valueOf(value.toUpperCase().replace("-", "_")); + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ErrorChunk.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ErrorChunk.java new file mode 100644 index 000000000..66e86e50d --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ErrorChunk.java @@ -0,0 +1,83 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class ErrorChunk extends StreamChunk { + public static final String SHORTHAND_PROPERTY = null; + + public String message = ""; + + public ErrorChunk() { + this.kind = "error"; + } + + @SuppressWarnings("unchecked") + public static ErrorChunk load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new ErrorChunk()); + } + ErrorChunk result = new ErrorChunk(); + ErrorChunk.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(ErrorChunk result, Map map, LoadContext ctx) { + StreamChunk.loadBaseInto(result, map, ctx); + if (map.containsKey("message") && map.get("message") != null) { + result.message = String.valueOf(map.get("message")); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + ErrorChunk obj = ctx.processObject(this); + Map result = super.save(ctx); + if (obj.message != null) result.put("message", serializeScalar(obj.message)); + return result; + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static ErrorChunk fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static ErrorChunk fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static ErrorChunk fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static ErrorChunk fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ErrorEventPayload.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ErrorEventPayload.java new file mode 100644 index 000000000..c4a3902f6 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ErrorEventPayload.java @@ -0,0 +1,90 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class ErrorEventPayload { + public static final String SHORTHAND_PROPERTY = null; + + public String message = ""; + public String errorKind = null; + public String phase = null; + + public ErrorEventPayload() { } + + @SuppressWarnings("unchecked") + public static ErrorEventPayload load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new ErrorEventPayload()); + } + ErrorEventPayload result = new ErrorEventPayload(); + ErrorEventPayload.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(ErrorEventPayload result, Map map, LoadContext ctx) { + if (map.containsKey("message") && map.get("message") != null) { + result.message = String.valueOf(map.get("message")); + } + if (map.containsKey("errorKind") && map.get("errorKind") != null) { + result.errorKind = String.valueOf(map.get("errorKind")); + } + if (map.containsKey("phase") && map.get("phase") != null) { + result.phase = String.valueOf(map.get("phase")); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + ErrorEventPayload obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.message != null) result.put("message", serializeScalar(obj.message)); + if (obj.errorKind != null) result.put("errorKind", serializeScalar(obj.errorKind)); + if (obj.phase != null) result.put("phase", serializeScalar(obj.phase)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static ErrorEventPayload fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static ErrorEventPayload fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static ErrorEventPayload fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static ErrorEventPayload fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/EventJournalWriter.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/EventJournalWriter.java new file mode 100644 index 000000000..b4edb8e49 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/EventJournalWriter.java @@ -0,0 +1,14 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public interface EventJournalWriter { + Boolean appendTurn(TurnEvent turnEvent); + Boolean appendSession(SessionEvent sessionEvent); + Boolean close(SessionSummary summary); +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/EventSink.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/EventSink.java new file mode 100644 index 000000000..134e756f5 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/EventSink.java @@ -0,0 +1,13 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public interface EventSink { + Boolean emitTurn(TurnEvent turnEvent); + Boolean emitSession(SessionEvent sessionEvent); +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Executor.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Executor.java new file mode 100644 index 000000000..d9627be31 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Executor.java @@ -0,0 +1,14 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public interface Executor { + Object execute(Prompty agent, List messages); + Object executeStream(Prompty agent, List messages); + List formatToolMessages(Object rawResponse, List toolCalls, List toolResults, String textContent); +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/FileNotFoundError.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/FileNotFoundError.java new file mode 100644 index 000000000..515b8039b --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/FileNotFoundError.java @@ -0,0 +1,85 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class FileNotFoundError { + public static final String SHORTHAND_PROPERTY = null; + + public String message = ""; + public String path = ""; + + public FileNotFoundError() { } + + @SuppressWarnings("unchecked") + public static FileNotFoundError load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new FileNotFoundError()); + } + FileNotFoundError result = new FileNotFoundError(); + FileNotFoundError.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(FileNotFoundError result, Map map, LoadContext ctx) { + if (map.containsKey("message") && map.get("message") != null) { + result.message = String.valueOf(map.get("message")); + } + if (map.containsKey("path") && map.get("path") != null) { + result.path = String.valueOf(map.get("path")); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + FileNotFoundError obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.message != null) result.put("message", serializeScalar(obj.message)); + if (obj.path != null) result.put("path", serializeScalar(obj.path)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static FileNotFoundError fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static FileNotFoundError fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static FileNotFoundError fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static FileNotFoundError fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/FilePart.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/FilePart.java new file mode 100644 index 000000000..2263a6809 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/FilePart.java @@ -0,0 +1,88 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class FilePart extends ContentPart { + public static final String SHORTHAND_PROPERTY = null; + + public String source = ""; + public String mediaType = null; + + public FilePart() { + this.kind = "file"; + } + + @SuppressWarnings("unchecked") + public static FilePart load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new FilePart()); + } + FilePart result = new FilePart(); + FilePart.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(FilePart result, Map map, LoadContext ctx) { + ContentPart.loadBaseInto(result, map, ctx); + if (map.containsKey("source") && map.get("source") != null) { + result.source = String.valueOf(map.get("source")); + } + if (map.containsKey("mediaType") && map.get("mediaType") != null) { + result.mediaType = String.valueOf(map.get("mediaType")); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + FilePart obj = ctx.processObject(this); + Map result = super.save(ctx); + if (obj.source != null) result.put("source", serializeScalar(obj.source)); + if (obj.mediaType != null) result.put("mediaType", serializeScalar(obj.mediaType)); + return result; + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static FilePart fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static FilePart fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static FilePart fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static FilePart fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/FinalOutputPolicyRequest.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/FinalOutputPolicyRequest.java new file mode 100644 index 000000000..9a89ecd6b --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/FinalOutputPolicyRequest.java @@ -0,0 +1,110 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class FinalOutputPolicyRequest { + public static final String SHORTHAND_PROPERTY = null; + + public String sessionId = ""; + public String turnId = ""; + public Integer iteration = 0; + public List messages = new ArrayList<>(); + public Object output = null; + public Object inputs = null; + + public FinalOutputPolicyRequest() { } + + @SuppressWarnings("unchecked") + public static FinalOutputPolicyRequest load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new FinalOutputPolicyRequest()); + } + FinalOutputPolicyRequest result = new FinalOutputPolicyRequest(); + FinalOutputPolicyRequest.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(FinalOutputPolicyRequest result, Map map, LoadContext ctx) { + if (map.containsKey("sessionId") && map.get("sessionId") != null) { + result.sessionId = String.valueOf(map.get("sessionId")); + } + if (map.containsKey("turnId") && map.get("turnId") != null) { + result.turnId = String.valueOf(map.get("turnId")); + } + if (map.containsKey("iteration") && map.get("iteration") != null) { + result.iteration = (map.get("iteration") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("iteration")))); + } + if (map.containsKey("messages") && map.get("messages") != null) { + result.messages = ModelCollections.loadList( + map.get("messages"), "messages", Message.SHORTHAND_PROPERTY, Message::load, ctx); + } + if (map.containsKey("output") && map.get("output") != null) { + result.output = map.get("output"); + } + if (map.containsKey("inputs") && map.get("inputs") != null) { + result.inputs = map.get("inputs"); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + FinalOutputPolicyRequest obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.sessionId != null) result.put("sessionId", serializeScalar(obj.sessionId)); + if (obj.turnId != null) result.put("turnId", serializeScalar(obj.turnId)); + if (obj.iteration != null) result.put("iteration", serializeScalar(obj.iteration)); + if (obj.messages != null) { + List items = new ArrayList<>(); + for (Message item : obj.messages) items.add(item.save(ctx)); + result.put("messages", items); + } + if (obj.output != null) result.put("output", serializeScalar(obj.output)); + if (obj.inputs != null) result.put("inputs", serializeScalar(obj.inputs)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static FinalOutputPolicyRequest fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static FinalOutputPolicyRequest fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static FinalOutputPolicyRequest fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static FinalOutputPolicyRequest fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/FinalOutputPolicyResult.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/FinalOutputPolicyResult.java new file mode 100644 index 000000000..5d978d922 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/FinalOutputPolicyResult.java @@ -0,0 +1,87 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class FinalOutputPolicyResult { + public static final String SHORTHAND_PROPERTY = null; + + public Object output = null; + public Map metadata = null; + + public FinalOutputPolicyResult() { } + + @SuppressWarnings("unchecked") + public static FinalOutputPolicyResult load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new FinalOutputPolicyResult()); + } + FinalOutputPolicyResult result = new FinalOutputPolicyResult(); + FinalOutputPolicyResult.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(FinalOutputPolicyResult result, Map map, LoadContext ctx) { + if (map.containsKey("output") && map.get("output") != null) { + result.output = map.get("output"); + } + if (map.containsKey("metadata") && map.get("metadata") != null) { + if (map.get("metadata") instanceof Map dict) { + result.metadata = copyMap(dict); + } + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + FinalOutputPolicyResult obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.output != null) result.put("output", serializeScalar(obj.output)); + if (obj.metadata != null) result.put("metadata", serializeScalar(obj.metadata)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static FinalOutputPolicyResult fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static FinalOutputPolicyResult fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static FinalOutputPolicyResult fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static FinalOutputPolicyResult fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/FormatConfig.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/FormatConfig.java new file mode 100644 index 000000000..04a6cc235 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/FormatConfig.java @@ -0,0 +1,97 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class FormatConfig { + public static final String SHORTHAND_PROPERTY = "kind"; + + public String kind = "*"; + public Boolean strict = null; + public Map options = null; + + public FormatConfig() { } + + @SuppressWarnings("unchecked") + public static FormatConfig load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (data instanceof String) { + FormatConfig result = new FormatConfig(); + result.kind = String.valueOf(data); + return ctx.processOutput(result); + } + if (!(data instanceof Map map)) { + return ctx.processOutput(new FormatConfig()); + } + FormatConfig result = new FormatConfig(); + FormatConfig.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(FormatConfig result, Map map, LoadContext ctx) { + if (map.containsKey("kind") && map.get("kind") != null) { + result.kind = String.valueOf(map.get("kind")); + } + if (map.containsKey("strict") && map.get("strict") != null) { + result.strict = (map.get("strict") instanceof Boolean b ? b : Boolean.parseBoolean(String.valueOf(map.get("strict")))); + } + if (map.containsKey("options") && map.get("options") != null) { + if (map.get("options") instanceof Map dict) { + result.options = copyMap(dict); + } + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + FormatConfig obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.kind != null) result.put("kind", serializeScalar(obj.kind)); + if (obj.strict != null) result.put("strict", serializeScalar(obj.strict)); + if (obj.options != null) result.put("options", serializeScalar(obj.options)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static FormatConfig fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static FormatConfig fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static FormatConfig fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static FormatConfig fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/FoundryConnection.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/FoundryConnection.java new file mode 100644 index 000000000..c110a134e --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/FoundryConnection.java @@ -0,0 +1,93 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class FoundryConnection extends Connection { + public static final String SHORTHAND_PROPERTY = null; + + public String endpoint = ""; + public String name = null; + public String connectionType = null; + + public FoundryConnection() { + this.kind = "foundry"; + } + + @SuppressWarnings("unchecked") + public static FoundryConnection load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new FoundryConnection()); + } + FoundryConnection result = new FoundryConnection(); + FoundryConnection.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(FoundryConnection result, Map map, LoadContext ctx) { + Connection.loadBaseInto(result, map, ctx); + if (map.containsKey("endpoint") && map.get("endpoint") != null) { + result.endpoint = String.valueOf(map.get("endpoint")); + } + if (map.containsKey("name") && map.get("name") != null) { + result.name = String.valueOf(map.get("name")); + } + if (map.containsKey("connectionType") && map.get("connectionType") != null) { + result.connectionType = String.valueOf(map.get("connectionType")); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + FoundryConnection obj = ctx.processObject(this); + Map result = super.save(ctx); + if (obj.endpoint != null) result.put("endpoint", serializeScalar(obj.endpoint)); + if (obj.name != null) result.put("name", serializeScalar(obj.name)); + if (obj.connectionType != null) result.put("connectionType", serializeScalar(obj.connectionType)); + return result; + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static FoundryConnection fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static FoundryConnection fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static FoundryConnection fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static FoundryConnection fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/FunctionTool.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/FunctionTool.java new file mode 100644 index 000000000..8f5a89e79 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/FunctionTool.java @@ -0,0 +1,92 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class FunctionTool extends Tool { + public static final String SHORTHAND_PROPERTY = null; + + public List parameters = new ArrayList<>(); + public Boolean strict = null; + + public FunctionTool() { + this.kind = "function"; + } + + @SuppressWarnings("unchecked") + public static FunctionTool load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new FunctionTool()); + } + FunctionTool result = new FunctionTool(); + FunctionTool.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(FunctionTool result, Map map, LoadContext ctx) { + Tool.loadBaseInto(result, map, ctx); + if (map.containsKey("parameters") && map.get("parameters") != null) { + result.parameters = ModelCollections.loadList( + map.get("parameters"), "parameters", Property.SHORTHAND_PROPERTY, Property::load, ctx); + } + if (map.containsKey("strict") && map.get("strict") != null) { + result.strict = (map.get("strict") instanceof Boolean b ? b : Boolean.parseBoolean(String.valueOf(map.get("strict")))); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + FunctionTool obj = ctx.processObject(this); + Map result = super.save(ctx); + if (obj.parameters != null) { + result.put("parameters", ModelCollections.saveList( + obj.parameters, Property.SHORTHAND_PROPERTY, item -> item.save(ctx), ctx)); + } + if (obj.strict != null) result.put("strict", serializeScalar(obj.strict)); + return result; + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static FunctionTool fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static FunctionTool fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static FunctionTool fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static FunctionTool fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/GuardrailResult.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/GuardrailResult.java new file mode 100644 index 000000000..0f6f62262 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/GuardrailResult.java @@ -0,0 +1,102 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class GuardrailResult { + public static final String SHORTHAND_PROPERTY = null; + + public Boolean allowed = false; + public String reason = null; + public Object rewrite = null; + + public GuardrailResult() { } + + @SuppressWarnings("unchecked") + public static GuardrailResult load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new GuardrailResult()); + } + GuardrailResult result = new GuardrailResult(); + GuardrailResult.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(GuardrailResult result, Map map, LoadContext ctx) { + if (map.containsKey("allowed") && map.get("allowed") != null) { + result.allowed = (map.get("allowed") instanceof Boolean b ? b : Boolean.parseBoolean(String.valueOf(map.get("allowed")))); + } + if (map.containsKey("reason") && map.get("reason") != null) { + result.reason = String.valueOf(map.get("reason")); + } + if (map.containsKey("rewrite") && map.get("rewrite") != null) { + result.rewrite = map.get("rewrite"); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + GuardrailResult obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.allowed != null) result.put("allowed", serializeScalar(obj.allowed)); + if (obj.reason != null) result.put("reason", serializeScalar(obj.reason)); + if (obj.rewrite != null) result.put("rewrite", serializeScalar(obj.rewrite)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static GuardrailResult fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static GuardrailResult fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static GuardrailResult fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static GuardrailResult fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + public static GuardrailResult rewrite(Object rewrite) { + return new GuardrailResult() {{ this.allowed = true; this.rewrite = rewrite; }}; + } + + public static GuardrailResult deny(String reason) { + return new GuardrailResult() {{ this.allowed = false; this.reason = reason; }}; + } + + public static GuardrailResult allow() { + return new GuardrailResult() {{ this.allowed = true; }}; + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/HarnessContext.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/HarnessContext.java new file mode 100644 index 000000000..b85afb16e --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/HarnessContext.java @@ -0,0 +1,92 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class HarnessContext { + public static final String SHORTHAND_PROPERTY = null; + + public String cwd = null; + public String gitRoot = null; + public Map metadata = null; + + public HarnessContext() { } + + @SuppressWarnings("unchecked") + public static HarnessContext load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new HarnessContext()); + } + HarnessContext result = new HarnessContext(); + HarnessContext.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(HarnessContext result, Map map, LoadContext ctx) { + if (map.containsKey("cwd") && map.get("cwd") != null) { + result.cwd = String.valueOf(map.get("cwd")); + } + if (map.containsKey("gitRoot") && map.get("gitRoot") != null) { + result.gitRoot = String.valueOf(map.get("gitRoot")); + } + if (map.containsKey("metadata") && map.get("metadata") != null) { + if (map.get("metadata") instanceof Map dict) { + result.metadata = copyMap(dict); + } + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + HarnessContext obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.cwd != null) result.put("cwd", serializeScalar(obj.cwd)); + if (obj.gitRoot != null) result.put("gitRoot", serializeScalar(obj.gitRoot)); + if (obj.metadata != null) result.put("metadata", serializeScalar(obj.metadata)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static HarnessContext fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static HarnessContext fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static HarnessContext fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static HarnessContext fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/HookEndPayload.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/HookEndPayload.java new file mode 100644 index 000000000..bc5e21d7f --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/HookEndPayload.java @@ -0,0 +1,117 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class HookEndPayload { + public static final String SHORTHAND_PROPERTY = null; + + public String hookInvocationId = ""; + public String hookType = ""; + public HookEndScope scope = null; + public Boolean success = false; + public Map output = null; + public Double durationMs = null; + public String error = null; + public RedactionMetadata redaction = null; + + public HookEndPayload() { } + + @SuppressWarnings("unchecked") + public static HookEndPayload load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new HookEndPayload()); + } + HookEndPayload result = new HookEndPayload(); + HookEndPayload.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(HookEndPayload result, Map map, LoadContext ctx) { + if (map.containsKey("hookInvocationId") && map.get("hookInvocationId") != null) { + result.hookInvocationId = String.valueOf(map.get("hookInvocationId")); + } + if (map.containsKey("hookType") && map.get("hookType") != null) { + result.hookType = String.valueOf(map.get("hookType")); + } + if (map.containsKey("scope") && map.get("scope") != null) { + result.scope = HookEndScope.fromValue(String.valueOf(map.get("scope"))); + } + if (map.containsKey("success") && map.get("success") != null) { + result.success = (map.get("success") instanceof Boolean b ? b : Boolean.parseBoolean(String.valueOf(map.get("success")))); + } + if (map.containsKey("output") && map.get("output") != null) { + if (map.get("output") instanceof Map dict) { + result.output = copyMap(dict); + } + } + if (map.containsKey("durationMs") && map.get("durationMs") != null) { + result.durationMs = (map.get("durationMs") instanceof Number n ? n.doubleValue() : Double.parseDouble(String.valueOf(map.get("durationMs")))); + } + if (map.containsKey("error") && map.get("error") != null) { + result.error = String.valueOf(map.get("error")); + } + if (map.containsKey("redaction") && map.get("redaction") != null) { + result.redaction = RedactionMetadata.load(map.get("redaction"), ctx); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + HookEndPayload obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.hookInvocationId != null) result.put("hookInvocationId", serializeScalar(obj.hookInvocationId)); + if (obj.hookType != null) result.put("hookType", serializeScalar(obj.hookType)); + if (obj.scope != null) result.put("scope", obj.scope.value); + if (obj.success != null) result.put("success", serializeScalar(obj.success)); + if (obj.output != null) result.put("output", serializeScalar(obj.output)); + if (obj.durationMs != null) result.put("durationMs", serializeScalar(obj.durationMs)); + if (obj.error != null) result.put("error", serializeScalar(obj.error)); + if (obj.redaction != null) result.put("redaction", obj.redaction.save(ctx)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static HookEndPayload fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static HookEndPayload fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static HookEndPayload fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static HookEndPayload fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/HookEndScope.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/HookEndScope.java new file mode 100644 index 000000000..c319a05ca --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/HookEndScope.java @@ -0,0 +1,18 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +public enum HookEndScope { + TURN("turn"), + SESSION("session"), + ; + + public final String value; + HookEndScope(String value) { this.value = value; } + public static HookEndScope fromValue(String value) { + for (HookEndScope item : values()) { + if (item.value.equals(value)) return item; + } + return valueOf(value.toUpperCase().replace("-", "_")); + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/HookStartPayload.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/HookStartPayload.java new file mode 100644 index 000000000..bbafcf36d --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/HookStartPayload.java @@ -0,0 +1,102 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class HookStartPayload { + public static final String SHORTHAND_PROPERTY = null; + + public String hookInvocationId = ""; + public String hookType = ""; + public HookStartScope scope = null; + public Map input = null; + public RedactionMetadata redaction = null; + + public HookStartPayload() { } + + @SuppressWarnings("unchecked") + public static HookStartPayload load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new HookStartPayload()); + } + HookStartPayload result = new HookStartPayload(); + HookStartPayload.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(HookStartPayload result, Map map, LoadContext ctx) { + if (map.containsKey("hookInvocationId") && map.get("hookInvocationId") != null) { + result.hookInvocationId = String.valueOf(map.get("hookInvocationId")); + } + if (map.containsKey("hookType") && map.get("hookType") != null) { + result.hookType = String.valueOf(map.get("hookType")); + } + if (map.containsKey("scope") && map.get("scope") != null) { + result.scope = HookStartScope.fromValue(String.valueOf(map.get("scope"))); + } + if (map.containsKey("input") && map.get("input") != null) { + if (map.get("input") instanceof Map dict) { + result.input = copyMap(dict); + } + } + if (map.containsKey("redaction") && map.get("redaction") != null) { + result.redaction = RedactionMetadata.load(map.get("redaction"), ctx); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + HookStartPayload obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.hookInvocationId != null) result.put("hookInvocationId", serializeScalar(obj.hookInvocationId)); + if (obj.hookType != null) result.put("hookType", serializeScalar(obj.hookType)); + if (obj.scope != null) result.put("scope", obj.scope.value); + if (obj.input != null) result.put("input", serializeScalar(obj.input)); + if (obj.redaction != null) result.put("redaction", obj.redaction.save(ctx)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static HookStartPayload fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static HookStartPayload fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static HookStartPayload fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static HookStartPayload fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/HookStartScope.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/HookStartScope.java new file mode 100644 index 000000000..0143df19d --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/HookStartScope.java @@ -0,0 +1,18 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +public enum HookStartScope { + TURN("turn"), + SESSION("session"), + ; + + public final String value; + HookStartScope(String value) { this.value = value; } + public static HookStartScope fromValue(String value) { + for (HookStartScope item : values()) { + if (item.value.equals(value)) return item; + } + return valueOf(value.toUpperCase().replace("-", "_")); + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/HostPolicyRequest.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/HostPolicyRequest.java new file mode 100644 index 000000000..1e929cffe --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/HostPolicyRequest.java @@ -0,0 +1,110 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class HostPolicyRequest { + public static final String SHORTHAND_PROPERTY = null; + + public String sessionId = ""; + public String turnId = ""; + public Integer iteration = 0; + public List messages = new ArrayList<>(); + public Integer stablePrefixMessages = 0; + public Object inputs = null; + + public HostPolicyRequest() { } + + @SuppressWarnings("unchecked") + public static HostPolicyRequest load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new HostPolicyRequest()); + } + HostPolicyRequest result = new HostPolicyRequest(); + HostPolicyRequest.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(HostPolicyRequest result, Map map, LoadContext ctx) { + if (map.containsKey("sessionId") && map.get("sessionId") != null) { + result.sessionId = String.valueOf(map.get("sessionId")); + } + if (map.containsKey("turnId") && map.get("turnId") != null) { + result.turnId = String.valueOf(map.get("turnId")); + } + if (map.containsKey("iteration") && map.get("iteration") != null) { + result.iteration = (map.get("iteration") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("iteration")))); + } + if (map.containsKey("messages") && map.get("messages") != null) { + result.messages = ModelCollections.loadList( + map.get("messages"), "messages", Message.SHORTHAND_PROPERTY, Message::load, ctx); + } + if (map.containsKey("stablePrefixMessages") && map.get("stablePrefixMessages") != null) { + result.stablePrefixMessages = (map.get("stablePrefixMessages") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("stablePrefixMessages")))); + } + if (map.containsKey("inputs") && map.get("inputs") != null) { + result.inputs = map.get("inputs"); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + HostPolicyRequest obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.sessionId != null) result.put("sessionId", serializeScalar(obj.sessionId)); + if (obj.turnId != null) result.put("turnId", serializeScalar(obj.turnId)); + if (obj.iteration != null) result.put("iteration", serializeScalar(obj.iteration)); + if (obj.messages != null) { + List items = new ArrayList<>(); + for (Message item : obj.messages) items.add(item.save(ctx)); + result.put("messages", items); + } + if (obj.stablePrefixMessages != null) result.put("stablePrefixMessages", serializeScalar(obj.stablePrefixMessages)); + if (obj.inputs != null) result.put("inputs", serializeScalar(obj.inputs)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static HostPolicyRequest fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static HostPolicyRequest fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static HostPolicyRequest fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static HostPolicyRequest fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/HostPolicyResult.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/HostPolicyResult.java new file mode 100644 index 000000000..8f534595d --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/HostPolicyResult.java @@ -0,0 +1,97 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class HostPolicyResult { + public static final String SHORTHAND_PROPERTY = null; + + public List messages = new ArrayList<>(); + public Integer stablePrefixMessages = 0; + public Map metadata = null; + + public HostPolicyResult() { } + + @SuppressWarnings("unchecked") + public static HostPolicyResult load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new HostPolicyResult()); + } + HostPolicyResult result = new HostPolicyResult(); + HostPolicyResult.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(HostPolicyResult result, Map map, LoadContext ctx) { + if (map.containsKey("messages") && map.get("messages") != null) { + result.messages = ModelCollections.loadList( + map.get("messages"), "messages", Message.SHORTHAND_PROPERTY, Message::load, ctx); + } + if (map.containsKey("stablePrefixMessages") && map.get("stablePrefixMessages") != null) { + result.stablePrefixMessages = (map.get("stablePrefixMessages") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("stablePrefixMessages")))); + } + if (map.containsKey("metadata") && map.get("metadata") != null) { + if (map.get("metadata") instanceof Map dict) { + result.metadata = copyMap(dict); + } + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + HostPolicyResult obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.messages != null) { + List items = new ArrayList<>(); + for (Message item : obj.messages) items.add(item.save(ctx)); + result.put("messages", items); + } + if (obj.stablePrefixMessages != null) result.put("stablePrefixMessages", serializeScalar(obj.stablePrefixMessages)); + if (obj.metadata != null) result.put("metadata", serializeScalar(obj.metadata)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static HostPolicyResult fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static HostPolicyResult fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static HostPolicyResult fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static HostPolicyResult fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/HostToolExecutor.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/HostToolExecutor.java new file mode 100644 index 000000000..11c89b724 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/HostToolExecutor.java @@ -0,0 +1,12 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public interface HostToolExecutor { + HostToolResult execute(HostToolRequest request); +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/HostToolRequest.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/HostToolRequest.java new file mode 100644 index 000000000..8664fd58c --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/HostToolRequest.java @@ -0,0 +1,102 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class HostToolRequest { + public static final String SHORTHAND_PROPERTY = null; + + public String requestId = null; + public String toolCallId = null; + public String toolName = ""; + public Map arguments = null; + public String workingDirectory = null; + + public HostToolRequest() { } + + @SuppressWarnings("unchecked") + public static HostToolRequest load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new HostToolRequest()); + } + HostToolRequest result = new HostToolRequest(); + HostToolRequest.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(HostToolRequest result, Map map, LoadContext ctx) { + if (map.containsKey("requestId") && map.get("requestId") != null) { + result.requestId = String.valueOf(map.get("requestId")); + } + if (map.containsKey("toolCallId") && map.get("toolCallId") != null) { + result.toolCallId = String.valueOf(map.get("toolCallId")); + } + if (map.containsKey("toolName") && map.get("toolName") != null) { + result.toolName = String.valueOf(map.get("toolName")); + } + if (map.containsKey("arguments") && map.get("arguments") != null) { + if (map.get("arguments") instanceof Map dict) { + result.arguments = copyMap(dict); + } + } + if (map.containsKey("workingDirectory") && map.get("workingDirectory") != null) { + result.workingDirectory = String.valueOf(map.get("workingDirectory")); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + HostToolRequest obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.requestId != null) result.put("requestId", serializeScalar(obj.requestId)); + if (obj.toolCallId != null) result.put("toolCallId", serializeScalar(obj.toolCallId)); + if (obj.toolName != null) result.put("toolName", serializeScalar(obj.toolName)); + if (obj.arguments != null) result.put("arguments", serializeScalar(obj.arguments)); + if (obj.workingDirectory != null) result.put("workingDirectory", serializeScalar(obj.workingDirectory)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static HostToolRequest fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static HostToolRequest fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static HostToolRequest fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static HostToolRequest fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/HostToolResult.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/HostToolResult.java new file mode 100644 index 000000000..ea51e20ac --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/HostToolResult.java @@ -0,0 +1,122 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class HostToolResult { + public static final String SHORTHAND_PROPERTY = null; + + public String requestId = null; + public String toolCallId = null; + public String toolName = ""; + public Boolean success = false; + public Object result = null; + public Integer exitCode = null; + public Double durationMs = null; + public String errorKind = null; + public Map telemetry = null; + + public HostToolResult() { } + + @SuppressWarnings("unchecked") + public static HostToolResult load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new HostToolResult()); + } + HostToolResult result = new HostToolResult(); + HostToolResult.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(HostToolResult result, Map map, LoadContext ctx) { + if (map.containsKey("requestId") && map.get("requestId") != null) { + result.requestId = String.valueOf(map.get("requestId")); + } + if (map.containsKey("toolCallId") && map.get("toolCallId") != null) { + result.toolCallId = String.valueOf(map.get("toolCallId")); + } + if (map.containsKey("toolName") && map.get("toolName") != null) { + result.toolName = String.valueOf(map.get("toolName")); + } + if (map.containsKey("success") && map.get("success") != null) { + result.success = (map.get("success") instanceof Boolean b ? b : Boolean.parseBoolean(String.valueOf(map.get("success")))); + } + if (map.containsKey("result") && map.get("result") != null) { + result.result = map.get("result"); + } + if (map.containsKey("exitCode") && map.get("exitCode") != null) { + result.exitCode = (map.get("exitCode") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("exitCode")))); + } + if (map.containsKey("durationMs") && map.get("durationMs") != null) { + result.durationMs = (map.get("durationMs") instanceof Number n ? n.doubleValue() : Double.parseDouble(String.valueOf(map.get("durationMs")))); + } + if (map.containsKey("errorKind") && map.get("errorKind") != null) { + result.errorKind = String.valueOf(map.get("errorKind")); + } + if (map.containsKey("telemetry") && map.get("telemetry") != null) { + if (map.get("telemetry") instanceof Map dict) { + result.telemetry = copyMap(dict); + } + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + HostToolResult obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.requestId != null) result.put("requestId", serializeScalar(obj.requestId)); + if (obj.toolCallId != null) result.put("toolCallId", serializeScalar(obj.toolCallId)); + if (obj.toolName != null) result.put("toolName", serializeScalar(obj.toolName)); + if (obj.success != null) result.put("success", serializeScalar(obj.success)); + if (obj.result != null) result.put("result", serializeScalar(obj.result)); + if (obj.exitCode != null) result.put("exitCode", serializeScalar(obj.exitCode)); + if (obj.durationMs != null) result.put("durationMs", serializeScalar(obj.durationMs)); + if (obj.errorKind != null) result.put("errorKind", serializeScalar(obj.errorKind)); + if (obj.telemetry != null) result.put("telemetry", serializeScalar(obj.telemetry)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static HostToolResult fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static HostToolResult fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static HostToolResult fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static HostToolResult fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ImagePart.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ImagePart.java new file mode 100644 index 000000000..03d6fe928 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ImagePart.java @@ -0,0 +1,93 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class ImagePart extends ContentPart { + public static final String SHORTHAND_PROPERTY = null; + + public String source = ""; + public String detail = null; + public String mediaType = null; + + public ImagePart() { + this.kind = "image"; + } + + @SuppressWarnings("unchecked") + public static ImagePart load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new ImagePart()); + } + ImagePart result = new ImagePart(); + ImagePart.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(ImagePart result, Map map, LoadContext ctx) { + ContentPart.loadBaseInto(result, map, ctx); + if (map.containsKey("source") && map.get("source") != null) { + result.source = String.valueOf(map.get("source")); + } + if (map.containsKey("detail") && map.get("detail") != null) { + result.detail = String.valueOf(map.get("detail")); + } + if (map.containsKey("mediaType") && map.get("mediaType") != null) { + result.mediaType = String.valueOf(map.get("mediaType")); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + ImagePart obj = ctx.processObject(this); + Map result = super.save(ctx); + if (obj.source != null) result.put("source", serializeScalar(obj.source)); + if (obj.detail != null) result.put("detail", serializeScalar(obj.detail)); + if (obj.mediaType != null) result.put("mediaType", serializeScalar(obj.mediaType)); + return result; + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static ImagePart fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static ImagePart fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static ImagePart fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static ImagePart fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/InvocationContextDecision.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/InvocationContextDecision.java new file mode 100644 index 000000000..36a64dac7 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/InvocationContextDecision.java @@ -0,0 +1,107 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class InvocationContextDecision { + public static final String SHORTHAND_PROPERTY = null; + + public String candidateId = ""; + public InvocationContextDisposition disposition = InvocationContextDisposition.INCLUDED; + public String reason = ""; + public Integer rank = null; + public Integer estimatedTokens = null; + public Map metadata = null; + + public InvocationContextDecision() { } + + @SuppressWarnings("unchecked") + public static InvocationContextDecision load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new InvocationContextDecision()); + } + InvocationContextDecision result = new InvocationContextDecision(); + InvocationContextDecision.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(InvocationContextDecision result, Map map, LoadContext ctx) { + if (map.containsKey("candidateId") && map.get("candidateId") != null) { + result.candidateId = String.valueOf(map.get("candidateId")); + } + if (map.containsKey("disposition") && map.get("disposition") != null) { + result.disposition = InvocationContextDisposition.fromValue(String.valueOf(map.get("disposition"))); + } + if (map.containsKey("reason") && map.get("reason") != null) { + result.reason = String.valueOf(map.get("reason")); + } + if (map.containsKey("rank") && map.get("rank") != null) { + result.rank = (map.get("rank") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("rank")))); + } + if (map.containsKey("estimatedTokens") && map.get("estimatedTokens") != null) { + result.estimatedTokens = (map.get("estimatedTokens") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("estimatedTokens")))); + } + if (map.containsKey("metadata") && map.get("metadata") != null) { + if (map.get("metadata") instanceof Map dict) { + result.metadata = copyMap(dict); + } + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + InvocationContextDecision obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.candidateId != null) result.put("candidateId", serializeScalar(obj.candidateId)); + result.put("disposition", obj.disposition.value); + if (obj.reason != null) result.put("reason", serializeScalar(obj.reason)); + if (obj.rank != null) result.put("rank", serializeScalar(obj.rank)); + if (obj.estimatedTokens != null) result.put("estimatedTokens", serializeScalar(obj.estimatedTokens)); + if (obj.metadata != null) result.put("metadata", serializeScalar(obj.metadata)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static InvocationContextDecision fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static InvocationContextDecision fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static InvocationContextDecision fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static InvocationContextDecision fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/InvocationContextDisposition.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/InvocationContextDisposition.java new file mode 100644 index 000000000..c679e21f5 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/InvocationContextDisposition.java @@ -0,0 +1,18 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +public enum InvocationContextDisposition { + INCLUDED("included"), + EXCLUDED("excluded"), + ; + + public final String value; + InvocationContextDisposition(String value) { this.value = value; } + public static InvocationContextDisposition fromValue(String value) { + for (InvocationContextDisposition item : values()) { + if (item.value.equals(value)) return item; + } + return valueOf(value.toUpperCase().replace("-", "_")); + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/InvocationContextPortability.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/InvocationContextPortability.java new file mode 100644 index 000000000..84ae7f53f --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/InvocationContextPortability.java @@ -0,0 +1,19 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +public enum InvocationContextPortability { + PORTABLE("portable"), + DELEGATED("delegated"), + OPAQUE("opaque"), + ; + + public final String value; + InvocationContextPortability(String value) { this.value = value; } + public static InvocationContextPortability fromValue(String value) { + for (InvocationContextPortability item : values()) { + if (item.value.equals(value)) return item; + } + return valueOf(value.toUpperCase().replace("-", "_")); + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/InvocationContextState.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/InvocationContextState.java new file mode 100644 index 000000000..372486e1a --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/InvocationContextState.java @@ -0,0 +1,90 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class InvocationContextState { + public static final String SHORTHAND_PROPERTY = null; + + public InvocationContextPortability portability = InvocationContextPortability.fromValue("portable"); + public List delegatedState = null; + + public InvocationContextState() { } + + @SuppressWarnings("unchecked") + public static InvocationContextState load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new InvocationContextState()); + } + InvocationContextState result = new InvocationContextState(); + InvocationContextState.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(InvocationContextState result, Map map, LoadContext ctx) { + if (map.containsKey("portability") && map.get("portability") != null) { + result.portability = InvocationContextPortability.fromValue(String.valueOf(map.get("portability"))); + } + if (map.containsKey("delegatedState") && map.get("delegatedState") != null) { + result.delegatedState = ModelCollections.loadList( + map.get("delegatedState"), "delegatedState", DelegatedStateReference.SHORTHAND_PROPERTY, DelegatedStateReference::load, ctx); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + InvocationContextState obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.portability != null) result.put("portability", obj.portability.value); + if (obj.delegatedState != null) { + List items = new ArrayList<>(); + for (DelegatedStateReference item : obj.delegatedState) items.add(item.save(ctx)); + result.put("delegatedState", items); + } + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static InvocationContextState fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static InvocationContextState fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static InvocationContextState fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static InvocationContextState fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/InvocationUsage.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/InvocationUsage.java new file mode 100644 index 000000000..84ad73214 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/InvocationUsage.java @@ -0,0 +1,116 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class InvocationUsage { + public static final String SHORTHAND_PROPERTY = null; + + public Long inputTokens = 0L; + public Long outputTokens = 0L; + public Long totalTokens = 0L; + + public InvocationUsage() { } + + @SuppressWarnings("unchecked") + public static InvocationUsage load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new InvocationUsage()); + } + InvocationUsage result = new InvocationUsage(); + InvocationUsage.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(InvocationUsage result, Map map, LoadContext ctx) { + if (map.containsKey("inputTokens") && map.get("inputTokens") != null) { + result.inputTokens = (map.get("inputTokens") instanceof Number n ? n.longValue() : Long.parseLong(String.valueOf(map.get("inputTokens")))); + } + if (map.containsKey("outputTokens") && map.get("outputTokens") != null) { + result.outputTokens = (map.get("outputTokens") instanceof Number n ? n.longValue() : Long.parseLong(String.valueOf(map.get("outputTokens")))); + } + if (map.containsKey("totalTokens") && map.get("totalTokens") != null) { + result.totalTokens = (map.get("totalTokens") instanceof Number n ? n.longValue() : Long.parseLong(String.valueOf(map.get("totalTokens")))); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + InvocationUsage obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.inputTokens != null) result.put("inputTokens", serializeScalar(obj.inputTokens)); + if (obj.outputTokens != null) result.put("outputTokens", serializeScalar(obj.outputTokens)); + if (obj.totalTokens != null) result.put("totalTokens", serializeScalar(obj.totalTokens)); + return ctx.processDict(result); + } + + public Map toWire(String provider) { + Map result = new LinkedHashMap<>(); + String target = provider == null ? "" : provider; + { + String wireName = "inputTokens"; + boolean include = target.isEmpty(); + if (target.equals("openai")) { wireName = "prompt_tokens"; include = true; } + if (target.equals("anthropic")) { wireName = "input_tokens"; include = true; } + if (include && this.inputTokens != null) result.put(wireName, serializeScalar(this.inputTokens)); + } + { + String wireName = "outputTokens"; + boolean include = target.isEmpty(); + if (target.equals("openai")) { wireName = "completion_tokens"; include = true; } + if (target.equals("anthropic")) { wireName = "output_tokens"; include = true; } + if (include && this.outputTokens != null) result.put(wireName, serializeScalar(this.outputTokens)); + } + { + String wireName = "totalTokens"; + boolean include = target.isEmpty(); + if (target.equals("openai")) { wireName = "total_tokens"; include = true; } + if (include && this.totalTokens != null) result.put(wireName, serializeScalar(this.totalTokens)); + } + return result; + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static InvocationUsage fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static InvocationUsage fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static InvocationUsage fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static InvocationUsage fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/InvokerError.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/InvokerError.java new file mode 100644 index 000000000..7652fb5b4 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/InvokerError.java @@ -0,0 +1,90 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class InvokerError { + public static final String SHORTHAND_PROPERTY = null; + + public String message = ""; + public String component = ""; + public String key = ""; + + public InvokerError() { } + + @SuppressWarnings("unchecked") + public static InvokerError load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new InvokerError()); + } + InvokerError result = new InvokerError(); + InvokerError.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(InvokerError result, Map map, LoadContext ctx) { + if (map.containsKey("message") && map.get("message") != null) { + result.message = String.valueOf(map.get("message")); + } + if (map.containsKey("component") && map.get("component") != null) { + result.component = String.valueOf(map.get("component")); + } + if (map.containsKey("key") && map.get("key") != null) { + result.key = String.valueOf(map.get("key")); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + InvokerError obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.message != null) result.put("message", serializeScalar(obj.message)); + if (obj.component != null) result.put("component", serializeScalar(obj.component)); + if (obj.key != null) result.put("key", serializeScalar(obj.key)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static InvokerError fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static InvokerError fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static InvokerError fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static InvokerError fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/LlmCompletePayload.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/LlmCompletePayload.java new file mode 100644 index 000000000..07235a3a7 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/LlmCompletePayload.java @@ -0,0 +1,95 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class LlmCompletePayload { + public static final String SHORTHAND_PROPERTY = null; + + public String requestId = null; + public String serviceRequestId = null; + public TokenUsage usage = null; + public Double durationMs = null; + + public LlmCompletePayload() { } + + @SuppressWarnings("unchecked") + public static LlmCompletePayload load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new LlmCompletePayload()); + } + LlmCompletePayload result = new LlmCompletePayload(); + LlmCompletePayload.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(LlmCompletePayload result, Map map, LoadContext ctx) { + if (map.containsKey("requestId") && map.get("requestId") != null) { + result.requestId = String.valueOf(map.get("requestId")); + } + if (map.containsKey("serviceRequestId") && map.get("serviceRequestId") != null) { + result.serviceRequestId = String.valueOf(map.get("serviceRequestId")); + } + if (map.containsKey("usage") && map.get("usage") != null) { + result.usage = TokenUsage.load(map.get("usage"), ctx); + } + if (map.containsKey("durationMs") && map.get("durationMs") != null) { + result.durationMs = (map.get("durationMs") instanceof Number n ? n.doubleValue() : Double.parseDouble(String.valueOf(map.get("durationMs")))); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + LlmCompletePayload obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.requestId != null) result.put("requestId", serializeScalar(obj.requestId)); + if (obj.serviceRequestId != null) result.put("serviceRequestId", serializeScalar(obj.serviceRequestId)); + if (obj.usage != null) result.put("usage", obj.usage.save(ctx)); + if (obj.durationMs != null) result.put("durationMs", serializeScalar(obj.durationMs)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static LlmCompletePayload fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static LlmCompletePayload fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static LlmCompletePayload fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static LlmCompletePayload fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/LlmStartPayload.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/LlmStartPayload.java new file mode 100644 index 000000000..a0cc9b910 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/LlmStartPayload.java @@ -0,0 +1,95 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class LlmStartPayload { + public static final String SHORTHAND_PROPERTY = null; + + public String provider = null; + public String modelId = null; + public Integer messageCount = null; + public Integer attempt = null; + + public LlmStartPayload() { } + + @SuppressWarnings("unchecked") + public static LlmStartPayload load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new LlmStartPayload()); + } + LlmStartPayload result = new LlmStartPayload(); + LlmStartPayload.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(LlmStartPayload result, Map map, LoadContext ctx) { + if (map.containsKey("provider") && map.get("provider") != null) { + result.provider = String.valueOf(map.get("provider")); + } + if (map.containsKey("modelId") && map.get("modelId") != null) { + result.modelId = String.valueOf(map.get("modelId")); + } + if (map.containsKey("messageCount") && map.get("messageCount") != null) { + result.messageCount = (map.get("messageCount") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("messageCount")))); + } + if (map.containsKey("attempt") && map.get("attempt") != null) { + result.attempt = (map.get("attempt") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("attempt")))); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + LlmStartPayload obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.provider != null) result.put("provider", serializeScalar(obj.provider)); + if (obj.modelId != null) result.put("modelId", serializeScalar(obj.modelId)); + if (obj.messageCount != null) result.put("messageCount", serializeScalar(obj.messageCount)); + if (obj.attempt != null) result.put("attempt", serializeScalar(obj.attempt)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static LlmStartPayload fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static LlmStartPayload fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static LlmStartPayload fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static LlmStartPayload fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/LoadContext.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/LoadContext.java new file mode 100644 index 000000000..15171e12a --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/LoadContext.java @@ -0,0 +1,29 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.Map; +import java.util.function.Function; + +public final class LoadContext { + private final Function preProcess; + private final Function postProcess; + + public LoadContext() { + this(null, null); + } + + public LoadContext(Function preProcess, Function postProcess) { + this.preProcess = preProcess; + this.postProcess = postProcess; + } + + public Object processInput(Object value) { + return preProcess == null ? value : preProcess.apply(value); + } + + @SuppressWarnings("unchecked") + public T processOutput(T value) { + return postProcess == null ? value : (T) postProcess.apply(value); + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/McpApprovalMode.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/McpApprovalMode.java new file mode 100644 index 000000000..3670cb2b5 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/McpApprovalMode.java @@ -0,0 +1,105 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class McpApprovalMode { + public static final String SHORTHAND_PROPERTY = "kind"; + + public McpApprovalModeKind kind = McpApprovalModeKind.ALWAYS; + public List alwaysRequireApprovalTools = null; + public List neverRequireApprovalTools = null; + + public McpApprovalMode() { } + + @SuppressWarnings("unchecked") + public static McpApprovalMode load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (data instanceof String) { + McpApprovalMode result = new McpApprovalMode(); + result.kind = McpApprovalModeKind.fromValue(String.valueOf(data)); + return ctx.processOutput(result); + } + if (!(data instanceof Map map)) { + return ctx.processOutput(new McpApprovalMode()); + } + McpApprovalMode result = new McpApprovalMode(); + McpApprovalMode.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(McpApprovalMode result, Map map, LoadContext ctx) { + if (map.containsKey("kind") && map.get("kind") != null) { + result.kind = McpApprovalModeKind.fromValue(String.valueOf(map.get("kind"))); + } + if (map.containsKey("alwaysRequireApprovalTools") && map.get("alwaysRequireApprovalTools") != null) { + result.alwaysRequireApprovalTools = new ArrayList<>(); + if (map.get("alwaysRequireApprovalTools") instanceof Iterable values) { + for (Object item : values) { + result.alwaysRequireApprovalTools.add(String.valueOf(item)); + } + } + } + if (map.containsKey("neverRequireApprovalTools") && map.get("neverRequireApprovalTools") != null) { + result.neverRequireApprovalTools = new ArrayList<>(); + if (map.get("neverRequireApprovalTools") instanceof Iterable values) { + for (Object item : values) { + result.neverRequireApprovalTools.add(String.valueOf(item)); + } + } + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + McpApprovalMode obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + result.put("kind", obj.kind.value); + if (obj.alwaysRequireApprovalTools != null) result.put("alwaysRequireApprovalTools", new ArrayList<>(obj.alwaysRequireApprovalTools)); + if (obj.neverRequireApprovalTools != null) result.put("neverRequireApprovalTools", new ArrayList<>(obj.neverRequireApprovalTools)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static McpApprovalMode fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static McpApprovalMode fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static McpApprovalMode fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static McpApprovalMode fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/McpApprovalModeKind.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/McpApprovalModeKind.java new file mode 100644 index 000000000..b5b60447b --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/McpApprovalModeKind.java @@ -0,0 +1,19 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +public enum McpApprovalModeKind { + ALWAYS("always"), + NEVER("never"), + SPECIFY("specify"), + ; + + public final String value; + McpApprovalModeKind(String value) { this.value = value; } + public static McpApprovalModeKind fromValue(String value) { + for (McpApprovalModeKind item : values()) { + if (item.value.equals(value)) return item; + } + return valueOf(value.toUpperCase().replace("-", "_")); + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/McpTool.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/McpTool.java new file mode 100644 index 000000000..ac790f8a2 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/McpTool.java @@ -0,0 +1,108 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class McpTool extends Tool { + public static final String SHORTHAND_PROPERTY = null; + + public Connection connection = null; + public String serverName = ""; + public String serverDescription = null; + public McpApprovalMode approvalMode = null; + public List allowedTools = null; + + public McpTool() { + this.kind = "mcp"; + } + + @SuppressWarnings("unchecked") + public static McpTool load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new McpTool()); + } + McpTool result = new McpTool(); + McpTool.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(McpTool result, Map map, LoadContext ctx) { + Tool.loadBaseInto(result, map, ctx); + if (map.containsKey("connection") && map.get("connection") != null) { + result.connection = Connection.load(map.get("connection"), ctx); + } + if (map.containsKey("serverName") && map.get("serverName") != null) { + result.serverName = String.valueOf(map.get("serverName")); + } + if (map.containsKey("serverDescription") && map.get("serverDescription") != null) { + result.serverDescription = String.valueOf(map.get("serverDescription")); + } + if (map.containsKey("approvalMode") && map.get("approvalMode") != null) { + result.approvalMode = McpApprovalMode.load(map.get("approvalMode"), ctx); + } + if (map.containsKey("allowedTools") && map.get("allowedTools") != null) { + result.allowedTools = new ArrayList<>(); + if (map.get("allowedTools") instanceof Iterable values) { + for (Object item : values) { + result.allowedTools.add(String.valueOf(item)); + } + } + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + McpTool obj = ctx.processObject(this); + Map result = super.save(ctx); + if (obj.connection != null) result.put("connection", obj.connection.save(ctx)); + if (obj.serverName != null) result.put("serverName", serializeScalar(obj.serverName)); + if (obj.serverDescription != null) result.put("serverDescription", serializeScalar(obj.serverDescription)); + if (obj.approvalMode != null) result.put("approvalMode", obj.approvalMode.save(ctx)); + if (obj.allowedTools != null) result.put("allowedTools", new ArrayList<>(obj.allowedTools)); + return result; + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static McpTool fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static McpTool fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static McpTool fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static McpTool fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/MemoryCategory.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/MemoryCategory.java new file mode 100644 index 000000000..6aee34d11 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/MemoryCategory.java @@ -0,0 +1,19 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +public enum MemoryCategory { + CORE("core"), + ARCHIVAL("archival"), + INSIGHT("insight"), + ; + + public final String value; + MemoryCategory(String value) { this.value = value; } + public static MemoryCategory fromValue(String value) { + for (MemoryCategory item : values()) { + if (item.value.equals(value)) return item; + } + return valueOf(value.toUpperCase().replace("-", "_")); + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/MemoryEntry.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/MemoryEntry.java new file mode 100644 index 000000000..43f4ce7c8 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/MemoryEntry.java @@ -0,0 +1,100 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class MemoryEntry { + public static final String SHORTHAND_PROPERTY = null; + + public String content = ""; + public MemoryCategory category = MemoryCategory.fromValue("core"); + public String createdAt = null; + public List tags = null; + + public MemoryEntry() { } + + @SuppressWarnings("unchecked") + public static MemoryEntry load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new MemoryEntry()); + } + MemoryEntry result = new MemoryEntry(); + MemoryEntry.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(MemoryEntry result, Map map, LoadContext ctx) { + if (map.containsKey("content") && map.get("content") != null) { + result.content = String.valueOf(map.get("content")); + } + if (map.containsKey("category") && map.get("category") != null) { + result.category = MemoryCategory.fromValue(String.valueOf(map.get("category"))); + } + if (map.containsKey("createdAt") && map.get("createdAt") != null) { + result.createdAt = String.valueOf(map.get("createdAt")); + } + if (map.containsKey("tags") && map.get("tags") != null) { + result.tags = new ArrayList<>(); + if (map.get("tags") instanceof Iterable values) { + for (Object item : values) { + result.tags.add(String.valueOf(item)); + } + } + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + MemoryEntry obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.content != null) result.put("content", serializeScalar(obj.content)); + if (obj.category != null) result.put("category", obj.category.value); + if (obj.createdAt != null) result.put("createdAt", serializeScalar(obj.createdAt)); + if (obj.tags != null) result.put("tags", new ArrayList<>(obj.tags)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static MemoryEntry fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static MemoryEntry fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static MemoryEntry fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static MemoryEntry fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/MemoryStore.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/MemoryStore.java new file mode 100644 index 000000000..6e057db9a --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/MemoryStore.java @@ -0,0 +1,85 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class MemoryStore { + public static final String SHORTHAND_PROPERTY = null; + + public List entries = new ArrayList<>(); + + public MemoryStore() { } + + @SuppressWarnings("unchecked") + public static MemoryStore load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new MemoryStore()); + } + MemoryStore result = new MemoryStore(); + MemoryStore.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(MemoryStore result, Map map, LoadContext ctx) { + if (map.containsKey("entries") && map.get("entries") != null) { + result.entries = ModelCollections.loadList( + map.get("entries"), "entries", MemoryEntry.SHORTHAND_PROPERTY, MemoryEntry::load, ctx); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + MemoryStore obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.entries != null) { + List items = new ArrayList<>(); + for (MemoryEntry item : obj.entries) items.add(item.save(ctx)); + result.put("entries", items); + } + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static MemoryStore fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static MemoryStore fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static MemoryStore fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static MemoryStore fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Message.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Message.java new file mode 100644 index 000000000..5d3eabed4 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Message.java @@ -0,0 +1,117 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class Message { + public static final String SHORTHAND_PROPERTY = null; + + public Role role = Role.fromValue("user"); + public List parts = new ArrayList<>(); + public Map metadata = new LinkedHashMap<>(); + + public Message() { } + + @SuppressWarnings("unchecked") + public static Message load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new Message()); + } + Message result = new Message(); + Message.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(Message result, Map map, LoadContext ctx) { + if (map.containsKey("role") && map.get("role") != null) { + result.role = Role.fromValue(String.valueOf(map.get("role"))); + } + if (map.containsKey("parts") && map.get("parts") != null) { + result.parts = ModelCollections.loadList( + map.get("parts"), "parts", ContentPart.SHORTHAND_PROPERTY, ContentPart::load, ctx); + } + if (map.containsKey("metadata") && map.get("metadata") != null) { + if (map.get("metadata") instanceof Map dict) { + result.metadata = copyMap(dict); + } + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + Message obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.role != null) result.put("role", obj.role.value); + if (obj.parts != null) { + List items = new ArrayList<>(); + for (ContentPart item : obj.parts) items.add(item.save(ctx)); + result.put("parts", items); + } + if (obj.metadata != null) result.put("metadata", serializeScalar(obj.metadata)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static Message fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static Message fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static Message fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static Message fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + public static Message assistant(String text) { + return new Message() {{ this.role = Role.fromValue("assistant"); this.parts = new java.util.ArrayList<>(java.util.Arrays.asList(new TextPart() {{ this.value = text; }})); }}; + } + + public static Message system(String text) { + return new Message() {{ this.role = Role.fromValue("system"); this.parts = new java.util.ArrayList<>(java.util.Arrays.asList(new TextPart() {{ this.value = text; }})); }}; + } + + public static Message user(String text) { + return new Message() {{ this.role = Role.fromValue("user"); this.parts = new java.util.ArrayList<>(java.util.Arrays.asList(new TextPart() {{ this.value = text; }})); }}; + } + + public Object toTextContent() { + return MessageMethods.toTextContent(this); + } + + public String text() { + return MessageMethods.text(this); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/MessageMethods.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/MessageMethods.java new file mode 100644 index 000000000..795f09e73 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/MessageMethods.java @@ -0,0 +1,51 @@ +// Typra extension seam. This file is created once and is safe to edit. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Hand-written implementations for the {@code @method} declarations on + * {@link Message}. + * + *

The emitter creates this file once, when missing, and never rewrites it, + * so it is the designated home for these bodies. Behaviour mirrors the Rust + * reference implementation in {@code runtime/rust/prompty/src/model_ext.rs}. + */ +public final class MessageMethods { + private MessageMethods() { } + + /** + * Returns a plain string when every part is text, and the wire form of the + * parts otherwise. + * + *

Providers accept a bare string for single-modality content but require + * the structured form once a message carries an image, audio or file part. + */ + public static Object toTextContent(Message self) { + List parts = self.parts == null ? List.of() : self.parts; + boolean allText = parts.stream().allMatch(part -> part instanceof TextPart); + if (allText) { + return joinText(parts); + } + SaveContext ctx = new SaveContext(); + List wire = new ArrayList<>(parts.size()); + for (ContentPart part : parts) { + wire.add(part.save(ctx)); + } + return wire; + } + + /** Concatenates every {@link TextPart} value, joined by newline. */ + public static String text(Message self) { + return joinText(self.parts == null ? List.of() : self.parts); + } + + private static String joinText(List parts) { + return parts.stream() + .filter(TextPart.class::isInstance) + .map(part -> ((TextPart) part).value) + .collect(Collectors.joining("\n")); + } +} \ No newline at end of file diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/MessagesUpdatedPayload.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/MessagesUpdatedPayload.java new file mode 100644 index 000000000..525e4b844 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/MessagesUpdatedPayload.java @@ -0,0 +1,105 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class MessagesUpdatedPayload { + public static final String SHORTHAND_PROPERTY = null; + + public List messages = null; + public String reason = null; + public List appended = null; + public Integer removed = null; + + public MessagesUpdatedPayload() { } + + @SuppressWarnings("unchecked") + public static MessagesUpdatedPayload load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new MessagesUpdatedPayload()); + } + MessagesUpdatedPayload result = new MessagesUpdatedPayload(); + MessagesUpdatedPayload.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(MessagesUpdatedPayload result, Map map, LoadContext ctx) { + if (map.containsKey("messages") && map.get("messages") != null) { + result.messages = ModelCollections.loadList( + map.get("messages"), "messages", Message.SHORTHAND_PROPERTY, Message::load, ctx); + } + if (map.containsKey("reason") && map.get("reason") != null) { + result.reason = String.valueOf(map.get("reason")); + } + if (map.containsKey("appended") && map.get("appended") != null) { + result.appended = ModelCollections.loadList( + map.get("appended"), "appended", Message.SHORTHAND_PROPERTY, Message::load, ctx); + } + if (map.containsKey("removed") && map.get("removed") != null) { + result.removed = (map.get("removed") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("removed")))); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + MessagesUpdatedPayload obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.messages != null) { + List items = new ArrayList<>(); + for (Message item : obj.messages) items.add(item.save(ctx)); + result.put("messages", items); + } + if (obj.reason != null) result.put("reason", serializeScalar(obj.reason)); + if (obj.appended != null) { + List items = new ArrayList<>(); + for (Message item : obj.appended) items.add(item.save(ctx)); + result.put("appended", items); + } + if (obj.removed != null) result.put("removed", serializeScalar(obj.removed)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static MessagesUpdatedPayload fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static MessagesUpdatedPayload fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static MessagesUpdatedPayload fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static MessagesUpdatedPayload fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Model.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Model.java new file mode 100644 index 000000000..4b539cd28 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Model.java @@ -0,0 +1,105 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class Model { + public static final String SHORTHAND_PROPERTY = "id"; + + public String id = ""; + public String provider = null; + public String apiType = null; + public Connection connection = null; + public ModelOptions options = null; + + public Model() { } + + @SuppressWarnings("unchecked") + public static Model load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (data instanceof String) { + Model result = new Model(); + result.id = String.valueOf(data); + return ctx.processOutput(result); + } + if (!(data instanceof Map map)) { + return ctx.processOutput(new Model()); + } + Model result = new Model(); + Model.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(Model result, Map map, LoadContext ctx) { + if (map.containsKey("id") && map.get("id") != null) { + result.id = String.valueOf(map.get("id")); + } + if (map.containsKey("provider") && map.get("provider") != null) { + result.provider = String.valueOf(map.get("provider")); + } + if (map.containsKey("apiType") && map.get("apiType") != null) { + result.apiType = String.valueOf(map.get("apiType")); + } + if (map.containsKey("connection") && map.get("connection") != null) { + result.connection = Connection.load(map.get("connection"), ctx); + } + if (map.containsKey("options") && map.get("options") != null) { + result.options = ModelOptions.load(map.get("options"), ctx); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + Model obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.id != null) result.put("id", serializeScalar(obj.id)); + if (obj.provider != null) result.put("provider", serializeScalar(obj.provider)); + if (obj.apiType != null) result.put("apiType", serializeScalar(obj.apiType)); + if (obj.connection != null) result.put("connection", obj.connection.save(ctx)); + if (obj.options != null) result.put("options", obj.options.save(ctx)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static Model fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static Model fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static Model fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static Model fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelCollections.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelCollections.java new file mode 100644 index 000000000..46e607403 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelCollections.java @@ -0,0 +1,135 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Function; + +/** Shared collection loading and saving helpers used by the generated model. */ +final class ModelCollections { + private ModelCollections() { } + + /** + * Loads a model list from either a flat list or a name-keyed dictionary. + * + *

Dictionary keys are injected as the element's {@code name}; scalar values + * are widened through the element type's shorthand property. + */ + static List loadList( + Object raw, String property, String shorthand, BiFunction loader, LoadContext ctx) { + List result = new ArrayList<>(); + if (raw instanceof Map dict) { + for (Map.Entry entry : dict.entrySet()) { + String key = String.valueOf(entry.getKey()); + Object value = entry.getValue(); + if (isSequence(value)) { + // Rust silently skips an array-valued entry here, which turns a malformed document + // into an empty list and hides the mistake until the tool is called and its arguments + // are missing. C# rejects it; this follows C#, because a schema that was written wrong + // is worth surfacing at load time. + throw new IllegalArgumentException( + "Invalid '" + property + "' format: key '" + key + "' has an array value. '" + property + + "' must be a flat list of objects or a name-keyed dict - not a nested {" + key + + ": [...]} structure."); + } + Map item = new LinkedHashMap<>(); + if (value instanceof Map nested && !nested.isEmpty()) { + for (Map.Entry field : nested.entrySet()) { + item.put(String.valueOf(field.getKey()), field.getValue()); + } + item.put("name", key); + } else { + item.put("name", key); + if (shorthand != null && value != null) { + item.put(shorthand, value); + } + } + result.add(loader.apply(item, ctx)); + } + } else if (raw instanceof Iterable values) { + for (Object item : values) { + Object widened = widen(item, shorthand); + if (widened != null) { + result.add(loader.apply(widened, ctx)); + } + } + } else if (raw instanceof Object[] values) { + for (Object item : values) { + Object widened = widen(item, shorthand); + if (widened != null) { + result.add(loader.apply(widened, ctx)); + } + } + } + return result; + } + + /** + * Saves a model list as either a name-keyed dictionary (the default) or a flat + * array, honouring {@link SaveContext#collectionFormat} and + * {@link SaveContext#useShorthand}. + * + *

The object form is only usable when every item carries a name, so a list + * with an unnamed item falls back to the array form rather than collapsing + * entries onto a shared key. + * + *

This is a deliberate, documented divergence: the C# runtime throws on an + * unnamed item and the Rust runtime silently drops it. Both lose data for a + * document that the load side accepts. Falling back to the array form is + * lossless and reloads identically, and every well-formed document — where + * each entry has a name — serializes the same way in all three runtimes. + */ + static Object saveList(List items, String shorthand, Function> saver, SaveContext ctx) { + List> saved = new ArrayList<>(); + for (T item : items) { + saved.add(saver.apply(item)); + } + if (!"object".equals(ctx.collectionFormat) || !allNamed(saved)) { + return new ArrayList(saved); + } + Map result = new LinkedHashMap<>(); + for (Map item : saved) { + String key = String.valueOf(item.remove("name")); + if (ctx.useShorthand && shorthand != null && item.size() == 1 && item.containsKey(shorthand)) { + result.put(key, item.get(shorthand)); + } else { + result.put(key, item); + } + } + return result; + } + + private static boolean allNamed(List> items) { + for (Map item : items) { + if (!(item.get("name") instanceof String name) || name.isEmpty()) { + return false; + } + } + return true; + } + + private static boolean isSequence(Object value) { + return value instanceof Iterable || value instanceof Object[]; + } + + /** + * Widens a list element into a loadable map. Scalars go through the shorthand + * property; empty and absent values are dropped, matching Prompty.Core's + * {@code GetDictionary} + {@code Count > 0} guard. + */ + private static Object widen(Object item, String shorthand) { + if (item instanceof Map map) { + return map.isEmpty() ? null : map; + } + if (item == null || shorthand == null) { + return null; + } + Map wrapped = new LinkedHashMap<>(); + wrapped.put(shorthand, item); + return wrapped; + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelInfo.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelInfo.java new file mode 100644 index 000000000..d29ff6f2b --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelInfo.java @@ -0,0 +1,165 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class ModelInfo { + public static final String SHORTHAND_PROPERTY = null; + + public String id = ""; + public String displayName = null; + public String ownedBy = null; + public Integer contextWindow = null; + public List inputModalities = null; + public List outputModalities = null; + public Map additionalProperties = null; + + public ModelInfo() { } + + @SuppressWarnings("unchecked") + public static ModelInfo load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new ModelInfo()); + } + ModelInfo result = new ModelInfo(); + ModelInfo.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(ModelInfo result, Map map, LoadContext ctx) { + if (map.containsKey("id") && map.get("id") != null) { + result.id = String.valueOf(map.get("id")); + } + if (map.containsKey("displayName") && map.get("displayName") != null) { + result.displayName = String.valueOf(map.get("displayName")); + } + if (map.containsKey("ownedBy") && map.get("ownedBy") != null) { + result.ownedBy = String.valueOf(map.get("ownedBy")); + } + if (map.containsKey("contextWindow") && map.get("contextWindow") != null) { + result.contextWindow = (map.get("contextWindow") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("contextWindow")))); + } + if (map.containsKey("inputModalities") && map.get("inputModalities") != null) { + result.inputModalities = new ArrayList<>(); + if (map.get("inputModalities") instanceof Iterable values) { + for (Object item : values) { + result.inputModalities.add(String.valueOf(item)); + } + } + } + if (map.containsKey("outputModalities") && map.get("outputModalities") != null) { + result.outputModalities = new ArrayList<>(); + if (map.get("outputModalities") instanceof Iterable values) { + for (Object item : values) { + result.outputModalities.add(String.valueOf(item)); + } + } + } + if (map.containsKey("additionalProperties") && map.get("additionalProperties") != null) { + if (map.get("additionalProperties") instanceof Map dict) { + result.additionalProperties = copyMap(dict); + } + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + ModelInfo obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.id != null) result.put("id", serializeScalar(obj.id)); + if (obj.displayName != null) result.put("displayName", serializeScalar(obj.displayName)); + if (obj.ownedBy != null) result.put("ownedBy", serializeScalar(obj.ownedBy)); + if (obj.contextWindow != null) result.put("contextWindow", serializeScalar(obj.contextWindow)); + if (obj.inputModalities != null) result.put("inputModalities", new ArrayList<>(obj.inputModalities)); + if (obj.outputModalities != null) result.put("outputModalities", new ArrayList<>(obj.outputModalities)); + if (obj.additionalProperties != null) result.put("additionalProperties", serializeScalar(obj.additionalProperties)); + return ctx.processDict(result); + } + + public Map toWire(String provider) { + Map result = new LinkedHashMap<>(); + String target = provider == null ? "" : provider; + { + String wireName = "id"; + boolean include = target.isEmpty(); + if (target.equals("openai")) { wireName = "id"; include = true; } + if (target.equals("anthropic")) { wireName = "id"; include = true; } + if (include && this.id != null) result.put(wireName, serializeScalar(this.id)); + } + { + String wireName = "displayName"; + boolean include = target.isEmpty(); + if (target.equals("anthropic")) { wireName = "display_name"; include = true; } + if (include && this.displayName != null) result.put(wireName, serializeScalar(this.displayName)); + } + { + String wireName = "ownedBy"; + boolean include = target.isEmpty(); + if (target.equals("openai")) { wireName = "owned_by"; include = true; } + if (include && this.ownedBy != null) result.put(wireName, serializeScalar(this.ownedBy)); + } + { + String wireName = "contextWindow"; + boolean include = target.isEmpty(); + if (target.equals("anthropic")) { wireName = "context_length"; include = true; } + if (include && this.contextWindow != null) result.put(wireName, serializeScalar(this.contextWindow)); + } + { + String wireName = "inputModalities"; + boolean include = target.isEmpty(); + if (target.equals("anthropic")) { wireName = "input_modalities"; include = true; } + if (include && this.inputModalities != null) result.put(wireName, serializeScalar(this.inputModalities)); + } + { + String wireName = "outputModalities"; + boolean include = target.isEmpty(); + if (target.equals("anthropic")) { wireName = "output_modalities"; include = true; } + if (include && this.outputModalities != null) result.put(wireName, serializeScalar(this.outputModalities)); + } + return result; + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static ModelInfo fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static ModelInfo fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static ModelInfo fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static ModelInfo fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelInvocationContextSnapshot.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelInvocationContextSnapshot.java new file mode 100644 index 000000000..dd935cab8 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelInvocationContextSnapshot.java @@ -0,0 +1,137 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class ModelInvocationContextSnapshot { + public static final String SHORTHAND_PROPERTY = null; + + public String id = ""; + public String sessionId = ""; + public String turnId = ""; + public String invocationId = ""; + public Integer iteration = 0; + public List messages = new ArrayList<>(); + public List decisions = null; + public Integer stablePrefixMessages = 0; + public InvocationContextState contextState = null; + public Map metadata = null; + + public ModelInvocationContextSnapshot() { } + + @SuppressWarnings("unchecked") + public static ModelInvocationContextSnapshot load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new ModelInvocationContextSnapshot()); + } + ModelInvocationContextSnapshot result = new ModelInvocationContextSnapshot(); + ModelInvocationContextSnapshot.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(ModelInvocationContextSnapshot result, Map map, LoadContext ctx) { + if (map.containsKey("id") && map.get("id") != null) { + result.id = String.valueOf(map.get("id")); + } + if (map.containsKey("sessionId") && map.get("sessionId") != null) { + result.sessionId = String.valueOf(map.get("sessionId")); + } + if (map.containsKey("turnId") && map.get("turnId") != null) { + result.turnId = String.valueOf(map.get("turnId")); + } + if (map.containsKey("invocationId") && map.get("invocationId") != null) { + result.invocationId = String.valueOf(map.get("invocationId")); + } + if (map.containsKey("iteration") && map.get("iteration") != null) { + result.iteration = (map.get("iteration") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("iteration")))); + } + if (map.containsKey("messages") && map.get("messages") != null) { + result.messages = ModelCollections.loadList( + map.get("messages"), "messages", Message.SHORTHAND_PROPERTY, Message::load, ctx); + } + if (map.containsKey("decisions") && map.get("decisions") != null) { + result.decisions = ModelCollections.loadList( + map.get("decisions"), "decisions", InvocationContextDecision.SHORTHAND_PROPERTY, InvocationContextDecision::load, ctx); + } + if (map.containsKey("stablePrefixMessages") && map.get("stablePrefixMessages") != null) { + result.stablePrefixMessages = (map.get("stablePrefixMessages") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("stablePrefixMessages")))); + } + if (map.containsKey("contextState") && map.get("contextState") != null) { + result.contextState = InvocationContextState.load(map.get("contextState"), ctx); + } + if (map.containsKey("metadata") && map.get("metadata") != null) { + if (map.get("metadata") instanceof Map dict) { + result.metadata = copyMap(dict); + } + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + ModelInvocationContextSnapshot obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.id != null) result.put("id", serializeScalar(obj.id)); + if (obj.sessionId != null) result.put("sessionId", serializeScalar(obj.sessionId)); + if (obj.turnId != null) result.put("turnId", serializeScalar(obj.turnId)); + if (obj.invocationId != null) result.put("invocationId", serializeScalar(obj.invocationId)); + if (obj.iteration != null) result.put("iteration", serializeScalar(obj.iteration)); + if (obj.messages != null) { + List items = new ArrayList<>(); + for (Message item : obj.messages) items.add(item.save(ctx)); + result.put("messages", items); + } + if (obj.decisions != null) { + List items = new ArrayList<>(); + for (InvocationContextDecision item : obj.decisions) items.add(item.save(ctx)); + result.put("decisions", items); + } + if (obj.stablePrefixMessages != null) result.put("stablePrefixMessages", serializeScalar(obj.stablePrefixMessages)); + if (obj.contextState != null) result.put("contextState", obj.contextState.save(ctx)); + if (obj.metadata != null) result.put("metadata", serializeScalar(obj.metadata)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static ModelInvocationContextSnapshot fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static ModelInvocationContextSnapshot fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static ModelInvocationContextSnapshot fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static ModelInvocationContextSnapshot fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelInvocationRequest.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelInvocationRequest.java new file mode 100644 index 000000000..6456c9c1c --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelInvocationRequest.java @@ -0,0 +1,80 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class ModelInvocationRequest { + public static final String SHORTHAND_PROPERTY = null; + + public ModelInvocationContextSnapshot context = null; + + public ModelInvocationRequest() { } + + @SuppressWarnings("unchecked") + public static ModelInvocationRequest load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new ModelInvocationRequest()); + } + ModelInvocationRequest result = new ModelInvocationRequest(); + ModelInvocationRequest.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(ModelInvocationRequest result, Map map, LoadContext ctx) { + if (map.containsKey("context") && map.get("context") != null) { + result.context = ModelInvocationContextSnapshot.load(map.get("context"), ctx); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + ModelInvocationRequest obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.context != null) result.put("context", obj.context.save(ctx)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static ModelInvocationRequest fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static ModelInvocationRequest fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static ModelInvocationRequest fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static ModelInvocationRequest fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelInvocationResponse.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelInvocationResponse.java new file mode 100644 index 000000000..176dec9ff --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelInvocationResponse.java @@ -0,0 +1,116 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class ModelInvocationResponse { + public static final String SHORTHAND_PROPERTY = null; + + public Object output = null; + public InvocationUsage usage = null; + public List assistantMessages = null; + public List toolRequests = null; + public InvocationContextState nextContextState = null; + public Map metadata = null; + + public ModelInvocationResponse() { } + + @SuppressWarnings("unchecked") + public static ModelInvocationResponse load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new ModelInvocationResponse()); + } + ModelInvocationResponse result = new ModelInvocationResponse(); + ModelInvocationResponse.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(ModelInvocationResponse result, Map map, LoadContext ctx) { + if (map.containsKey("output") && map.get("output") != null) { + result.output = map.get("output"); + } + if (map.containsKey("usage") && map.get("usage") != null) { + result.usage = InvocationUsage.load(map.get("usage"), ctx); + } + if (map.containsKey("assistantMessages") && map.get("assistantMessages") != null) { + result.assistantMessages = ModelCollections.loadList( + map.get("assistantMessages"), "assistantMessages", Message.SHORTHAND_PROPERTY, Message::load, ctx); + } + if (map.containsKey("toolRequests") && map.get("toolRequests") != null) { + result.toolRequests = ModelCollections.loadList( + map.get("toolRequests"), "toolRequests", ModelToolRequest.SHORTHAND_PROPERTY, ModelToolRequest::load, ctx); + } + if (map.containsKey("nextContextState") && map.get("nextContextState") != null) { + result.nextContextState = InvocationContextState.load(map.get("nextContextState"), ctx); + } + if (map.containsKey("metadata") && map.get("metadata") != null) { + if (map.get("metadata") instanceof Map dict) { + result.metadata = copyMap(dict); + } + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + ModelInvocationResponse obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.output != null) result.put("output", serializeScalar(obj.output)); + if (obj.usage != null) result.put("usage", obj.usage.save(ctx)); + if (obj.assistantMessages != null) { + List items = new ArrayList<>(); + for (Message item : obj.assistantMessages) items.add(item.save(ctx)); + result.put("assistantMessages", items); + } + if (obj.toolRequests != null) { + result.put("toolRequests", ModelCollections.saveList( + obj.toolRequests, ModelToolRequest.SHORTHAND_PROPERTY, item -> item.save(ctx), ctx)); + } + if (obj.nextContextState != null) result.put("nextContextState", obj.nextContextState.save(ctx)); + if (obj.metadata != null) result.put("metadata", serializeScalar(obj.metadata)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static ModelInvocationResponse fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static ModelInvocationResponse fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static ModelInvocationResponse fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static ModelInvocationResponse fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelLister.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelLister.java new file mode 100644 index 000000000..50cf820c6 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelLister.java @@ -0,0 +1,12 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public interface ModelLister { + List listModels(Object connection); +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelOptions.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelOptions.java new file mode 100644 index 000000000..26db1ac96 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelOptions.java @@ -0,0 +1,200 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class ModelOptions { + public static final String SHORTHAND_PROPERTY = null; + + public Float frequencyPenalty = null; + public Integer maxOutputTokens = null; + public Float presencePenalty = null; + public Integer seed = null; + public Float temperature = null; + public Integer topK = null; + public Float topP = null; + public List stopSequences = null; + public Boolean allowMultipleToolCalls = null; + public Map additionalProperties = null; + + public ModelOptions() { } + + @SuppressWarnings("unchecked") + public static ModelOptions load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new ModelOptions()); + } + ModelOptions result = new ModelOptions(); + ModelOptions.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(ModelOptions result, Map map, LoadContext ctx) { + if (map.containsKey("frequencyPenalty") && map.get("frequencyPenalty") != null) { + result.frequencyPenalty = (map.get("frequencyPenalty") instanceof Number n ? n.floatValue() : Float.parseFloat(String.valueOf(map.get("frequencyPenalty")))); + } + if (map.containsKey("maxOutputTokens") && map.get("maxOutputTokens") != null) { + result.maxOutputTokens = (map.get("maxOutputTokens") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("maxOutputTokens")))); + } + if (map.containsKey("presencePenalty") && map.get("presencePenalty") != null) { + result.presencePenalty = (map.get("presencePenalty") instanceof Number n ? n.floatValue() : Float.parseFloat(String.valueOf(map.get("presencePenalty")))); + } + if (map.containsKey("seed") && map.get("seed") != null) { + result.seed = (map.get("seed") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("seed")))); + } + if (map.containsKey("temperature") && map.get("temperature") != null) { + result.temperature = (map.get("temperature") instanceof Number n ? n.floatValue() : Float.parseFloat(String.valueOf(map.get("temperature")))); + } + if (map.containsKey("topK") && map.get("topK") != null) { + result.topK = (map.get("topK") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("topK")))); + } + if (map.containsKey("topP") && map.get("topP") != null) { + result.topP = (map.get("topP") instanceof Number n ? n.floatValue() : Float.parseFloat(String.valueOf(map.get("topP")))); + } + if (map.containsKey("stopSequences") && map.get("stopSequences") != null) { + result.stopSequences = new ArrayList<>(); + if (map.get("stopSequences") instanceof Iterable values) { + for (Object item : values) { + result.stopSequences.add(String.valueOf(item)); + } + } + } + if (map.containsKey("allowMultipleToolCalls") && map.get("allowMultipleToolCalls") != null) { + result.allowMultipleToolCalls = (map.get("allowMultipleToolCalls") instanceof Boolean b ? b : Boolean.parseBoolean(String.valueOf(map.get("allowMultipleToolCalls")))); + } + if (map.containsKey("additionalProperties") && map.get("additionalProperties") != null) { + if (map.get("additionalProperties") instanceof Map dict) { + result.additionalProperties = copyMap(dict); + } + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + ModelOptions obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.frequencyPenalty != null) result.put("frequencyPenalty", serializeScalar(obj.frequencyPenalty)); + if (obj.maxOutputTokens != null) result.put("maxOutputTokens", serializeScalar(obj.maxOutputTokens)); + if (obj.presencePenalty != null) result.put("presencePenalty", serializeScalar(obj.presencePenalty)); + if (obj.seed != null) result.put("seed", serializeScalar(obj.seed)); + if (obj.temperature != null) result.put("temperature", serializeScalar(obj.temperature)); + if (obj.topK != null) result.put("topK", serializeScalar(obj.topK)); + if (obj.topP != null) result.put("topP", serializeScalar(obj.topP)); + if (obj.stopSequences != null) result.put("stopSequences", new ArrayList<>(obj.stopSequences)); + if (obj.allowMultipleToolCalls != null) result.put("allowMultipleToolCalls", serializeScalar(obj.allowMultipleToolCalls)); + if (obj.additionalProperties != null) result.put("additionalProperties", serializeScalar(obj.additionalProperties)); + return ctx.processDict(result); + } + + public Map toWire(String provider) { + Map result = new LinkedHashMap<>(); + String target = provider == null ? "" : provider; + { + String wireName = "frequencyPenalty"; + boolean include = target.isEmpty(); + if (target.equals("openai")) { wireName = "frequency_penalty"; include = true; } + if (include && this.frequencyPenalty != null) result.put(wireName, serializeScalar(this.frequencyPenalty)); + } + { + String wireName = "maxOutputTokens"; + boolean include = target.isEmpty(); + if (target.equals("openai")) { wireName = "max_completion_tokens"; include = true; } + if (target.equals("responses")) { wireName = "max_output_tokens"; include = true; } + if (target.equals("anthropic")) { wireName = "max_tokens"; include = true; } + if (include && this.maxOutputTokens != null) result.put(wireName, serializeScalar(this.maxOutputTokens)); + } + { + String wireName = "presencePenalty"; + boolean include = target.isEmpty(); + if (target.equals("openai")) { wireName = "presence_penalty"; include = true; } + if (include && this.presencePenalty != null) result.put(wireName, serializeScalar(this.presencePenalty)); + } + { + String wireName = "seed"; + boolean include = target.isEmpty(); + if (target.equals("openai")) { wireName = "seed"; include = true; } + if (include && this.seed != null) result.put(wireName, serializeScalar(this.seed)); + } + { + String wireName = "temperature"; + boolean include = target.isEmpty(); + if (target.equals("openai")) { wireName = "temperature"; include = true; } + if (target.equals("responses")) { wireName = "temperature"; include = true; } + if (target.equals("anthropic")) { wireName = "temperature"; include = true; } + if (include && this.temperature != null) result.put(wireName, serializeScalar(this.temperature)); + } + { + String wireName = "topK"; + boolean include = target.isEmpty(); + if (target.equals("openai")) { wireName = "top_k"; include = true; } + if (target.equals("anthropic")) { wireName = "top_k"; include = true; } + if (include && this.topK != null) result.put(wireName, serializeScalar(this.topK)); + } + { + String wireName = "topP"; + boolean include = target.isEmpty(); + if (target.equals("openai")) { wireName = "top_p"; include = true; } + if (target.equals("responses")) { wireName = "top_p"; include = true; } + if (target.equals("anthropic")) { wireName = "top_p"; include = true; } + if (include && this.topP != null) result.put(wireName, serializeScalar(this.topP)); + } + { + String wireName = "stopSequences"; + boolean include = target.isEmpty(); + if (target.equals("openai")) { wireName = "stop"; include = true; } + if (target.equals("anthropic")) { wireName = "stop_sequences"; include = true; } + if (include && this.stopSequences != null) result.put(wireName, serializeScalar(this.stopSequences)); + } + { + String wireName = "allowMultipleToolCalls"; + boolean include = target.isEmpty(); + if (target.equals("openai")) { wireName = "parallel_tool_calls"; include = true; } + if (include && this.allowMultipleToolCalls != null) result.put(wireName, serializeScalar(this.allowMultipleToolCalls)); + } + return result; + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static ModelOptions fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static ModelOptions fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static ModelOptions fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static ModelOptions fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelReconciliationState.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelReconciliationState.java new file mode 100644 index 000000000..aab9ed17d --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelReconciliationState.java @@ -0,0 +1,102 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class ModelReconciliationState { + public static final String SHORTHAND_PROPERTY = null; + + public String invocationId = ""; + public ModelInvocationRequest request = null; + public Integer failedAttempt = 0; + public String message = ""; + public Map metadata = null; + + public ModelReconciliationState() { } + + @SuppressWarnings("unchecked") + public static ModelReconciliationState load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new ModelReconciliationState()); + } + ModelReconciliationState result = new ModelReconciliationState(); + ModelReconciliationState.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(ModelReconciliationState result, Map map, LoadContext ctx) { + if (map.containsKey("invocationId") && map.get("invocationId") != null) { + result.invocationId = String.valueOf(map.get("invocationId")); + } + if (map.containsKey("request") && map.get("request") != null) { + result.request = ModelInvocationRequest.load(map.get("request"), ctx); + } + if (map.containsKey("failedAttempt") && map.get("failedAttempt") != null) { + result.failedAttempt = (map.get("failedAttempt") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("failedAttempt")))); + } + if (map.containsKey("message") && map.get("message") != null) { + result.message = String.valueOf(map.get("message")); + } + if (map.containsKey("metadata") && map.get("metadata") != null) { + if (map.get("metadata") instanceof Map dict) { + result.metadata = copyMap(dict); + } + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + ModelReconciliationState obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.invocationId != null) result.put("invocationId", serializeScalar(obj.invocationId)); + if (obj.request != null) result.put("request", obj.request.save(ctx)); + if (obj.failedAttempt != null) result.put("failedAttempt", serializeScalar(obj.failedAttempt)); + if (obj.message != null) result.put("message", serializeScalar(obj.message)); + if (obj.metadata != null) result.put("metadata", serializeScalar(obj.metadata)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static ModelReconciliationState fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static ModelReconciliationState fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static ModelReconciliationState fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static ModelReconciliationState fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelToolOutcome.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelToolOutcome.java new file mode 100644 index 000000000..f1d95c39f --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelToolOutcome.java @@ -0,0 +1,19 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +public enum ModelToolOutcome { + SUCCESS("success"), + FAILED("failed"), + INDETERMINATE("indeterminate"), + ; + + public final String value; + ModelToolOutcome(String value) { this.value = value; } + public static ModelToolOutcome fromValue(String value) { + for (ModelToolOutcome item : values()) { + if (item.value.equals(value)) return item; + } + return valueOf(value.toUpperCase().replace("-", "_")); + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelToolRequest.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelToolRequest.java new file mode 100644 index 000000000..6045d749d --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelToolRequest.java @@ -0,0 +1,97 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class ModelToolRequest { + public static final String SHORTHAND_PROPERTY = null; + + public String id = ""; + public String name = ""; + public Object arguments = null; + public Map metadata = null; + + public ModelToolRequest() { } + + @SuppressWarnings("unchecked") + public static ModelToolRequest load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new ModelToolRequest()); + } + ModelToolRequest result = new ModelToolRequest(); + ModelToolRequest.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(ModelToolRequest result, Map map, LoadContext ctx) { + if (map.containsKey("id") && map.get("id") != null) { + result.id = String.valueOf(map.get("id")); + } + if (map.containsKey("name") && map.get("name") != null) { + result.name = String.valueOf(map.get("name")); + } + if (map.containsKey("arguments") && map.get("arguments") != null) { + result.arguments = map.get("arguments"); + } + if (map.containsKey("metadata") && map.get("metadata") != null) { + if (map.get("metadata") instanceof Map dict) { + result.metadata = copyMap(dict); + } + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + ModelToolRequest obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.id != null) result.put("id", serializeScalar(obj.id)); + if (obj.name != null) result.put("name", serializeScalar(obj.name)); + if (obj.arguments != null) result.put("arguments", serializeScalar(obj.arguments)); + if (obj.metadata != null) result.put("metadata", serializeScalar(obj.metadata)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static ModelToolRequest fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static ModelToolRequest fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static ModelToolRequest fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static ModelToolRequest fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelToolResult.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelToolResult.java new file mode 100644 index 000000000..fc2b1b848 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelToolResult.java @@ -0,0 +1,107 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class ModelToolResult { + public static final String SHORTHAND_PROPERTY = null; + + public String requestId = ""; + public String name = ""; + public ModelToolOutcome outcome = ModelToolOutcome.SUCCESS; + public Object output = null; + public String errorKind = null; + public Map metadata = null; + + public ModelToolResult() { } + + @SuppressWarnings("unchecked") + public static ModelToolResult load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new ModelToolResult()); + } + ModelToolResult result = new ModelToolResult(); + ModelToolResult.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(ModelToolResult result, Map map, LoadContext ctx) { + if (map.containsKey("requestId") && map.get("requestId") != null) { + result.requestId = String.valueOf(map.get("requestId")); + } + if (map.containsKey("name") && map.get("name") != null) { + result.name = String.valueOf(map.get("name")); + } + if (map.containsKey("outcome") && map.get("outcome") != null) { + result.outcome = ModelToolOutcome.fromValue(String.valueOf(map.get("outcome"))); + } + if (map.containsKey("output") && map.get("output") != null) { + result.output = map.get("output"); + } + if (map.containsKey("errorKind") && map.get("errorKind") != null) { + result.errorKind = String.valueOf(map.get("errorKind")); + } + if (map.containsKey("metadata") && map.get("metadata") != null) { + if (map.get("metadata") instanceof Map dict) { + result.metadata = copyMap(dict); + } + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + ModelToolResult obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.requestId != null) result.put("requestId", serializeScalar(obj.requestId)); + if (obj.name != null) result.put("name", serializeScalar(obj.name)); + result.put("outcome", obj.outcome.value); + if (obj.output != null) result.put("output", serializeScalar(obj.output)); + if (obj.errorKind != null) result.put("errorKind", serializeScalar(obj.errorKind)); + if (obj.metadata != null) result.put("metadata", serializeScalar(obj.metadata)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static ModelToolResult fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static ModelToolResult fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static ModelToolResult fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static ModelToolResult fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/OAuthConnection.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/OAuthConnection.java new file mode 100644 index 000000000..53d2e1706 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/OAuthConnection.java @@ -0,0 +1,108 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class OAuthConnection extends Connection { + public static final String SHORTHAND_PROPERTY = null; + + public String endpoint = ""; + public String clientId = ""; + public String clientSecret = ""; + public String tokenUrl = ""; + public List scopes = null; + + public OAuthConnection() { + this.kind = "oauth"; + } + + @SuppressWarnings("unchecked") + public static OAuthConnection load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new OAuthConnection()); + } + OAuthConnection result = new OAuthConnection(); + OAuthConnection.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(OAuthConnection result, Map map, LoadContext ctx) { + Connection.loadBaseInto(result, map, ctx); + if (map.containsKey("endpoint") && map.get("endpoint") != null) { + result.endpoint = String.valueOf(map.get("endpoint")); + } + if (map.containsKey("clientId") && map.get("clientId") != null) { + result.clientId = String.valueOf(map.get("clientId")); + } + if (map.containsKey("clientSecret") && map.get("clientSecret") != null) { + result.clientSecret = String.valueOf(map.get("clientSecret")); + } + if (map.containsKey("tokenUrl") && map.get("tokenUrl") != null) { + result.tokenUrl = String.valueOf(map.get("tokenUrl")); + } + if (map.containsKey("scopes") && map.get("scopes") != null) { + result.scopes = new ArrayList<>(); + if (map.get("scopes") instanceof Iterable values) { + for (Object item : values) { + result.scopes.add(String.valueOf(item)); + } + } + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + OAuthConnection obj = ctx.processObject(this); + Map result = super.save(ctx); + if (obj.endpoint != null) result.put("endpoint", serializeScalar(obj.endpoint)); + if (obj.clientId != null) result.put("clientId", serializeScalar(obj.clientId)); + if (obj.clientSecret != null) result.put("clientSecret", serializeScalar(obj.clientSecret)); + if (obj.tokenUrl != null) result.put("tokenUrl", serializeScalar(obj.tokenUrl)); + if (obj.scopes != null) result.put("scopes", new ArrayList<>(obj.scopes)); + return result; + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static OAuthConnection fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static OAuthConnection fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static OAuthConnection fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static OAuthConnection fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/OAuthToken.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/OAuthToken.java new file mode 100644 index 000000000..fe60b4210 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/OAuthToken.java @@ -0,0 +1,136 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class OAuthToken { + public static final String SHORTHAND_PROPERTY = null; + + public String accessToken = ""; + public String tokenType = ""; + public Long expiresIn = 0L; + public String refreshToken = null; + public String scope = null; + + public OAuthToken() { } + + @SuppressWarnings("unchecked") + public static OAuthToken load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new OAuthToken()); + } + OAuthToken result = new OAuthToken(); + OAuthToken.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(OAuthToken result, Map map, LoadContext ctx) { + if (map.containsKey("accessToken") && map.get("accessToken") != null) { + result.accessToken = String.valueOf(map.get("accessToken")); + } + if (map.containsKey("tokenType") && map.get("tokenType") != null) { + result.tokenType = String.valueOf(map.get("tokenType")); + } + if (map.containsKey("expiresIn") && map.get("expiresIn") != null) { + result.expiresIn = (map.get("expiresIn") instanceof Number n ? n.longValue() : Long.parseLong(String.valueOf(map.get("expiresIn")))); + } + if (map.containsKey("refreshToken") && map.get("refreshToken") != null) { + result.refreshToken = String.valueOf(map.get("refreshToken")); + } + if (map.containsKey("scope") && map.get("scope") != null) { + result.scope = String.valueOf(map.get("scope")); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + OAuthToken obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.accessToken != null) result.put("accessToken", serializeScalar(obj.accessToken)); + if (obj.tokenType != null) result.put("tokenType", serializeScalar(obj.tokenType)); + if (obj.expiresIn != null) result.put("expiresIn", serializeScalar(obj.expiresIn)); + if (obj.refreshToken != null) result.put("refreshToken", serializeScalar(obj.refreshToken)); + if (obj.scope != null) result.put("scope", serializeScalar(obj.scope)); + return ctx.processDict(result); + } + + public Map toWire(String provider) { + Map result = new LinkedHashMap<>(); + String target = provider == null ? "" : provider; + { + String wireName = "accessToken"; + boolean include = target.isEmpty(); + if (target.equals("foundry")) { wireName = "access_token"; include = true; } + if (include && this.accessToken != null) result.put(wireName, serializeScalar(this.accessToken)); + } + { + String wireName = "tokenType"; + boolean include = target.isEmpty(); + if (target.equals("foundry")) { wireName = "token_type"; include = true; } + if (include && this.tokenType != null) result.put(wireName, serializeScalar(this.tokenType)); + } + { + String wireName = "expiresIn"; + boolean include = target.isEmpty(); + if (target.equals("foundry")) { wireName = "expires_in"; include = true; } + if (include && this.expiresIn != null) result.put(wireName, serializeScalar(this.expiresIn)); + } + { + String wireName = "refreshToken"; + boolean include = target.isEmpty(); + if (target.equals("foundry")) { wireName = "refresh_token"; include = true; } + if (include && this.refreshToken != null) result.put(wireName, serializeScalar(this.refreshToken)); + } + { + String wireName = "scope"; + boolean include = target.isEmpty(); + if (target.equals("foundry")) { wireName = "scope"; include = true; } + if (include && this.scope != null) result.put(wireName, serializeScalar(this.scope)); + } + return result; + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static OAuthToken fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static OAuthToken fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static OAuthToken fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static OAuthToken fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ObjectProperty.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ObjectProperty.java new file mode 100644 index 000000000..14c77c43a --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ObjectProperty.java @@ -0,0 +1,87 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class ObjectProperty extends Property { + public static final String SHORTHAND_PROPERTY = null; + + public List properties = new ArrayList<>(); + + public ObjectProperty() { + this.kind = "object"; + } + + @SuppressWarnings("unchecked") + public static ObjectProperty load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new ObjectProperty()); + } + ObjectProperty result = new ObjectProperty(); + ObjectProperty.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(ObjectProperty result, Map map, LoadContext ctx) { + Property.loadBaseInto(result, map, ctx); + if (map.containsKey("properties") && map.get("properties") != null) { + result.properties = ModelCollections.loadList( + map.get("properties"), "properties", Property.SHORTHAND_PROPERTY, Property::load, ctx); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + ObjectProperty obj = ctx.processObject(this); + Map result = super.save(ctx); + if (obj.properties != null) { + result.put("properties", ModelCollections.saveList( + obj.properties, Property.SHORTHAND_PROPERTY, item -> item.save(ctx), ctx)); + } + return result; + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static ObjectProperty fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static ObjectProperty fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static ObjectProperty fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static ObjectProperty fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/OpenApiTool.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/OpenApiTool.java new file mode 100644 index 000000000..b7e81907e --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/OpenApiTool.java @@ -0,0 +1,88 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class OpenApiTool extends Tool { + public static final String SHORTHAND_PROPERTY = null; + + public Connection connection = null; + public String specification = ""; + + public OpenApiTool() { + this.kind = "openapi"; + } + + @SuppressWarnings("unchecked") + public static OpenApiTool load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new OpenApiTool()); + } + OpenApiTool result = new OpenApiTool(); + OpenApiTool.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(OpenApiTool result, Map map, LoadContext ctx) { + Tool.loadBaseInto(result, map, ctx); + if (map.containsKey("connection") && map.get("connection") != null) { + result.connection = Connection.load(map.get("connection"), ctx); + } + if (map.containsKey("specification") && map.get("specification") != null) { + result.specification = String.valueOf(map.get("specification")); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + OpenApiTool obj = ctx.processObject(this); + Map result = super.save(ctx); + if (obj.connection != null) result.put("connection", obj.connection.save(ctx)); + if (obj.specification != null) result.put("specification", serializeScalar(obj.specification)); + return result; + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static OpenApiTool fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static OpenApiTool fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static OpenApiTool fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static OpenApiTool fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Parser.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Parser.java new file mode 100644 index 000000000..6f406f515 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Parser.java @@ -0,0 +1,13 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public interface Parser { + Object preRender(String template); + List parse(Prompty agent, String rendered, Map context); +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ParserConfig.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ParserConfig.java new file mode 100644 index 000000000..a7b7de3f0 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ParserConfig.java @@ -0,0 +1,92 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class ParserConfig { + public static final String SHORTHAND_PROPERTY = "kind"; + + public String kind = "*"; + public Map options = null; + + public ParserConfig() { } + + @SuppressWarnings("unchecked") + public static ParserConfig load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (data instanceof String) { + ParserConfig result = new ParserConfig(); + result.kind = String.valueOf(data); + return ctx.processOutput(result); + } + if (!(data instanceof Map map)) { + return ctx.processOutput(new ParserConfig()); + } + ParserConfig result = new ParserConfig(); + ParserConfig.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(ParserConfig result, Map map, LoadContext ctx) { + if (map.containsKey("kind") && map.get("kind") != null) { + result.kind = String.valueOf(map.get("kind")); + } + if (map.containsKey("options") && map.get("options") != null) { + if (map.get("options") instanceof Map dict) { + result.options = copyMap(dict); + } + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + ParserConfig obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.kind != null) result.put("kind", serializeScalar(obj.kind)); + if (obj.options != null) result.put("options", serializeScalar(obj.options)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static ParserConfig fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static ParserConfig fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static ParserConfig fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static ParserConfig fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/PermissionCompletedPayload.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/PermissionCompletedPayload.java new file mode 100644 index 000000000..262562abe --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/PermissionCompletedPayload.java @@ -0,0 +1,112 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class PermissionCompletedPayload { + public static final String SHORTHAND_PROPERTY = null; + + public String requestId = null; + public String toolCallId = null; + public String permission = ""; + public Boolean approved = false; + public String reason = null; + public Map result = null; + public RedactionMetadata redaction = null; + + public PermissionCompletedPayload() { } + + @SuppressWarnings("unchecked") + public static PermissionCompletedPayload load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new PermissionCompletedPayload()); + } + PermissionCompletedPayload result = new PermissionCompletedPayload(); + PermissionCompletedPayload.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(PermissionCompletedPayload result, Map map, LoadContext ctx) { + if (map.containsKey("requestId") && map.get("requestId") != null) { + result.requestId = String.valueOf(map.get("requestId")); + } + if (map.containsKey("toolCallId") && map.get("toolCallId") != null) { + result.toolCallId = String.valueOf(map.get("toolCallId")); + } + if (map.containsKey("permission") && map.get("permission") != null) { + result.permission = String.valueOf(map.get("permission")); + } + if (map.containsKey("approved") && map.get("approved") != null) { + result.approved = (map.get("approved") instanceof Boolean b ? b : Boolean.parseBoolean(String.valueOf(map.get("approved")))); + } + if (map.containsKey("reason") && map.get("reason") != null) { + result.reason = String.valueOf(map.get("reason")); + } + if (map.containsKey("result") && map.get("result") != null) { + if (map.get("result") instanceof Map dict) { + result.result = copyMap(dict); + } + } + if (map.containsKey("redaction") && map.get("redaction") != null) { + result.redaction = RedactionMetadata.load(map.get("redaction"), ctx); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + PermissionCompletedPayload obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.requestId != null) result.put("requestId", serializeScalar(obj.requestId)); + if (obj.toolCallId != null) result.put("toolCallId", serializeScalar(obj.toolCallId)); + if (obj.permission != null) result.put("permission", serializeScalar(obj.permission)); + if (obj.approved != null) result.put("approved", serializeScalar(obj.approved)); + if (obj.reason != null) result.put("reason", serializeScalar(obj.reason)); + if (obj.result != null) result.put("result", serializeScalar(obj.result)); + if (obj.redaction != null) result.put("redaction", obj.redaction.save(ctx)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static PermissionCompletedPayload fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static PermissionCompletedPayload fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static PermissionCompletedPayload fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static PermissionCompletedPayload fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/PermissionDecision.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/PermissionDecision.java new file mode 100644 index 000000000..951e15275 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/PermissionDecision.java @@ -0,0 +1,107 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class PermissionDecision { + public static final String SHORTHAND_PROPERTY = null; + + public String requestId = null; + public String toolCallId = null; + public String permission = ""; + public Boolean approved = false; + public String reason = null; + public Map result = null; + + public PermissionDecision() { } + + @SuppressWarnings("unchecked") + public static PermissionDecision load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new PermissionDecision()); + } + PermissionDecision result = new PermissionDecision(); + PermissionDecision.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(PermissionDecision result, Map map, LoadContext ctx) { + if (map.containsKey("requestId") && map.get("requestId") != null) { + result.requestId = String.valueOf(map.get("requestId")); + } + if (map.containsKey("toolCallId") && map.get("toolCallId") != null) { + result.toolCallId = String.valueOf(map.get("toolCallId")); + } + if (map.containsKey("permission") && map.get("permission") != null) { + result.permission = String.valueOf(map.get("permission")); + } + if (map.containsKey("approved") && map.get("approved") != null) { + result.approved = (map.get("approved") instanceof Boolean b ? b : Boolean.parseBoolean(String.valueOf(map.get("approved")))); + } + if (map.containsKey("reason") && map.get("reason") != null) { + result.reason = String.valueOf(map.get("reason")); + } + if (map.containsKey("result") && map.get("result") != null) { + if (map.get("result") instanceof Map dict) { + result.result = copyMap(dict); + } + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + PermissionDecision obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.requestId != null) result.put("requestId", serializeScalar(obj.requestId)); + if (obj.toolCallId != null) result.put("toolCallId", serializeScalar(obj.toolCallId)); + if (obj.permission != null) result.put("permission", serializeScalar(obj.permission)); + if (obj.approved != null) result.put("approved", serializeScalar(obj.approved)); + if (obj.reason != null) result.put("reason", serializeScalar(obj.reason)); + if (obj.result != null) result.put("result", serializeScalar(obj.result)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static PermissionDecision fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static PermissionDecision fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static PermissionDecision fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static PermissionDecision fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/PermissionRequest.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/PermissionRequest.java new file mode 100644 index 000000000..72a783e4c --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/PermissionRequest.java @@ -0,0 +1,114 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class PermissionRequest { + public static final String SHORTHAND_PROPERTY = null; + + public String requestId = null; + public String toolCallId = null; + public String permission = ""; + public String target = null; + public Map details = null; + public String promptRequest = null; + public Map policy = null; + + public PermissionRequest() { } + + @SuppressWarnings("unchecked") + public static PermissionRequest load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new PermissionRequest()); + } + PermissionRequest result = new PermissionRequest(); + PermissionRequest.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(PermissionRequest result, Map map, LoadContext ctx) { + if (map.containsKey("requestId") && map.get("requestId") != null) { + result.requestId = String.valueOf(map.get("requestId")); + } + if (map.containsKey("toolCallId") && map.get("toolCallId") != null) { + result.toolCallId = String.valueOf(map.get("toolCallId")); + } + if (map.containsKey("permission") && map.get("permission") != null) { + result.permission = String.valueOf(map.get("permission")); + } + if (map.containsKey("target") && map.get("target") != null) { + result.target = String.valueOf(map.get("target")); + } + if (map.containsKey("details") && map.get("details") != null) { + if (map.get("details") instanceof Map dict) { + result.details = copyMap(dict); + } + } + if (map.containsKey("promptRequest") && map.get("promptRequest") != null) { + result.promptRequest = String.valueOf(map.get("promptRequest")); + } + if (map.containsKey("policy") && map.get("policy") != null) { + if (map.get("policy") instanceof Map dict) { + result.policy = copyMap(dict); + } + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + PermissionRequest obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.requestId != null) result.put("requestId", serializeScalar(obj.requestId)); + if (obj.toolCallId != null) result.put("toolCallId", serializeScalar(obj.toolCallId)); + if (obj.permission != null) result.put("permission", serializeScalar(obj.permission)); + if (obj.target != null) result.put("target", serializeScalar(obj.target)); + if (obj.details != null) result.put("details", serializeScalar(obj.details)); + if (obj.promptRequest != null) result.put("promptRequest", serializeScalar(obj.promptRequest)); + if (obj.policy != null) result.put("policy", serializeScalar(obj.policy)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static PermissionRequest fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static PermissionRequest fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static PermissionRequest fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static PermissionRequest fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/PermissionRequestedPayload.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/PermissionRequestedPayload.java new file mode 100644 index 000000000..e10f67500 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/PermissionRequestedPayload.java @@ -0,0 +1,119 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class PermissionRequestedPayload { + public static final String SHORTHAND_PROPERTY = null; + + public String requestId = null; + public String toolCallId = null; + public String permission = ""; + public String target = null; + public Map details = null; + public String promptRequest = null; + public Map policy = null; + public RedactionMetadata redaction = null; + + public PermissionRequestedPayload() { } + + @SuppressWarnings("unchecked") + public static PermissionRequestedPayload load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new PermissionRequestedPayload()); + } + PermissionRequestedPayload result = new PermissionRequestedPayload(); + PermissionRequestedPayload.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(PermissionRequestedPayload result, Map map, LoadContext ctx) { + if (map.containsKey("requestId") && map.get("requestId") != null) { + result.requestId = String.valueOf(map.get("requestId")); + } + if (map.containsKey("toolCallId") && map.get("toolCallId") != null) { + result.toolCallId = String.valueOf(map.get("toolCallId")); + } + if (map.containsKey("permission") && map.get("permission") != null) { + result.permission = String.valueOf(map.get("permission")); + } + if (map.containsKey("target") && map.get("target") != null) { + result.target = String.valueOf(map.get("target")); + } + if (map.containsKey("details") && map.get("details") != null) { + if (map.get("details") instanceof Map dict) { + result.details = copyMap(dict); + } + } + if (map.containsKey("promptRequest") && map.get("promptRequest") != null) { + result.promptRequest = String.valueOf(map.get("promptRequest")); + } + if (map.containsKey("policy") && map.get("policy") != null) { + if (map.get("policy") instanceof Map dict) { + result.policy = copyMap(dict); + } + } + if (map.containsKey("redaction") && map.get("redaction") != null) { + result.redaction = RedactionMetadata.load(map.get("redaction"), ctx); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + PermissionRequestedPayload obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.requestId != null) result.put("requestId", serializeScalar(obj.requestId)); + if (obj.toolCallId != null) result.put("toolCallId", serializeScalar(obj.toolCallId)); + if (obj.permission != null) result.put("permission", serializeScalar(obj.permission)); + if (obj.target != null) result.put("target", serializeScalar(obj.target)); + if (obj.details != null) result.put("details", serializeScalar(obj.details)); + if (obj.promptRequest != null) result.put("promptRequest", serializeScalar(obj.promptRequest)); + if (obj.policy != null) result.put("policy", serializeScalar(obj.policy)); + if (obj.redaction != null) result.put("redaction", obj.redaction.save(ctx)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static PermissionRequestedPayload fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static PermissionRequestedPayload fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static PermissionRequestedPayload fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static PermissionRequestedPayload fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/PermissionResolver.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/PermissionResolver.java new file mode 100644 index 000000000..f33af6565 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/PermissionResolver.java @@ -0,0 +1,12 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public interface PermissionResolver { + PermissionDecision request(PermissionRequest request); +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Processor.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Processor.java new file mode 100644 index 000000000..7bcab5546 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Processor.java @@ -0,0 +1,13 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public interface Processor { + Object process(Prompty agent, Object response); + Object processStream(Object stream); +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ProjectInfo.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ProjectInfo.java new file mode 100644 index 000000000..38dad84b8 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ProjectInfo.java @@ -0,0 +1,114 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class ProjectInfo { + public static final String SHORTHAND_PROPERTY = null; + + public String name = ""; + public String displayName = ""; + public String endpoint = ""; + + public ProjectInfo() { } + + @SuppressWarnings("unchecked") + public static ProjectInfo load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new ProjectInfo()); + } + ProjectInfo result = new ProjectInfo(); + ProjectInfo.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(ProjectInfo result, Map map, LoadContext ctx) { + if (map.containsKey("name") && map.get("name") != null) { + result.name = String.valueOf(map.get("name")); + } + if (map.containsKey("displayName") && map.get("displayName") != null) { + result.displayName = String.valueOf(map.get("displayName")); + } + if (map.containsKey("endpoint") && map.get("endpoint") != null) { + result.endpoint = String.valueOf(map.get("endpoint")); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + ProjectInfo obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.name != null) result.put("name", serializeScalar(obj.name)); + if (obj.displayName != null) result.put("displayName", serializeScalar(obj.displayName)); + if (obj.endpoint != null) result.put("endpoint", serializeScalar(obj.endpoint)); + return ctx.processDict(result); + } + + public Map toWire(String provider) { + Map result = new LinkedHashMap<>(); + String target = provider == null ? "" : provider; + { + String wireName = "name"; + boolean include = target.isEmpty(); + if (target.equals("foundry")) { wireName = "name"; include = true; } + if (include && this.name != null) result.put(wireName, serializeScalar(this.name)); + } + { + String wireName = "displayName"; + boolean include = target.isEmpty(); + if (target.equals("foundry")) { wireName = "display_name"; include = true; } + if (include && this.displayName != null) result.put(wireName, serializeScalar(this.displayName)); + } + { + String wireName = "endpoint"; + boolean include = target.isEmpty(); + if (target.equals("foundry")) { wireName = "endpoint"; include = true; } + if (include && this.endpoint != null) result.put(wireName, serializeScalar(this.endpoint)); + } + return result; + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static ProjectInfo fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static ProjectInfo fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static ProjectInfo fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static ProjectInfo fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Prompty.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Prompty.java new file mode 100644 index 000000000..b3782e7ac --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Prompty.java @@ -0,0 +1,139 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class Prompty { + public static final String SHORTHAND_PROPERTY = null; + + public String name = ""; + public String displayName = null; + public String description = null; + public Map metadata = null; + public List inputs = null; + public List outputs = null; + public Model model = null; + public List tools = null; + public Template template = null; + public String instructions = null; + + public Prompty() { } + + @SuppressWarnings("unchecked") + public static Prompty load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new Prompty()); + } + Prompty result = new Prompty(); + Prompty.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(Prompty result, Map map, LoadContext ctx) { + if (map.containsKey("name") && map.get("name") != null) { + result.name = String.valueOf(map.get("name")); + } + if (map.containsKey("displayName") && map.get("displayName") != null) { + result.displayName = String.valueOf(map.get("displayName")); + } + if (map.containsKey("description") && map.get("description") != null) { + result.description = String.valueOf(map.get("description")); + } + if (map.containsKey("metadata") && map.get("metadata") != null) { + if (map.get("metadata") instanceof Map dict) { + result.metadata = copyMap(dict); + } + } + if (map.containsKey("inputs") && map.get("inputs") != null) { + result.inputs = ModelCollections.loadList( + map.get("inputs"), "inputs", Property.SHORTHAND_PROPERTY, Property::load, ctx); + } + if (map.containsKey("outputs") && map.get("outputs") != null) { + result.outputs = ModelCollections.loadList( + map.get("outputs"), "outputs", Property.SHORTHAND_PROPERTY, Property::load, ctx); + } + if (map.containsKey("model") && map.get("model") != null) { + result.model = Model.load(map.get("model"), ctx); + } + if (map.containsKey("tools") && map.get("tools") != null) { + result.tools = ModelCollections.loadList( + map.get("tools"), "tools", Tool.SHORTHAND_PROPERTY, Tool::load, ctx); + } + if (map.containsKey("template") && map.get("template") != null) { + result.template = Template.load(map.get("template"), ctx); + } + if (map.containsKey("instructions") && map.get("instructions") != null) { + result.instructions = String.valueOf(map.get("instructions")); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + Prompty obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.name != null) result.put("name", serializeScalar(obj.name)); + if (obj.displayName != null) result.put("displayName", serializeScalar(obj.displayName)); + if (obj.description != null) result.put("description", serializeScalar(obj.description)); + if (obj.metadata != null) result.put("metadata", serializeScalar(obj.metadata)); + if (obj.inputs != null) { + result.put("inputs", ModelCollections.saveList( + obj.inputs, Property.SHORTHAND_PROPERTY, item -> item.save(ctx), ctx)); + } + if (obj.outputs != null) { + result.put("outputs", ModelCollections.saveList( + obj.outputs, Property.SHORTHAND_PROPERTY, item -> item.save(ctx), ctx)); + } + if (obj.model != null) result.put("model", obj.model.save(ctx)); + if (obj.tools != null) { + result.put("tools", ModelCollections.saveList( + obj.tools, Tool.SHORTHAND_PROPERTY, item -> item.save(ctx), ctx)); + } + if (obj.template != null) result.put("template", obj.template.save(ctx)); + if (obj.instructions != null) result.put("instructions", serializeScalar(obj.instructions)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static Prompty fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static Prompty fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static Prompty fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static Prompty fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/PromptyTool.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/PromptyTool.java new file mode 100644 index 000000000..cb5a8bd99 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/PromptyTool.java @@ -0,0 +1,88 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class PromptyTool extends Tool { + public static final String SHORTHAND_PROPERTY = null; + + public String path = ""; + public String mode = "single"; + + public PromptyTool() { + this.kind = "prompty"; + } + + @SuppressWarnings("unchecked") + public static PromptyTool load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new PromptyTool()); + } + PromptyTool result = new PromptyTool(); + PromptyTool.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(PromptyTool result, Map map, LoadContext ctx) { + Tool.loadBaseInto(result, map, ctx); + if (map.containsKey("path") && map.get("path") != null) { + result.path = String.valueOf(map.get("path")); + } + if (map.containsKey("mode") && map.get("mode") != null) { + result.mode = String.valueOf(map.get("mode")); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + PromptyTool obj = ctx.processObject(this); + Map result = super.save(ctx); + if (obj.path != null) result.put("path", serializeScalar(obj.path)); + if (obj.mode != null) result.put("mode", serializeScalar(obj.mode)); + return result; + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static PromptyTool fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static PromptyTool fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static PromptyTool fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static PromptyTool fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Property.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Property.java new file mode 100644 index 000000000..5f14d6453 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Property.java @@ -0,0 +1,159 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class Property { + public static final String SHORTHAND_PROPERTY = "example"; + + public String name = ""; + public String kind = ""; + public String description = null; + public Boolean required = null; + public Boolean nullable = null; + public Object defaultValue = null; + public Object example = null; + public List enumValues = null; + + public Property() { } + + @SuppressWarnings("unchecked") + public static Property load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (data instanceof Boolean) { + Property result = new Property(); + result.kind = "boolean"; + result.example = (data instanceof Boolean b ? b : Boolean.parseBoolean(String.valueOf(data))); + return ctx.processOutput(result); + } + if (data instanceof Double || data instanceof Float || data instanceof java.math.BigDecimal) { + Property result = new Property(); + result.kind = "float"; + result.example = (data instanceof Number n ? n.floatValue() : Float.parseFloat(String.valueOf(data))); + return ctx.processOutput(result); + } + if (data instanceof Number) { + Property result = new Property(); + result.kind = "integer"; + result.example = (data instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(data))); + return ctx.processOutput(result); + } + if (data instanceof String) { + Property result = new Property(); + result.kind = "string"; + result.example = String.valueOf(data); + return ctx.processOutput(result); + } + if (data instanceof Map dispatchMap) { + Object discriminator = dispatchMap.get("kind"); + if (discriminator != null) { + switch (String.valueOf(discriminator).toLowerCase(java.util.Locale.ROOT)) { + case "array": + return ArrayProperty.load(data, ctx); + case "object": + return ObjectProperty.load(data, ctx); + case "union": + return UnionProperty.load(data, ctx); + default: + break; + } + } + } + if (!(data instanceof Map map)) { + return ctx.processOutput(new Property()); + } + Property result = new Property(); + Property.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(Property result, Map map, LoadContext ctx) { + if (map.containsKey("name") && map.get("name") != null) { + result.name = String.valueOf(map.get("name")); + } + if (map.containsKey("kind") && map.get("kind") != null) { + result.kind = String.valueOf(map.get("kind")); + } + if (map.containsKey("description") && map.get("description") != null) { + result.description = String.valueOf(map.get("description")); + } + if (map.containsKey("required") && map.get("required") != null) { + result.required = (map.get("required") instanceof Boolean b ? b : Boolean.parseBoolean(String.valueOf(map.get("required")))); + } + if (map.containsKey("nullable") && map.get("nullable") != null) { + result.nullable = (map.get("nullable") instanceof Boolean b ? b : Boolean.parseBoolean(String.valueOf(map.get("nullable")))); + } + if (map.containsKey("default") && map.get("default") != null) { + result.defaultValue = map.get("default"); + } + if (map.containsKey("example") && map.get("example") != null) { + result.example = map.get("example"); + } + if (map.containsKey("enumValues") && map.get("enumValues") != null) { + result.enumValues = new ArrayList<>(); + if (map.get("enumValues") instanceof Iterable values) { + for (Object item : values) { + result.enumValues.add(item); + } + } + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + Property obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.name != null) result.put("name", serializeScalar(obj.name)); + if (obj.kind != null) result.put("kind", serializeScalar(obj.kind)); + if (obj.description != null) result.put("description", serializeScalar(obj.description)); + if (obj.required != null) result.put("required", serializeScalar(obj.required)); + if (obj.nullable != null) result.put("nullable", serializeScalar(obj.nullable)); + if (obj.defaultValue != null) result.put("default", serializeScalar(obj.defaultValue)); + if (obj.example != null) result.put("example", serializeScalar(obj.example)); + if (obj.enumValues != null) result.put("enumValues", new ArrayList<>(obj.enumValues)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static Property fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static Property fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static Property fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static Property fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/RedactedField.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/RedactedField.java new file mode 100644 index 000000000..91ba083e9 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/RedactedField.java @@ -0,0 +1,90 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class RedactedField { + public static final String SHORTHAND_PROPERTY = null; + + public String path = ""; + public RedactionMode mode = RedactionMode.NONE; + public String reason = null; + + public RedactedField() { } + + @SuppressWarnings("unchecked") + public static RedactedField load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new RedactedField()); + } + RedactedField result = new RedactedField(); + RedactedField.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(RedactedField result, Map map, LoadContext ctx) { + if (map.containsKey("path") && map.get("path") != null) { + result.path = String.valueOf(map.get("path")); + } + if (map.containsKey("mode") && map.get("mode") != null) { + result.mode = RedactionMode.fromValue(String.valueOf(map.get("mode"))); + } + if (map.containsKey("reason") && map.get("reason") != null) { + result.reason = String.valueOf(map.get("reason")); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + RedactedField obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.path != null) result.put("path", serializeScalar(obj.path)); + result.put("mode", obj.mode.value); + if (obj.reason != null) result.put("reason", serializeScalar(obj.reason)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static RedactedField fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static RedactedField fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static RedactedField fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static RedactedField fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/RedactionMetadata.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/RedactionMetadata.java new file mode 100644 index 000000000..4b03631cb --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/RedactionMetadata.java @@ -0,0 +1,95 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class RedactionMetadata { + public static final String SHORTHAND_PROPERTY = null; + + public Boolean sanitized = null; + public List fields = null; + public String policy = null; + + public RedactionMetadata() { } + + @SuppressWarnings("unchecked") + public static RedactionMetadata load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new RedactionMetadata()); + } + RedactionMetadata result = new RedactionMetadata(); + RedactionMetadata.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(RedactionMetadata result, Map map, LoadContext ctx) { + if (map.containsKey("sanitized") && map.get("sanitized") != null) { + result.sanitized = (map.get("sanitized") instanceof Boolean b ? b : Boolean.parseBoolean(String.valueOf(map.get("sanitized")))); + } + if (map.containsKey("fields") && map.get("fields") != null) { + result.fields = ModelCollections.loadList( + map.get("fields"), "fields", RedactedField.SHORTHAND_PROPERTY, RedactedField::load, ctx); + } + if (map.containsKey("policy") && map.get("policy") != null) { + result.policy = String.valueOf(map.get("policy")); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + RedactionMetadata obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.sanitized != null) result.put("sanitized", serializeScalar(obj.sanitized)); + if (obj.fields != null) { + List items = new ArrayList<>(); + for (RedactedField item : obj.fields) items.add(item.save(ctx)); + result.put("fields", items); + } + if (obj.policy != null) result.put("policy", serializeScalar(obj.policy)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static RedactionMetadata fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static RedactionMetadata fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static RedactionMetadata fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static RedactionMetadata fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/RedactionMode.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/RedactionMode.java new file mode 100644 index 000000000..89e160858 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/RedactionMode.java @@ -0,0 +1,21 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +public enum RedactionMode { + NONE("none"), + REDACTED("redacted"), + HASHED("hashed"), + SUMMARY("summary"), + REFERENCE("reference"), + ; + + public final String value; + RedactionMode(String value) { this.value = value; } + public static RedactionMode fromValue(String value) { + for (RedactionMode item : values()) { + if (item.value.equals(value)) return item; + } + return valueOf(value.toUpperCase().replace("-", "_")); + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ReferenceConnection.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ReferenceConnection.java new file mode 100644 index 000000000..7e8ea06a4 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ReferenceConnection.java @@ -0,0 +1,88 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class ReferenceConnection extends Connection { + public static final String SHORTHAND_PROPERTY = null; + + public String name = ""; + public String target = null; + + public ReferenceConnection() { + this.kind = "reference"; + } + + @SuppressWarnings("unchecked") + public static ReferenceConnection load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new ReferenceConnection()); + } + ReferenceConnection result = new ReferenceConnection(); + ReferenceConnection.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(ReferenceConnection result, Map map, LoadContext ctx) { + Connection.loadBaseInto(result, map, ctx); + if (map.containsKey("name") && map.get("name") != null) { + result.name = String.valueOf(map.get("name")); + } + if (map.containsKey("target") && map.get("target") != null) { + result.target = String.valueOf(map.get("target")); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + ReferenceConnection obj = ctx.processObject(this); + Map result = super.save(ctx); + if (obj.name != null) result.put("name", serializeScalar(obj.name)); + if (obj.target != null) result.put("target", serializeScalar(obj.target)); + return result; + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static ReferenceConnection fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static ReferenceConnection fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static ReferenceConnection fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static ReferenceConnection fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/RemoteConnection.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/RemoteConnection.java new file mode 100644 index 000000000..01fd15fb9 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/RemoteConnection.java @@ -0,0 +1,88 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class RemoteConnection extends Connection { + public static final String SHORTHAND_PROPERTY = null; + + public String name = ""; + public String endpoint = ""; + + public RemoteConnection() { + this.kind = "remote"; + } + + @SuppressWarnings("unchecked") + public static RemoteConnection load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new RemoteConnection()); + } + RemoteConnection result = new RemoteConnection(); + RemoteConnection.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(RemoteConnection result, Map map, LoadContext ctx) { + Connection.loadBaseInto(result, map, ctx); + if (map.containsKey("name") && map.get("name") != null) { + result.name = String.valueOf(map.get("name")); + } + if (map.containsKey("endpoint") && map.get("endpoint") != null) { + result.endpoint = String.valueOf(map.get("endpoint")); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + RemoteConnection obj = ctx.processObject(this); + Map result = super.save(ctx); + if (obj.name != null) result.put("name", serializeScalar(obj.name)); + if (obj.endpoint != null) result.put("endpoint", serializeScalar(obj.endpoint)); + return result; + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static RemoteConnection fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static RemoteConnection fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static RemoteConnection fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static RemoteConnection fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Renderer.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Renderer.java new file mode 100644 index 000000000..4c89a5b05 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Renderer.java @@ -0,0 +1,12 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public interface Renderer { + String render(Prompty agent, String template, Map inputs); +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ReplayJournalRecord.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ReplayJournalRecord.java new file mode 100644 index 000000000..62ddb46b3 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ReplayJournalRecord.java @@ -0,0 +1,135 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class ReplayJournalRecord { + public static final String SHORTHAND_PROPERTY = null; + + public ReplayRecordKind kind = ReplayRecordKind.SESSION; + public String type = null; + public String sessionId = null; + public String turnId = null; + public Integer iteration = null; + public ReplayRecordStatus status = null; + public String requestId = null; + public String toolName = null; + public Boolean success = null; + public String errorKind = null; + public Integer turns = null; + public Integer checkpoints = null; + + public ReplayJournalRecord() { } + + @SuppressWarnings("unchecked") + public static ReplayJournalRecord load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new ReplayJournalRecord()); + } + ReplayJournalRecord result = new ReplayJournalRecord(); + ReplayJournalRecord.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(ReplayJournalRecord result, Map map, LoadContext ctx) { + if (map.containsKey("kind") && map.get("kind") != null) { + result.kind = ReplayRecordKind.fromValue(String.valueOf(map.get("kind"))); + } + if (map.containsKey("type") && map.get("type") != null) { + result.type = String.valueOf(map.get("type")); + } + if (map.containsKey("sessionId") && map.get("sessionId") != null) { + result.sessionId = String.valueOf(map.get("sessionId")); + } + if (map.containsKey("turnId") && map.get("turnId") != null) { + result.turnId = String.valueOf(map.get("turnId")); + } + if (map.containsKey("iteration") && map.get("iteration") != null) { + result.iteration = (map.get("iteration") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("iteration")))); + } + if (map.containsKey("status") && map.get("status") != null) { + result.status = ReplayRecordStatus.fromValue(String.valueOf(map.get("status"))); + } + if (map.containsKey("requestId") && map.get("requestId") != null) { + result.requestId = String.valueOf(map.get("requestId")); + } + if (map.containsKey("toolName") && map.get("toolName") != null) { + result.toolName = String.valueOf(map.get("toolName")); + } + if (map.containsKey("success") && map.get("success") != null) { + result.success = (map.get("success") instanceof Boolean b ? b : Boolean.parseBoolean(String.valueOf(map.get("success")))); + } + if (map.containsKey("errorKind") && map.get("errorKind") != null) { + result.errorKind = String.valueOf(map.get("errorKind")); + } + if (map.containsKey("turns") && map.get("turns") != null) { + result.turns = (map.get("turns") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("turns")))); + } + if (map.containsKey("checkpoints") && map.get("checkpoints") != null) { + result.checkpoints = (map.get("checkpoints") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("checkpoints")))); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + ReplayJournalRecord obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + result.put("kind", obj.kind.value); + if (obj.type != null) result.put("type", serializeScalar(obj.type)); + if (obj.sessionId != null) result.put("sessionId", serializeScalar(obj.sessionId)); + if (obj.turnId != null) result.put("turnId", serializeScalar(obj.turnId)); + if (obj.iteration != null) result.put("iteration", serializeScalar(obj.iteration)); + if (obj.status != null) result.put("status", obj.status.value); + if (obj.requestId != null) result.put("requestId", serializeScalar(obj.requestId)); + if (obj.toolName != null) result.put("toolName", serializeScalar(obj.toolName)); + if (obj.success != null) result.put("success", serializeScalar(obj.success)); + if (obj.errorKind != null) result.put("errorKind", serializeScalar(obj.errorKind)); + if (obj.turns != null) result.put("turns", serializeScalar(obj.turns)); + if (obj.checkpoints != null) result.put("checkpoints", serializeScalar(obj.checkpoints)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static ReplayJournalRecord fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static ReplayJournalRecord fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static ReplayJournalRecord fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static ReplayJournalRecord fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ReplayMismatch.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ReplayMismatch.java new file mode 100644 index 000000000..1c59a8f84 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ReplayMismatch.java @@ -0,0 +1,95 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class ReplayMismatch { + public static final String SHORTHAND_PROPERTY = null; + + public Integer index = 0; + public ReplayJournalRecord expected = null; + public ReplayJournalRecord actual = null; + public String message = ""; + + public ReplayMismatch() { } + + @SuppressWarnings("unchecked") + public static ReplayMismatch load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new ReplayMismatch()); + } + ReplayMismatch result = new ReplayMismatch(); + ReplayMismatch.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(ReplayMismatch result, Map map, LoadContext ctx) { + if (map.containsKey("index") && map.get("index") != null) { + result.index = (map.get("index") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("index")))); + } + if (map.containsKey("expected") && map.get("expected") != null) { + result.expected = ReplayJournalRecord.load(map.get("expected"), ctx); + } + if (map.containsKey("actual") && map.get("actual") != null) { + result.actual = ReplayJournalRecord.load(map.get("actual"), ctx); + } + if (map.containsKey("message") && map.get("message") != null) { + result.message = String.valueOf(map.get("message")); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + ReplayMismatch obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.index != null) result.put("index", serializeScalar(obj.index)); + if (obj.expected != null) result.put("expected", obj.expected.save(ctx)); + if (obj.actual != null) result.put("actual", obj.actual.save(ctx)); + if (obj.message != null) result.put("message", serializeScalar(obj.message)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static ReplayMismatch fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static ReplayMismatch fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static ReplayMismatch fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static ReplayMismatch fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ReplayRecordKind.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ReplayRecordKind.java new file mode 100644 index 000000000..8c580e229 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ReplayRecordKind.java @@ -0,0 +1,19 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +public enum ReplayRecordKind { + SESSION("session"), + TURN("turn"), + SUMMARY("summary"), + ; + + public final String value; + ReplayRecordKind(String value) { this.value = value; } + public static ReplayRecordKind fromValue(String value) { + for (ReplayRecordKind item : values()) { + if (item.value.equals(value)) return item; + } + return valueOf(value.toUpperCase().replace("-", "_")); + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ReplayRecordStatus.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ReplayRecordStatus.java new file mode 100644 index 000000000..9c3241fca --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ReplayRecordStatus.java @@ -0,0 +1,19 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +public enum ReplayRecordStatus { + SUCCESS("success"), + ERROR("error"), + CANCELLED("cancelled"), + ; + + public final String value; + ReplayRecordStatus(String value) { this.value = value; } + public static ReplayRecordStatus fromValue(String value) { + for (ReplayRecordStatus item : values()) { + if (item.value.equals(value)) return item; + } + return valueOf(value.toUpperCase().replace("-", "_")); + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ReplayVerificationRequest.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ReplayVerificationRequest.java new file mode 100644 index 000000000..c6971b7c2 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ReplayVerificationRequest.java @@ -0,0 +1,95 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class ReplayVerificationRequest { + public static final String SHORTHAND_PROPERTY = null; + + public List expected = new ArrayList<>(); + public List actual = new ArrayList<>(); + + public ReplayVerificationRequest() { } + + @SuppressWarnings("unchecked") + public static ReplayVerificationRequest load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new ReplayVerificationRequest()); + } + ReplayVerificationRequest result = new ReplayVerificationRequest(); + ReplayVerificationRequest.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(ReplayVerificationRequest result, Map map, LoadContext ctx) { + if (map.containsKey("expected") && map.get("expected") != null) { + result.expected = ModelCollections.loadList( + map.get("expected"), "expected", ReplayJournalRecord.SHORTHAND_PROPERTY, ReplayJournalRecord::load, ctx); + } + if (map.containsKey("actual") && map.get("actual") != null) { + result.actual = ModelCollections.loadList( + map.get("actual"), "actual", ReplayJournalRecord.SHORTHAND_PROPERTY, ReplayJournalRecord::load, ctx); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + ReplayVerificationRequest obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.expected != null) { + List items = new ArrayList<>(); + for (ReplayJournalRecord item : obj.expected) items.add(item.save(ctx)); + result.put("expected", items); + } + if (obj.actual != null) { + List items = new ArrayList<>(); + for (ReplayJournalRecord item : obj.actual) items.add(item.save(ctx)); + result.put("actual", items); + } + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static ReplayVerificationRequest fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static ReplayVerificationRequest fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static ReplayVerificationRequest fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static ReplayVerificationRequest fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ReplayVerificationResult.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ReplayVerificationResult.java new file mode 100644 index 000000000..dcdfa0b6b --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ReplayVerificationResult.java @@ -0,0 +1,100 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class ReplayVerificationResult { + public static final String SHORTHAND_PROPERTY = null; + + public ReplayVerificationStatus status = ReplayVerificationStatus.PASSED; + public List mismatches = null; + public Integer expectedCount = 0; + public Integer actualCount = 0; + + public ReplayVerificationResult() { } + + @SuppressWarnings("unchecked") + public static ReplayVerificationResult load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new ReplayVerificationResult()); + } + ReplayVerificationResult result = new ReplayVerificationResult(); + ReplayVerificationResult.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(ReplayVerificationResult result, Map map, LoadContext ctx) { + if (map.containsKey("status") && map.get("status") != null) { + result.status = ReplayVerificationStatus.fromValue(String.valueOf(map.get("status"))); + } + if (map.containsKey("mismatches") && map.get("mismatches") != null) { + result.mismatches = ModelCollections.loadList( + map.get("mismatches"), "mismatches", ReplayMismatch.SHORTHAND_PROPERTY, ReplayMismatch::load, ctx); + } + if (map.containsKey("expectedCount") && map.get("expectedCount") != null) { + result.expectedCount = (map.get("expectedCount") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("expectedCount")))); + } + if (map.containsKey("actualCount") && map.get("actualCount") != null) { + result.actualCount = (map.get("actualCount") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("actualCount")))); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + ReplayVerificationResult obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + result.put("status", obj.status.value); + if (obj.mismatches != null) { + List items = new ArrayList<>(); + for (ReplayMismatch item : obj.mismatches) items.add(item.save(ctx)); + result.put("mismatches", items); + } + if (obj.expectedCount != null) result.put("expectedCount", serializeScalar(obj.expectedCount)); + if (obj.actualCount != null) result.put("actualCount", serializeScalar(obj.actualCount)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static ReplayVerificationResult fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static ReplayVerificationResult fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static ReplayVerificationResult fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static ReplayVerificationResult fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ReplayVerificationStatus.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ReplayVerificationStatus.java new file mode 100644 index 000000000..eb061e5ba --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ReplayVerificationStatus.java @@ -0,0 +1,18 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +public enum ReplayVerificationStatus { + PASSED("passed"), + FAILED("failed"), + ; + + public final String value; + ReplayVerificationStatus(String value) { this.value = value; } + public static ReplayVerificationStatus fromValue(String value) { + for (ReplayVerificationStatus item : values()) { + if (item.value.equals(value)) return item; + } + return valueOf(value.toUpperCase().replace("-", "_")); + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ResumeContext.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ResumeContext.java new file mode 100644 index 000000000..e3f3d8ef7 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ResumeContext.java @@ -0,0 +1,102 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class ResumeContext { + public static final String SHORTHAND_PROPERTY = null; + + public EngineCheckpoint checkpoint = null; + public Integer maxIterations = 0; + public Integer maxModelAttempts = 0; + public Long lastJournalSequence = 0L; + public Map metadata = null; + + public ResumeContext() { } + + @SuppressWarnings("unchecked") + public static ResumeContext load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new ResumeContext()); + } + ResumeContext result = new ResumeContext(); + ResumeContext.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(ResumeContext result, Map map, LoadContext ctx) { + if (map.containsKey("checkpoint") && map.get("checkpoint") != null) { + result.checkpoint = EngineCheckpoint.load(map.get("checkpoint"), ctx); + } + if (map.containsKey("maxIterations") && map.get("maxIterations") != null) { + result.maxIterations = (map.get("maxIterations") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("maxIterations")))); + } + if (map.containsKey("maxModelAttempts") && map.get("maxModelAttempts") != null) { + result.maxModelAttempts = (map.get("maxModelAttempts") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("maxModelAttempts")))); + } + if (map.containsKey("lastJournalSequence") && map.get("lastJournalSequence") != null) { + result.lastJournalSequence = (map.get("lastJournalSequence") instanceof Number n ? n.longValue() : Long.parseLong(String.valueOf(map.get("lastJournalSequence")))); + } + if (map.containsKey("metadata") && map.get("metadata") != null) { + if (map.get("metadata") instanceof Map dict) { + result.metadata = copyMap(dict); + } + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + ResumeContext obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.checkpoint != null) result.put("checkpoint", obj.checkpoint.save(ctx)); + if (obj.maxIterations != null) result.put("maxIterations", serializeScalar(obj.maxIterations)); + if (obj.maxModelAttempts != null) result.put("maxModelAttempts", serializeScalar(obj.maxModelAttempts)); + if (obj.lastJournalSequence != null) result.put("lastJournalSequence", serializeScalar(obj.lastJournalSequence)); + if (obj.metadata != null) result.put("metadata", serializeScalar(obj.metadata)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static ResumeContext fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static ResumeContext fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static ResumeContext fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static ResumeContext fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/RetryPayload.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/RetryPayload.java new file mode 100644 index 000000000..6f4520f4c --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/RetryPayload.java @@ -0,0 +1,100 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class RetryPayload { + public static final String SHORTHAND_PROPERTY = null; + + public String operation = ""; + public Integer attempt = 0; + public Integer maxAttempts = null; + public Double delayMs = null; + public String reason = null; + + public RetryPayload() { } + + @SuppressWarnings("unchecked") + public static RetryPayload load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new RetryPayload()); + } + RetryPayload result = new RetryPayload(); + RetryPayload.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(RetryPayload result, Map map, LoadContext ctx) { + if (map.containsKey("operation") && map.get("operation") != null) { + result.operation = String.valueOf(map.get("operation")); + } + if (map.containsKey("attempt") && map.get("attempt") != null) { + result.attempt = (map.get("attempt") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("attempt")))); + } + if (map.containsKey("maxAttempts") && map.get("maxAttempts") != null) { + result.maxAttempts = (map.get("maxAttempts") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("maxAttempts")))); + } + if (map.containsKey("delayMs") && map.get("delayMs") != null) { + result.delayMs = (map.get("delayMs") instanceof Number n ? n.doubleValue() : Double.parseDouble(String.valueOf(map.get("delayMs")))); + } + if (map.containsKey("reason") && map.get("reason") != null) { + result.reason = String.valueOf(map.get("reason")); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + RetryPayload obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.operation != null) result.put("operation", serializeScalar(obj.operation)); + if (obj.attempt != null) result.put("attempt", serializeScalar(obj.attempt)); + if (obj.maxAttempts != null) result.put("maxAttempts", serializeScalar(obj.maxAttempts)); + if (obj.delayMs != null) result.put("delayMs", serializeScalar(obj.delayMs)); + if (obj.reason != null) result.put("reason", serializeScalar(obj.reason)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static RetryPayload fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static RetryPayload fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static RetryPayload fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static RetryPayload fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/RetryPolicyRequest.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/RetryPolicyRequest.java new file mode 100644 index 000000000..27584c936 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/RetryPolicyRequest.java @@ -0,0 +1,95 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class RetryPolicyRequest { + public static final String SHORTHAND_PROPERTY = null; + + public Integer failedAttempts = 0; + public Integer nextAttempt = 0; + public Integer maxAttempts = 0; + public String reason = ""; + + public RetryPolicyRequest() { } + + @SuppressWarnings("unchecked") + public static RetryPolicyRequest load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new RetryPolicyRequest()); + } + RetryPolicyRequest result = new RetryPolicyRequest(); + RetryPolicyRequest.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(RetryPolicyRequest result, Map map, LoadContext ctx) { + if (map.containsKey("failedAttempts") && map.get("failedAttempts") != null) { + result.failedAttempts = (map.get("failedAttempts") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("failedAttempts")))); + } + if (map.containsKey("nextAttempt") && map.get("nextAttempt") != null) { + result.nextAttempt = (map.get("nextAttempt") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("nextAttempt")))); + } + if (map.containsKey("maxAttempts") && map.get("maxAttempts") != null) { + result.maxAttempts = (map.get("maxAttempts") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("maxAttempts")))); + } + if (map.containsKey("reason") && map.get("reason") != null) { + result.reason = String.valueOf(map.get("reason")); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + RetryPolicyRequest obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.failedAttempts != null) result.put("failedAttempts", serializeScalar(obj.failedAttempts)); + if (obj.nextAttempt != null) result.put("nextAttempt", serializeScalar(obj.nextAttempt)); + if (obj.maxAttempts != null) result.put("maxAttempts", serializeScalar(obj.maxAttempts)); + if (obj.reason != null) result.put("reason", serializeScalar(obj.reason)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static RetryPolicyRequest fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static RetryPolicyRequest fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static RetryPolicyRequest fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static RetryPolicyRequest fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Role.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Role.java new file mode 100644 index 000000000..3e1e43ff4 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Role.java @@ -0,0 +1,21 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +public enum Role { + SYSTEM("system"), + USER("user"), + ASSISTANT("assistant"), + DEVELOPER("developer"), + TOOL("tool"), + ; + + public final String value; + Role(String value) { this.value = value; } + public static Role fromValue(String value) { + for (Role item : values()) { + if (item.value.equals(value)) return item; + } + return valueOf(value.toUpperCase().replace("-", "_")); + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/RunTurnRequest.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/RunTurnRequest.java new file mode 100644 index 000000000..ced626ae0 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/RunTurnRequest.java @@ -0,0 +1,97 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class RunTurnRequest { + public static final String SHORTHAND_PROPERTY = null; + + public String sessionId = ""; + public String turnId = ""; + public Map inputs = null; + public TurnOptions options = null; + + public RunTurnRequest() { } + + @SuppressWarnings("unchecked") + public static RunTurnRequest load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new RunTurnRequest()); + } + RunTurnRequest result = new RunTurnRequest(); + RunTurnRequest.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(RunTurnRequest result, Map map, LoadContext ctx) { + if (map.containsKey("sessionId") && map.get("sessionId") != null) { + result.sessionId = String.valueOf(map.get("sessionId")); + } + if (map.containsKey("turnId") && map.get("turnId") != null) { + result.turnId = String.valueOf(map.get("turnId")); + } + if (map.containsKey("inputs") && map.get("inputs") != null) { + if (map.get("inputs") instanceof Map dict) { + result.inputs = copyMap(dict); + } + } + if (map.containsKey("options") && map.get("options") != null) { + result.options = TurnOptions.load(map.get("options"), ctx); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + RunTurnRequest obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.sessionId != null) result.put("sessionId", serializeScalar(obj.sessionId)); + if (obj.turnId != null) result.put("turnId", serializeScalar(obj.turnId)); + if (obj.inputs != null) result.put("inputs", serializeScalar(obj.inputs)); + if (obj.options != null) result.put("options", obj.options.save(ctx)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static RunTurnRequest fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static RunTurnRequest fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static RunTurnRequest fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static RunTurnRequest fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/RunTurnResult.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/RunTurnResult.java new file mode 100644 index 000000000..3ac2b6c4a --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/RunTurnResult.java @@ -0,0 +1,120 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class RunTurnResult { + public static final String SHORTHAND_PROPERTY = null; + + public String sessionId = ""; + public String turnId = ""; + public RunTurnStatus status = RunTurnStatus.SUCCESS; + public Object output = null; + public Integer iterations = 0; + public List toolResults = null; + public List checkpoints = null; + + public RunTurnResult() { } + + @SuppressWarnings("unchecked") + public static RunTurnResult load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new RunTurnResult()); + } + RunTurnResult result = new RunTurnResult(); + RunTurnResult.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(RunTurnResult result, Map map, LoadContext ctx) { + if (map.containsKey("sessionId") && map.get("sessionId") != null) { + result.sessionId = String.valueOf(map.get("sessionId")); + } + if (map.containsKey("turnId") && map.get("turnId") != null) { + result.turnId = String.valueOf(map.get("turnId")); + } + if (map.containsKey("status") && map.get("status") != null) { + result.status = RunTurnStatus.fromValue(String.valueOf(map.get("status"))); + } + if (map.containsKey("output") && map.get("output") != null) { + result.output = map.get("output"); + } + if (map.containsKey("iterations") && map.get("iterations") != null) { + result.iterations = (map.get("iterations") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("iterations")))); + } + if (map.containsKey("toolResults") && map.get("toolResults") != null) { + result.toolResults = ModelCollections.loadList( + map.get("toolResults"), "toolResults", HostToolResult.SHORTHAND_PROPERTY, HostToolResult::load, ctx); + } + if (map.containsKey("checkpoints") && map.get("checkpoints") != null) { + result.checkpoints = ModelCollections.loadList( + map.get("checkpoints"), "checkpoints", Checkpoint.SHORTHAND_PROPERTY, Checkpoint::load, ctx); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + RunTurnResult obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.sessionId != null) result.put("sessionId", serializeScalar(obj.sessionId)); + if (obj.turnId != null) result.put("turnId", serializeScalar(obj.turnId)); + result.put("status", obj.status.value); + if (obj.output != null) result.put("output", serializeScalar(obj.output)); + if (obj.iterations != null) result.put("iterations", serializeScalar(obj.iterations)); + if (obj.toolResults != null) { + List items = new ArrayList<>(); + for (HostToolResult item : obj.toolResults) items.add(item.save(ctx)); + result.put("toolResults", items); + } + if (obj.checkpoints != null) { + List items = new ArrayList<>(); + for (Checkpoint item : obj.checkpoints) items.add(item.save(ctx)); + result.put("checkpoints", items); + } + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static RunTurnResult fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static RunTurnResult fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static RunTurnResult fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static RunTurnResult fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/RunTurnStatus.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/RunTurnStatus.java new file mode 100644 index 000000000..6a3b8f52b --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/RunTurnStatus.java @@ -0,0 +1,19 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +public enum RunTurnStatus { + SUCCESS("success"), + ERROR("error"), + CANCELLED("cancelled"), + ; + + public final String value; + RunTurnStatus(String value) { this.value = value; } + public static RunTurnStatus fromValue(String value) { + for (RunTurnStatus item : values()) { + if (item.value.equals(value)) return item; + } + return valueOf(value.toUpperCase().replace("-", "_")); + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SaveContext.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SaveContext.java new file mode 100644 index 000000000..89e3be748 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SaveContext.java @@ -0,0 +1,35 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.Map; +import java.util.function.Function; + +public final class SaveContext { + private final Function preSave; + private final Function, Map> postSave; + + public SaveContext() { + this(null, null); + } + + public SaveContext(Function preSave, Function, Map> postSave) { + this.preSave = preSave; + this.postSave = postSave; + } + + /** Output format for collections: "object" (name as key) or "array" (list of dicts). */ + public String collectionFormat = "object"; + + /** Use the shorthand scalar representation when possible. */ + public boolean useShorthand = true; + + @SuppressWarnings("unchecked") + public T processObject(T value) { + return preSave == null ? value : (T) preSave.apply(value); + } + + public Map processDict(Map value) { + return postSave == null ? value : postSave.apply(value); + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionEndPayload.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionEndPayload.java new file mode 100644 index 000000000..88c2519e8 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionEndPayload.java @@ -0,0 +1,95 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class SessionEndPayload { + public static final String SHORTHAND_PROPERTY = null; + + public String sessionId = null; + public SessionEndStatus status = null; + public String reason = null; + public Double durationMs = null; + + public SessionEndPayload() { } + + @SuppressWarnings("unchecked") + public static SessionEndPayload load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new SessionEndPayload()); + } + SessionEndPayload result = new SessionEndPayload(); + SessionEndPayload.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(SessionEndPayload result, Map map, LoadContext ctx) { + if (map.containsKey("sessionId") && map.get("sessionId") != null) { + result.sessionId = String.valueOf(map.get("sessionId")); + } + if (map.containsKey("status") && map.get("status") != null) { + result.status = SessionEndStatus.fromValue(String.valueOf(map.get("status"))); + } + if (map.containsKey("reason") && map.get("reason") != null) { + result.reason = String.valueOf(map.get("reason")); + } + if (map.containsKey("durationMs") && map.get("durationMs") != null) { + result.durationMs = (map.get("durationMs") instanceof Number n ? n.doubleValue() : Double.parseDouble(String.valueOf(map.get("durationMs")))); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + SessionEndPayload obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.sessionId != null) result.put("sessionId", serializeScalar(obj.sessionId)); + if (obj.status != null) result.put("status", obj.status.value); + if (obj.reason != null) result.put("reason", serializeScalar(obj.reason)); + if (obj.durationMs != null) result.put("durationMs", serializeScalar(obj.durationMs)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static SessionEndPayload fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static SessionEndPayload fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static SessionEndPayload fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static SessionEndPayload fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionEndStatus.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionEndStatus.java new file mode 100644 index 000000000..bd86a3a38 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionEndStatus.java @@ -0,0 +1,20 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +public enum SessionEndStatus { + SUCCESS("success"), + ERROR("error"), + CANCELLED("cancelled"), + INTERRUPTED("interrupted"), + ; + + public final String value; + SessionEndStatus(String value) { this.value = value; } + public static SessionEndStatus fromValue(String value) { + for (SessionEndStatus item : values()) { + if (item.value.equals(value)) return item; + } + return valueOf(value.toUpperCase().replace("-", "_")); + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionEvent.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionEvent.java new file mode 100644 index 000000000..4cedd78a0 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionEvent.java @@ -0,0 +1,122 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class SessionEvent { + public static final String SHORTHAND_PROPERTY = null; + + public String id = ""; + public SessionEventType type = SessionEventType.SESSION_START; + public String timestamp = ""; + public String sessionId = null; + public String turnId = null; + public String parentId = null; + public String spanId = null; + public Map payload = new LinkedHashMap<>(); + public RedactionMetadata redaction = null; + + public SessionEvent() { } + + @SuppressWarnings("unchecked") + public static SessionEvent load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new SessionEvent()); + } + SessionEvent result = new SessionEvent(); + SessionEvent.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(SessionEvent result, Map map, LoadContext ctx) { + if (map.containsKey("id") && map.get("id") != null) { + result.id = String.valueOf(map.get("id")); + } + if (map.containsKey("type") && map.get("type") != null) { + result.type = SessionEventType.fromValue(String.valueOf(map.get("type"))); + } + if (map.containsKey("timestamp") && map.get("timestamp") != null) { + result.timestamp = String.valueOf(map.get("timestamp")); + } + if (map.containsKey("sessionId") && map.get("sessionId") != null) { + result.sessionId = String.valueOf(map.get("sessionId")); + } + if (map.containsKey("turnId") && map.get("turnId") != null) { + result.turnId = String.valueOf(map.get("turnId")); + } + if (map.containsKey("parentId") && map.get("parentId") != null) { + result.parentId = String.valueOf(map.get("parentId")); + } + if (map.containsKey("spanId") && map.get("spanId") != null) { + result.spanId = String.valueOf(map.get("spanId")); + } + if (map.containsKey("payload") && map.get("payload") != null) { + if (map.get("payload") instanceof Map dict) { + result.payload = copyMap(dict); + } + } + if (map.containsKey("redaction") && map.get("redaction") != null) { + result.redaction = RedactionMetadata.load(map.get("redaction"), ctx); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + SessionEvent obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.id != null) result.put("id", serializeScalar(obj.id)); + result.put("type", obj.type.value); + if (obj.timestamp != null) result.put("timestamp", serializeScalar(obj.timestamp)); + if (obj.sessionId != null) result.put("sessionId", serializeScalar(obj.sessionId)); + if (obj.turnId != null) result.put("turnId", serializeScalar(obj.turnId)); + if (obj.parentId != null) result.put("parentId", serializeScalar(obj.parentId)); + if (obj.spanId != null) result.put("spanId", serializeScalar(obj.spanId)); + if (obj.payload != null) result.put("payload", serializeScalar(obj.payload)); + if (obj.redaction != null) result.put("redaction", obj.redaction.save(ctx)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static SessionEvent fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static SessionEvent fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static SessionEvent fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static SessionEvent fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionEventType.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionEventType.java new file mode 100644 index 000000000..02829d3ac --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionEventType.java @@ -0,0 +1,23 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +public enum SessionEventType { + SESSION_START("session_start"), + SESSION_END("session_end"), + SESSION_WARNING("session_warning"), + SESSION_HOOK_START("session_hook_start"), + SESSION_HOOK_END("session_hook_end"), + CHECKPOINT_CREATED("checkpoint_created"), + TRAJECTORY_EVENT("trajectory_event"), + ; + + public final String value; + SessionEventType(String value) { this.value = value; } + public static SessionEventType fromValue(String value) { + for (SessionEventType item : values()) { + if (item.value.equals(value)) return item; + } + return valueOf(value.toUpperCase().replace("-", "_")); + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionFileRef.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionFileRef.java new file mode 100644 index 000000000..e82b2d64b --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionFileRef.java @@ -0,0 +1,100 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class SessionFileRef { + public static final String SHORTHAND_PROPERTY = null; + + public String sessionId = null; + public String path = ""; + public String toolName = null; + public Integer turnIndex = null; + public String firstSeenAt = null; + + public SessionFileRef() { } + + @SuppressWarnings("unchecked") + public static SessionFileRef load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new SessionFileRef()); + } + SessionFileRef result = new SessionFileRef(); + SessionFileRef.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(SessionFileRef result, Map map, LoadContext ctx) { + if (map.containsKey("sessionId") && map.get("sessionId") != null) { + result.sessionId = String.valueOf(map.get("sessionId")); + } + if (map.containsKey("path") && map.get("path") != null) { + result.path = String.valueOf(map.get("path")); + } + if (map.containsKey("toolName") && map.get("toolName") != null) { + result.toolName = String.valueOf(map.get("toolName")); + } + if (map.containsKey("turnIndex") && map.get("turnIndex") != null) { + result.turnIndex = (map.get("turnIndex") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("turnIndex")))); + } + if (map.containsKey("firstSeenAt") && map.get("firstSeenAt") != null) { + result.firstSeenAt = String.valueOf(map.get("firstSeenAt")); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + SessionFileRef obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.sessionId != null) result.put("sessionId", serializeScalar(obj.sessionId)); + if (obj.path != null) result.put("path", serializeScalar(obj.path)); + if (obj.toolName != null) result.put("toolName", serializeScalar(obj.toolName)); + if (obj.turnIndex != null) result.put("turnIndex", serializeScalar(obj.turnIndex)); + if (obj.firstSeenAt != null) result.put("firstSeenAt", serializeScalar(obj.firstSeenAt)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static SessionFileRef fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static SessionFileRef fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static SessionFileRef fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static SessionFileRef fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionRef.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionRef.java new file mode 100644 index 000000000..0472b6d90 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionRef.java @@ -0,0 +1,100 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class SessionRef { + public static final String SHORTHAND_PROPERTY = null; + + public String sessionId = null; + public String refType = ""; + public String refValue = ""; + public Integer turnIndex = null; + public String createdAt = null; + + public SessionRef() { } + + @SuppressWarnings("unchecked") + public static SessionRef load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new SessionRef()); + } + SessionRef result = new SessionRef(); + SessionRef.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(SessionRef result, Map map, LoadContext ctx) { + if (map.containsKey("sessionId") && map.get("sessionId") != null) { + result.sessionId = String.valueOf(map.get("sessionId")); + } + if (map.containsKey("refType") && map.get("refType") != null) { + result.refType = String.valueOf(map.get("refType")); + } + if (map.containsKey("refValue") && map.get("refValue") != null) { + result.refValue = String.valueOf(map.get("refValue")); + } + if (map.containsKey("turnIndex") && map.get("turnIndex") != null) { + result.turnIndex = (map.get("turnIndex") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("turnIndex")))); + } + if (map.containsKey("createdAt") && map.get("createdAt") != null) { + result.createdAt = String.valueOf(map.get("createdAt")); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + SessionRef obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.sessionId != null) result.put("sessionId", serializeScalar(obj.sessionId)); + if (obj.refType != null) result.put("refType", serializeScalar(obj.refType)); + if (obj.refValue != null) result.put("refValue", serializeScalar(obj.refValue)); + if (obj.turnIndex != null) result.put("turnIndex", serializeScalar(obj.turnIndex)); + if (obj.createdAt != null) result.put("createdAt", serializeScalar(obj.createdAt)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static SessionRef fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static SessionRef fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static SessionRef fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static SessionRef fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionStartPayload.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionStartPayload.java new file mode 100644 index 000000000..fcf95ab91 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionStartPayload.java @@ -0,0 +1,120 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class SessionStartPayload { + public static final String SHORTHAND_PROPERTY = null; + + public String sessionId = ""; + public String schemaVersion = null; + public String producer = null; + public String runtime = null; + public String promptyVersion = null; + public String startTime = null; + public String selectedModel = null; + public String reasoningEffort = null; + public HarnessContext context = null; + + public SessionStartPayload() { } + + @SuppressWarnings("unchecked") + public static SessionStartPayload load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new SessionStartPayload()); + } + SessionStartPayload result = new SessionStartPayload(); + SessionStartPayload.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(SessionStartPayload result, Map map, LoadContext ctx) { + if (map.containsKey("sessionId") && map.get("sessionId") != null) { + result.sessionId = String.valueOf(map.get("sessionId")); + } + if (map.containsKey("schemaVersion") && map.get("schemaVersion") != null) { + result.schemaVersion = String.valueOf(map.get("schemaVersion")); + } + if (map.containsKey("producer") && map.get("producer") != null) { + result.producer = String.valueOf(map.get("producer")); + } + if (map.containsKey("runtime") && map.get("runtime") != null) { + result.runtime = String.valueOf(map.get("runtime")); + } + if (map.containsKey("promptyVersion") && map.get("promptyVersion") != null) { + result.promptyVersion = String.valueOf(map.get("promptyVersion")); + } + if (map.containsKey("startTime") && map.get("startTime") != null) { + result.startTime = String.valueOf(map.get("startTime")); + } + if (map.containsKey("selectedModel") && map.get("selectedModel") != null) { + result.selectedModel = String.valueOf(map.get("selectedModel")); + } + if (map.containsKey("reasoningEffort") && map.get("reasoningEffort") != null) { + result.reasoningEffort = String.valueOf(map.get("reasoningEffort")); + } + if (map.containsKey("context") && map.get("context") != null) { + result.context = HarnessContext.load(map.get("context"), ctx); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + SessionStartPayload obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.sessionId != null) result.put("sessionId", serializeScalar(obj.sessionId)); + if (obj.schemaVersion != null) result.put("schemaVersion", serializeScalar(obj.schemaVersion)); + if (obj.producer != null) result.put("producer", serializeScalar(obj.producer)); + if (obj.runtime != null) result.put("runtime", serializeScalar(obj.runtime)); + if (obj.promptyVersion != null) result.put("promptyVersion", serializeScalar(obj.promptyVersion)); + if (obj.startTime != null) result.put("startTime", serializeScalar(obj.startTime)); + if (obj.selectedModel != null) result.put("selectedModel", serializeScalar(obj.selectedModel)); + if (obj.reasoningEffort != null) result.put("reasoningEffort", serializeScalar(obj.reasoningEffort)); + if (obj.context != null) result.put("context", obj.context.save(ctx)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static SessionStartPayload fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static SessionStartPayload fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static SessionStartPayload fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static SessionStartPayload fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionSummary.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionSummary.java new file mode 100644 index 000000000..958e254d7 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionSummary.java @@ -0,0 +1,105 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class SessionSummary { + public static final String SHORTHAND_PROPERTY = null; + + public String sessionId = ""; + public SessionSummaryStatus status = null; + public Integer turns = null; + public Integer checkpoints = null; + public TokenUsage usage = null; + public Double durationMs = null; + + public SessionSummary() { } + + @SuppressWarnings("unchecked") + public static SessionSummary load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new SessionSummary()); + } + SessionSummary result = new SessionSummary(); + SessionSummary.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(SessionSummary result, Map map, LoadContext ctx) { + if (map.containsKey("sessionId") && map.get("sessionId") != null) { + result.sessionId = String.valueOf(map.get("sessionId")); + } + if (map.containsKey("status") && map.get("status") != null) { + result.status = SessionSummaryStatus.fromValue(String.valueOf(map.get("status"))); + } + if (map.containsKey("turns") && map.get("turns") != null) { + result.turns = (map.get("turns") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("turns")))); + } + if (map.containsKey("checkpoints") && map.get("checkpoints") != null) { + result.checkpoints = (map.get("checkpoints") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("checkpoints")))); + } + if (map.containsKey("usage") && map.get("usage") != null) { + result.usage = TokenUsage.load(map.get("usage"), ctx); + } + if (map.containsKey("durationMs") && map.get("durationMs") != null) { + result.durationMs = (map.get("durationMs") instanceof Number n ? n.doubleValue() : Double.parseDouble(String.valueOf(map.get("durationMs")))); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + SessionSummary obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.sessionId != null) result.put("sessionId", serializeScalar(obj.sessionId)); + if (obj.status != null) result.put("status", obj.status.value); + if (obj.turns != null) result.put("turns", serializeScalar(obj.turns)); + if (obj.checkpoints != null) result.put("checkpoints", serializeScalar(obj.checkpoints)); + if (obj.usage != null) result.put("usage", obj.usage.save(ctx)); + if (obj.durationMs != null) result.put("durationMs", serializeScalar(obj.durationMs)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static SessionSummary fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static SessionSummary fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static SessionSummary fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static SessionSummary fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionSummaryStatus.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionSummaryStatus.java new file mode 100644 index 000000000..1a4e8e079 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionSummaryStatus.java @@ -0,0 +1,20 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +public enum SessionSummaryStatus { + SUCCESS("success"), + ERROR("error"), + CANCELLED("cancelled"), + INTERRUPTED("interrupted"), + ; + + public final String value; + SessionSummaryStatus(String value) { this.value = value; } + public static SessionSummaryStatus fromValue(String value) { + for (SessionSummaryStatus item : values()) { + if (item.value.equals(value)) return item; + } + return valueOf(value.toUpperCase().replace("-", "_")); + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionTrace.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionTrace.java new file mode 100644 index 000000000..8ce1a27e8 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionTrace.java @@ -0,0 +1,160 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class SessionTrace { + public static final String SHORTHAND_PROPERTY = null; + + public String version = "1"; + public String runtime = null; + public String promptyVersion = null; + public String sessionId = null; + public List events = new ArrayList<>(); + public List turns = null; + public List checkpoints = null; + public List trajectory = null; + public List files = null; + public List refs = null; + public SessionSummary summary = null; + + public SessionTrace() { } + + @SuppressWarnings("unchecked") + public static SessionTrace load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new SessionTrace()); + } + SessionTrace result = new SessionTrace(); + SessionTrace.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(SessionTrace result, Map map, LoadContext ctx) { + if (map.containsKey("version") && map.get("version") != null) { + result.version = String.valueOf(map.get("version")); + } + if (map.containsKey("runtime") && map.get("runtime") != null) { + result.runtime = String.valueOf(map.get("runtime")); + } + if (map.containsKey("promptyVersion") && map.get("promptyVersion") != null) { + result.promptyVersion = String.valueOf(map.get("promptyVersion")); + } + if (map.containsKey("sessionId") && map.get("sessionId") != null) { + result.sessionId = String.valueOf(map.get("sessionId")); + } + if (map.containsKey("events") && map.get("events") != null) { + result.events = ModelCollections.loadList( + map.get("events"), "events", SessionEvent.SHORTHAND_PROPERTY, SessionEvent::load, ctx); + } + if (map.containsKey("turns") && map.get("turns") != null) { + result.turns = ModelCollections.loadList( + map.get("turns"), "turns", TurnTrace.SHORTHAND_PROPERTY, TurnTrace::load, ctx); + } + if (map.containsKey("checkpoints") && map.get("checkpoints") != null) { + result.checkpoints = ModelCollections.loadList( + map.get("checkpoints"), "checkpoints", Checkpoint.SHORTHAND_PROPERTY, Checkpoint::load, ctx); + } + if (map.containsKey("trajectory") && map.get("trajectory") != null) { + result.trajectory = ModelCollections.loadList( + map.get("trajectory"), "trajectory", TrajectoryEvent.SHORTHAND_PROPERTY, TrajectoryEvent::load, ctx); + } + if (map.containsKey("files") && map.get("files") != null) { + result.files = ModelCollections.loadList( + map.get("files"), "files", SessionFileRef.SHORTHAND_PROPERTY, SessionFileRef::load, ctx); + } + if (map.containsKey("refs") && map.get("refs") != null) { + result.refs = ModelCollections.loadList( + map.get("refs"), "refs", SessionRef.SHORTHAND_PROPERTY, SessionRef::load, ctx); + } + if (map.containsKey("summary") && map.get("summary") != null) { + result.summary = SessionSummary.load(map.get("summary"), ctx); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + SessionTrace obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.version != null) result.put("version", serializeScalar(obj.version)); + if (obj.runtime != null) result.put("runtime", serializeScalar(obj.runtime)); + if (obj.promptyVersion != null) result.put("promptyVersion", serializeScalar(obj.promptyVersion)); + if (obj.sessionId != null) result.put("sessionId", serializeScalar(obj.sessionId)); + if (obj.events != null) { + List items = new ArrayList<>(); + for (SessionEvent item : obj.events) items.add(item.save(ctx)); + result.put("events", items); + } + if (obj.turns != null) { + List items = new ArrayList<>(); + for (TurnTrace item : obj.turns) items.add(item.save(ctx)); + result.put("turns", items); + } + if (obj.checkpoints != null) { + List items = new ArrayList<>(); + for (Checkpoint item : obj.checkpoints) items.add(item.save(ctx)); + result.put("checkpoints", items); + } + if (obj.trajectory != null) { + List items = new ArrayList<>(); + for (TrajectoryEvent item : obj.trajectory) items.add(item.save(ctx)); + result.put("trajectory", items); + } + if (obj.files != null) { + List items = new ArrayList<>(); + for (SessionFileRef item : obj.files) items.add(item.save(ctx)); + result.put("files", items); + } + if (obj.refs != null) { + List items = new ArrayList<>(); + for (SessionRef item : obj.refs) items.add(item.save(ctx)); + result.put("refs", items); + } + if (obj.summary != null) result.put("summary", obj.summary.save(ctx)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static SessionTrace fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static SessionTrace fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static SessionTrace fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static SessionTrace fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionWarningPayload.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionWarningPayload.java new file mode 100644 index 000000000..f84c4e5bb --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionWarningPayload.java @@ -0,0 +1,92 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class SessionWarningPayload { + public static final String SHORTHAND_PROPERTY = null; + + public String warningType = ""; + public String message = ""; + public Map details = null; + + public SessionWarningPayload() { } + + @SuppressWarnings("unchecked") + public static SessionWarningPayload load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new SessionWarningPayload()); + } + SessionWarningPayload result = new SessionWarningPayload(); + SessionWarningPayload.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(SessionWarningPayload result, Map map, LoadContext ctx) { + if (map.containsKey("warningType") && map.get("warningType") != null) { + result.warningType = String.valueOf(map.get("warningType")); + } + if (map.containsKey("message") && map.get("message") != null) { + result.message = String.valueOf(map.get("message")); + } + if (map.containsKey("details") && map.get("details") != null) { + if (map.get("details") instanceof Map dict) { + result.details = copyMap(dict); + } + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + SessionWarningPayload obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.warningType != null) result.put("warningType", serializeScalar(obj.warningType)); + if (obj.message != null) result.put("message", serializeScalar(obj.message)); + if (obj.details != null) result.put("details", serializeScalar(obj.details)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static SessionWarningPayload fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static SessionWarningPayload fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static SessionWarningPayload fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static SessionWarningPayload fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/StatusEventPayload.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/StatusEventPayload.java new file mode 100644 index 000000000..166fb7b5b --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/StatusEventPayload.java @@ -0,0 +1,80 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class StatusEventPayload { + public static final String SHORTHAND_PROPERTY = null; + + public String message = ""; + + public StatusEventPayload() { } + + @SuppressWarnings("unchecked") + public static StatusEventPayload load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new StatusEventPayload()); + } + StatusEventPayload result = new StatusEventPayload(); + StatusEventPayload.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(StatusEventPayload result, Map map, LoadContext ctx) { + if (map.containsKey("message") && map.get("message") != null) { + result.message = String.valueOf(map.get("message")); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + StatusEventPayload obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.message != null) result.put("message", serializeScalar(obj.message)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static StatusEventPayload fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static StatusEventPayload fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static StatusEventPayload fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static StatusEventPayload fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/StreamChunk.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/StreamChunk.java new file mode 100644 index 000000000..1b94a1fd6 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/StreamChunk.java @@ -0,0 +1,94 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public abstract class StreamChunk { + public static final String SHORTHAND_PROPERTY = null; + + public String kind = ""; + + public StreamChunk() { } + + @SuppressWarnings("unchecked") + public static StreamChunk load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (data instanceof Map dispatchMap) { + Object discriminator = dispatchMap.get("kind"); + if (discriminator != null) { + switch (String.valueOf(discriminator).toLowerCase(java.util.Locale.ROOT)) { + case "text": + return TextChunk.load(data, ctx); + case "thinking": + return ThinkingChunk.load(data, ctx); + case "tool": + return ToolChunk.load(data, ctx); + case "usage": + return UsageChunk.load(data, ctx); + case "error": + return ErrorChunk.load(data, ctx); + default: + break; + } + } + } + throw new IllegalArgumentException("Cannot instantiate abstract StreamChunk; expected a matching 'kind' discriminator."); + } + + static void loadBaseInto(StreamChunk result, Map map, LoadContext ctx) { + if (map.containsKey("kind") && map.get("kind") != null) { + result.kind = String.valueOf(map.get("kind")); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + StreamChunk obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.kind != null) result.put("kind", serializeScalar(obj.kind)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static StreamChunk fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static StreamChunk fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static StreamChunk fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static StreamChunk fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/StreamOptions.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/StreamOptions.java new file mode 100644 index 000000000..6d7da008e --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/StreamOptions.java @@ -0,0 +1,80 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class StreamOptions { + public static final String SHORTHAND_PROPERTY = null; + + public Boolean includeUsage = null; + + public StreamOptions() { } + + @SuppressWarnings("unchecked") + public static StreamOptions load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new StreamOptions()); + } + StreamOptions result = new StreamOptions(); + StreamOptions.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(StreamOptions result, Map map, LoadContext ctx) { + if (map.containsKey("includeUsage") && map.get("includeUsage") != null) { + result.includeUsage = (map.get("includeUsage") instanceof Boolean b ? b : Boolean.parseBoolean(String.valueOf(map.get("includeUsage")))); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + StreamOptions obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.includeUsage != null) result.put("includeUsage", serializeScalar(obj.includeUsage)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static StreamOptions fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static StreamOptions fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static StreamOptions fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static StreamOptions fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SubscriptionInfo.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SubscriptionInfo.java new file mode 100644 index 000000000..02bb74938 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SubscriptionInfo.java @@ -0,0 +1,114 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class SubscriptionInfo { + public static final String SHORTHAND_PROPERTY = null; + + public String subscriptionId = ""; + public String displayName = ""; + public String state = ""; + + public SubscriptionInfo() { } + + @SuppressWarnings("unchecked") + public static SubscriptionInfo load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new SubscriptionInfo()); + } + SubscriptionInfo result = new SubscriptionInfo(); + SubscriptionInfo.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(SubscriptionInfo result, Map map, LoadContext ctx) { + if (map.containsKey("subscriptionId") && map.get("subscriptionId") != null) { + result.subscriptionId = String.valueOf(map.get("subscriptionId")); + } + if (map.containsKey("displayName") && map.get("displayName") != null) { + result.displayName = String.valueOf(map.get("displayName")); + } + if (map.containsKey("state") && map.get("state") != null) { + result.state = String.valueOf(map.get("state")); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + SubscriptionInfo obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.subscriptionId != null) result.put("subscriptionId", serializeScalar(obj.subscriptionId)); + if (obj.displayName != null) result.put("displayName", serializeScalar(obj.displayName)); + if (obj.state != null) result.put("state", serializeScalar(obj.state)); + return ctx.processDict(result); + } + + public Map toWire(String provider) { + Map result = new LinkedHashMap<>(); + String target = provider == null ? "" : provider; + { + String wireName = "subscriptionId"; + boolean include = target.isEmpty(); + if (target.equals("foundry")) { wireName = "subscription_id"; include = true; } + if (include && this.subscriptionId != null) result.put(wireName, serializeScalar(this.subscriptionId)); + } + { + String wireName = "displayName"; + boolean include = target.isEmpty(); + if (target.equals("foundry")) { wireName = "display_name"; include = true; } + if (include && this.displayName != null) result.put(wireName, serializeScalar(this.displayName)); + } + { + String wireName = "state"; + boolean include = target.isEmpty(); + if (target.equals("foundry")) { wireName = "state"; include = true; } + if (include && this.state != null) result.put(wireName, serializeScalar(this.state)); + } + return result; + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static SubscriptionInfo fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static SubscriptionInfo fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static SubscriptionInfo fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static SubscriptionInfo fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Template.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Template.java new file mode 100644 index 000000000..8aa225294 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Template.java @@ -0,0 +1,85 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class Template { + public static final String SHORTHAND_PROPERTY = null; + + public FormatConfig format = null; + public ParserConfig parser = null; + + public Template() { } + + @SuppressWarnings("unchecked") + public static Template load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new Template()); + } + Template result = new Template(); + Template.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(Template result, Map map, LoadContext ctx) { + if (map.containsKey("format") && map.get("format") != null) { + result.format = FormatConfig.load(map.get("format"), ctx); + } + if (map.containsKey("parser") && map.get("parser") != null) { + result.parser = ParserConfig.load(map.get("parser"), ctx); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + Template obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.format != null) result.put("format", obj.format.save(ctx)); + if (obj.parser != null) result.put("parser", obj.parser.save(ctx)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static Template fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static Template fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static Template fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static Template fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TextChunk.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TextChunk.java new file mode 100644 index 000000000..efc90bb1d --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TextChunk.java @@ -0,0 +1,83 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class TextChunk extends StreamChunk { + public static final String SHORTHAND_PROPERTY = null; + + public String value = ""; + + public TextChunk() { + this.kind = "text"; + } + + @SuppressWarnings("unchecked") + public static TextChunk load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new TextChunk()); + } + TextChunk result = new TextChunk(); + TextChunk.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(TextChunk result, Map map, LoadContext ctx) { + StreamChunk.loadBaseInto(result, map, ctx); + if (map.containsKey("value") && map.get("value") != null) { + result.value = String.valueOf(map.get("value")); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + TextChunk obj = ctx.processObject(this); + Map result = super.save(ctx); + if (obj.value != null) result.put("value", serializeScalar(obj.value)); + return result; + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static TextChunk fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static TextChunk fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static TextChunk fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static TextChunk fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TextPart.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TextPart.java new file mode 100644 index 000000000..95a6454e4 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TextPart.java @@ -0,0 +1,83 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class TextPart extends ContentPart { + public static final String SHORTHAND_PROPERTY = null; + + public String value = ""; + + public TextPart() { + this.kind = "text"; + } + + @SuppressWarnings("unchecked") + public static TextPart load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new TextPart()); + } + TextPart result = new TextPart(); + TextPart.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(TextPart result, Map map, LoadContext ctx) { + ContentPart.loadBaseInto(result, map, ctx); + if (map.containsKey("value") && map.get("value") != null) { + result.value = String.valueOf(map.get("value")); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + TextPart obj = ctx.processObject(this); + Map result = super.save(ctx); + if (obj.value != null) result.put("value", serializeScalar(obj.value)); + return result; + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static TextPart fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static TextPart fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static TextPart fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static TextPart fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ThinkingChunk.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ThinkingChunk.java new file mode 100644 index 000000000..fc1c1ccb6 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ThinkingChunk.java @@ -0,0 +1,83 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class ThinkingChunk extends StreamChunk { + public static final String SHORTHAND_PROPERTY = null; + + public String value = ""; + + public ThinkingChunk() { + this.kind = "thinking"; + } + + @SuppressWarnings("unchecked") + public static ThinkingChunk load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new ThinkingChunk()); + } + ThinkingChunk result = new ThinkingChunk(); + ThinkingChunk.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(ThinkingChunk result, Map map, LoadContext ctx) { + StreamChunk.loadBaseInto(result, map, ctx); + if (map.containsKey("value") && map.get("value") != null) { + result.value = String.valueOf(map.get("value")); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + ThinkingChunk obj = ctx.processObject(this); + Map result = super.save(ctx); + if (obj.value != null) result.put("value", serializeScalar(obj.value)); + return result; + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static ThinkingChunk fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static ThinkingChunk fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static ThinkingChunk fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static ThinkingChunk fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ThinkingEventPayload.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ThinkingEventPayload.java new file mode 100644 index 000000000..7ac4abd30 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ThinkingEventPayload.java @@ -0,0 +1,80 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class ThinkingEventPayload { + public static final String SHORTHAND_PROPERTY = null; + + public String token = ""; + + public ThinkingEventPayload() { } + + @SuppressWarnings("unchecked") + public static ThinkingEventPayload load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new ThinkingEventPayload()); + } + ThinkingEventPayload result = new ThinkingEventPayload(); + ThinkingEventPayload.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(ThinkingEventPayload result, Map map, LoadContext ctx) { + if (map.containsKey("token") && map.get("token") != null) { + result.token = String.valueOf(map.get("token")); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + ThinkingEventPayload obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.token != null) result.put("token", serializeScalar(obj.token)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static ThinkingEventPayload fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static ThinkingEventPayload fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static ThinkingEventPayload fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static ThinkingEventPayload fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ThreadMarker.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ThreadMarker.java new file mode 100644 index 000000000..b247d3289 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ThreadMarker.java @@ -0,0 +1,85 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class ThreadMarker { + public static final String SHORTHAND_PROPERTY = null; + + public String name = "thread"; + public String kind = "thread"; + + public ThreadMarker() { } + + @SuppressWarnings("unchecked") + public static ThreadMarker load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new ThreadMarker()); + } + ThreadMarker result = new ThreadMarker(); + ThreadMarker.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(ThreadMarker result, Map map, LoadContext ctx) { + if (map.containsKey("name") && map.get("name") != null) { + result.name = String.valueOf(map.get("name")); + } + if (map.containsKey("kind") && map.get("kind") != null) { + result.kind = String.valueOf(map.get("kind")); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + ThreadMarker obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.name != null) result.put("name", serializeScalar(obj.name)); + if (obj.kind != null) result.put("kind", serializeScalar(obj.kind)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static ThreadMarker fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static ThreadMarker fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static ThreadMarker fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static ThreadMarker fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TokenEventPayload.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TokenEventPayload.java new file mode 100644 index 000000000..4f2568d48 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TokenEventPayload.java @@ -0,0 +1,80 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class TokenEventPayload { + public static final String SHORTHAND_PROPERTY = null; + + public String token = ""; + + public TokenEventPayload() { } + + @SuppressWarnings("unchecked") + public static TokenEventPayload load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new TokenEventPayload()); + } + TokenEventPayload result = new TokenEventPayload(); + TokenEventPayload.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(TokenEventPayload result, Map map, LoadContext ctx) { + if (map.containsKey("token") && map.get("token") != null) { + result.token = String.valueOf(map.get("token")); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + TokenEventPayload obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.token != null) result.put("token", serializeScalar(obj.token)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static TokenEventPayload fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static TokenEventPayload fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static TokenEventPayload fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static TokenEventPayload fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TokenUsage.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TokenUsage.java new file mode 100644 index 000000000..62974fda8 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TokenUsage.java @@ -0,0 +1,116 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class TokenUsage { + public static final String SHORTHAND_PROPERTY = null; + + public Integer promptTokens = null; + public Integer completionTokens = null; + public Integer totalTokens = null; + + public TokenUsage() { } + + @SuppressWarnings("unchecked") + public static TokenUsage load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new TokenUsage()); + } + TokenUsage result = new TokenUsage(); + TokenUsage.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(TokenUsage result, Map map, LoadContext ctx) { + if (map.containsKey("promptTokens") && map.get("promptTokens") != null) { + result.promptTokens = (map.get("promptTokens") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("promptTokens")))); + } + if (map.containsKey("completionTokens") && map.get("completionTokens") != null) { + result.completionTokens = (map.get("completionTokens") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("completionTokens")))); + } + if (map.containsKey("totalTokens") && map.get("totalTokens") != null) { + result.totalTokens = (map.get("totalTokens") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("totalTokens")))); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + TokenUsage obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.promptTokens != null) result.put("promptTokens", serializeScalar(obj.promptTokens)); + if (obj.completionTokens != null) result.put("completionTokens", serializeScalar(obj.completionTokens)); + if (obj.totalTokens != null) result.put("totalTokens", serializeScalar(obj.totalTokens)); + return ctx.processDict(result); + } + + public Map toWire(String provider) { + Map result = new LinkedHashMap<>(); + String target = provider == null ? "" : provider; + { + String wireName = "promptTokens"; + boolean include = target.isEmpty(); + if (target.equals("openai")) { wireName = "prompt_tokens"; include = true; } + if (target.equals("anthropic")) { wireName = "input_tokens"; include = true; } + if (include && this.promptTokens != null) result.put(wireName, serializeScalar(this.promptTokens)); + } + { + String wireName = "completionTokens"; + boolean include = target.isEmpty(); + if (target.equals("openai")) { wireName = "completion_tokens"; include = true; } + if (target.equals("anthropic")) { wireName = "output_tokens"; include = true; } + if (include && this.completionTokens != null) result.put(wireName, serializeScalar(this.completionTokens)); + } + { + String wireName = "totalTokens"; + boolean include = target.isEmpty(); + if (target.equals("openai")) { wireName = "total_tokens"; include = true; } + if (include && this.totalTokens != null) result.put(wireName, serializeScalar(this.totalTokens)); + } + return result; + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static TokenUsage fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static TokenUsage fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static TokenUsage fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static TokenUsage fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Tool.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Tool.java new file mode 100644 index 000000000..554cf94c6 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Tool.java @@ -0,0 +1,111 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public abstract class Tool { + public static final String SHORTHAND_PROPERTY = null; + + public String name = ""; + public String kind = ""; + public String description = null; + public List bindings = null; + + public Tool() { } + + @SuppressWarnings("unchecked") + public static Tool load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (data instanceof Map dispatchMap) { + Object discriminator = dispatchMap.get("kind"); + if (discriminator != null) { + switch (String.valueOf(discriminator).toLowerCase(java.util.Locale.ROOT)) { + case "function": + return FunctionTool.load(data, ctx); + case "mcp": + return McpTool.load(data, ctx); + case "openapi": + return OpenApiTool.load(data, ctx); + case "prompty": + return PromptyTool.load(data, ctx); + default: + return CustomTool.load(data, ctx); + } + } + } + throw new IllegalArgumentException("Cannot instantiate abstract Tool; expected a matching 'kind' discriminator."); + } + + static void loadBaseInto(Tool result, Map map, LoadContext ctx) { + if (map.containsKey("name") && map.get("name") != null) { + result.name = String.valueOf(map.get("name")); + } + if (map.containsKey("kind") && map.get("kind") != null) { + result.kind = String.valueOf(map.get("kind")); + } + if (map.containsKey("description") && map.get("description") != null) { + result.description = String.valueOf(map.get("description")); + } + if (map.containsKey("bindings") && map.get("bindings") != null) { + result.bindings = ModelCollections.loadList( + map.get("bindings"), "bindings", Binding.SHORTHAND_PROPERTY, Binding::load, ctx); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + Tool obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.name != null) result.put("name", serializeScalar(obj.name)); + if (obj.kind != null) result.put("kind", serializeScalar(obj.kind)); + if (obj.description != null) result.put("description", serializeScalar(obj.description)); + if (obj.bindings != null) { + result.put("bindings", ModelCollections.saveList( + obj.bindings, Binding.SHORTHAND_PROPERTY, item -> item.save(ctx), ctx)); + } + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static Tool fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static Tool fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static Tool fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static Tool fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolCall.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolCall.java new file mode 100644 index 000000000..43c85cd71 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolCall.java @@ -0,0 +1,90 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class ToolCall { + public static final String SHORTHAND_PROPERTY = null; + + public String id = ""; + public String name = ""; + public String arguments = ""; + + public ToolCall() { } + + @SuppressWarnings("unchecked") + public static ToolCall load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new ToolCall()); + } + ToolCall result = new ToolCall(); + ToolCall.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(ToolCall result, Map map, LoadContext ctx) { + if (map.containsKey("id") && map.get("id") != null) { + result.id = String.valueOf(map.get("id")); + } + if (map.containsKey("name") && map.get("name") != null) { + result.name = String.valueOf(map.get("name")); + } + if (map.containsKey("arguments") && map.get("arguments") != null) { + result.arguments = String.valueOf(map.get("arguments")); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + ToolCall obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.id != null) result.put("id", serializeScalar(obj.id)); + if (obj.name != null) result.put("name", serializeScalar(obj.name)); + if (obj.arguments != null) result.put("arguments", serializeScalar(obj.arguments)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static ToolCall fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static ToolCall fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static ToolCall fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static ToolCall fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolCallCompletePayload.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolCallCompletePayload.java new file mode 100644 index 000000000..4ef9a14f1 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolCallCompletePayload.java @@ -0,0 +1,105 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class ToolCallCompletePayload { + public static final String SHORTHAND_PROPERTY = null; + + public String id = null; + public String name = ""; + public Boolean success = false; + public ToolResult result = null; + public Double durationMs = null; + public String errorKind = null; + + public ToolCallCompletePayload() { } + + @SuppressWarnings("unchecked") + public static ToolCallCompletePayload load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new ToolCallCompletePayload()); + } + ToolCallCompletePayload result = new ToolCallCompletePayload(); + ToolCallCompletePayload.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(ToolCallCompletePayload result, Map map, LoadContext ctx) { + if (map.containsKey("id") && map.get("id") != null) { + result.id = String.valueOf(map.get("id")); + } + if (map.containsKey("name") && map.get("name") != null) { + result.name = String.valueOf(map.get("name")); + } + if (map.containsKey("success") && map.get("success") != null) { + result.success = (map.get("success") instanceof Boolean b ? b : Boolean.parseBoolean(String.valueOf(map.get("success")))); + } + if (map.containsKey("result") && map.get("result") != null) { + result.result = ToolResult.load(map.get("result"), ctx); + } + if (map.containsKey("durationMs") && map.get("durationMs") != null) { + result.durationMs = (map.get("durationMs") instanceof Number n ? n.doubleValue() : Double.parseDouble(String.valueOf(map.get("durationMs")))); + } + if (map.containsKey("errorKind") && map.get("errorKind") != null) { + result.errorKind = String.valueOf(map.get("errorKind")); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + ToolCallCompletePayload obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.id != null) result.put("id", serializeScalar(obj.id)); + if (obj.name != null) result.put("name", serializeScalar(obj.name)); + if (obj.success != null) result.put("success", serializeScalar(obj.success)); + if (obj.result != null) result.put("result", obj.result.save(ctx)); + if (obj.durationMs != null) result.put("durationMs", serializeScalar(obj.durationMs)); + if (obj.errorKind != null) result.put("errorKind", serializeScalar(obj.errorKind)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static ToolCallCompletePayload fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static ToolCallCompletePayload fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static ToolCallCompletePayload fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static ToolCallCompletePayload fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolCallStartPayload.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolCallStartPayload.java new file mode 100644 index 000000000..c6de661b5 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolCallStartPayload.java @@ -0,0 +1,90 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class ToolCallStartPayload { + public static final String SHORTHAND_PROPERTY = null; + + public String id = null; + public String name = ""; + public String arguments = ""; + + public ToolCallStartPayload() { } + + @SuppressWarnings("unchecked") + public static ToolCallStartPayload load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new ToolCallStartPayload()); + } + ToolCallStartPayload result = new ToolCallStartPayload(); + ToolCallStartPayload.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(ToolCallStartPayload result, Map map, LoadContext ctx) { + if (map.containsKey("id") && map.get("id") != null) { + result.id = String.valueOf(map.get("id")); + } + if (map.containsKey("name") && map.get("name") != null) { + result.name = String.valueOf(map.get("name")); + } + if (map.containsKey("arguments") && map.get("arguments") != null) { + result.arguments = String.valueOf(map.get("arguments")); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + ToolCallStartPayload obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.id != null) result.put("id", serializeScalar(obj.id)); + if (obj.name != null) result.put("name", serializeScalar(obj.name)); + if (obj.arguments != null) result.put("arguments", serializeScalar(obj.arguments)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static ToolCallStartPayload fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static ToolCallStartPayload fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static ToolCallStartPayload fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static ToolCallStartPayload fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolChunk.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolChunk.java new file mode 100644 index 000000000..0324d9c55 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolChunk.java @@ -0,0 +1,83 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class ToolChunk extends StreamChunk { + public static final String SHORTHAND_PROPERTY = null; + + public ToolCall toolCall = null; + + public ToolChunk() { + this.kind = "tool"; + } + + @SuppressWarnings("unchecked") + public static ToolChunk load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new ToolChunk()); + } + ToolChunk result = new ToolChunk(); + ToolChunk.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(ToolChunk result, Map map, LoadContext ctx) { + StreamChunk.loadBaseInto(result, map, ctx); + if (map.containsKey("toolCall") && map.get("toolCall") != null) { + result.toolCall = ToolCall.load(map.get("toolCall"), ctx); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + ToolChunk obj = ctx.processObject(this); + Map result = super.save(ctx); + if (obj.toolCall != null) result.put("toolCall", obj.toolCall.save(ctx)); + return result; + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static ToolChunk fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static ToolChunk fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static ToolChunk fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static ToolChunk fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolContext.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolContext.java new file mode 100644 index 000000000..59585a8f1 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolContext.java @@ -0,0 +1,92 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class ToolContext { + public static final String SHORTHAND_PROPERTY = null; + + public List messages = new ArrayList<>(); + public Map metadata = null; + + public ToolContext() { } + + @SuppressWarnings("unchecked") + public static ToolContext load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new ToolContext()); + } + ToolContext result = new ToolContext(); + ToolContext.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(ToolContext result, Map map, LoadContext ctx) { + if (map.containsKey("messages") && map.get("messages") != null) { + result.messages = ModelCollections.loadList( + map.get("messages"), "messages", Message.SHORTHAND_PROPERTY, Message::load, ctx); + } + if (map.containsKey("metadata") && map.get("metadata") != null) { + if (map.get("metadata") instanceof Map dict) { + result.metadata = copyMap(dict); + } + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + ToolContext obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.messages != null) { + List items = new ArrayList<>(); + for (Message item : obj.messages) items.add(item.save(ctx)); + result.put("messages", items); + } + if (obj.metadata != null) result.put("metadata", serializeScalar(obj.metadata)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static ToolContext fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static ToolContext fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static ToolContext fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static ToolContext fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolDispatchResult.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolDispatchResult.java new file mode 100644 index 000000000..99c856557 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolDispatchResult.java @@ -0,0 +1,90 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class ToolDispatchResult { + public static final String SHORTHAND_PROPERTY = null; + + public String toolCallId = ""; + public String name = ""; + public ToolResult result = null; + + public ToolDispatchResult() { } + + @SuppressWarnings("unchecked") + public static ToolDispatchResult load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new ToolDispatchResult()); + } + ToolDispatchResult result = new ToolDispatchResult(); + ToolDispatchResult.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(ToolDispatchResult result, Map map, LoadContext ctx) { + if (map.containsKey("toolCallId") && map.get("toolCallId") != null) { + result.toolCallId = String.valueOf(map.get("toolCallId")); + } + if (map.containsKey("name") && map.get("name") != null) { + result.name = String.valueOf(map.get("name")); + } + if (map.containsKey("result") && map.get("result") != null) { + result.result = ToolResult.load(map.get("result"), ctx); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + ToolDispatchResult obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.toolCallId != null) result.put("toolCallId", serializeScalar(obj.toolCallId)); + if (obj.name != null) result.put("name", serializeScalar(obj.name)); + if (obj.result != null) result.put("result", obj.result.save(ctx)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static ToolDispatchResult fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static ToolDispatchResult fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static ToolDispatchResult fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static ToolDispatchResult fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolExecutionCompletePayload.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolExecutionCompletePayload.java new file mode 100644 index 000000000..98c12cb4f --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolExecutionCompletePayload.java @@ -0,0 +1,127 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class ToolExecutionCompletePayload { + public static final String SHORTHAND_PROPERTY = null; + + public String requestId = null; + public String toolCallId = null; + public String toolName = ""; + public Boolean success = false; + public Object result = null; + public Integer exitCode = null; + public Double durationMs = null; + public String errorKind = null; + public Map telemetry = null; + public RedactionMetadata redaction = null; + + public ToolExecutionCompletePayload() { } + + @SuppressWarnings("unchecked") + public static ToolExecutionCompletePayload load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new ToolExecutionCompletePayload()); + } + ToolExecutionCompletePayload result = new ToolExecutionCompletePayload(); + ToolExecutionCompletePayload.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(ToolExecutionCompletePayload result, Map map, LoadContext ctx) { + if (map.containsKey("requestId") && map.get("requestId") != null) { + result.requestId = String.valueOf(map.get("requestId")); + } + if (map.containsKey("toolCallId") && map.get("toolCallId") != null) { + result.toolCallId = String.valueOf(map.get("toolCallId")); + } + if (map.containsKey("toolName") && map.get("toolName") != null) { + result.toolName = String.valueOf(map.get("toolName")); + } + if (map.containsKey("success") && map.get("success") != null) { + result.success = (map.get("success") instanceof Boolean b ? b : Boolean.parseBoolean(String.valueOf(map.get("success")))); + } + if (map.containsKey("result") && map.get("result") != null) { + result.result = map.get("result"); + } + if (map.containsKey("exitCode") && map.get("exitCode") != null) { + result.exitCode = (map.get("exitCode") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("exitCode")))); + } + if (map.containsKey("durationMs") && map.get("durationMs") != null) { + result.durationMs = (map.get("durationMs") instanceof Number n ? n.doubleValue() : Double.parseDouble(String.valueOf(map.get("durationMs")))); + } + if (map.containsKey("errorKind") && map.get("errorKind") != null) { + result.errorKind = String.valueOf(map.get("errorKind")); + } + if (map.containsKey("telemetry") && map.get("telemetry") != null) { + if (map.get("telemetry") instanceof Map dict) { + result.telemetry = copyMap(dict); + } + } + if (map.containsKey("redaction") && map.get("redaction") != null) { + result.redaction = RedactionMetadata.load(map.get("redaction"), ctx); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + ToolExecutionCompletePayload obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.requestId != null) result.put("requestId", serializeScalar(obj.requestId)); + if (obj.toolCallId != null) result.put("toolCallId", serializeScalar(obj.toolCallId)); + if (obj.toolName != null) result.put("toolName", serializeScalar(obj.toolName)); + if (obj.success != null) result.put("success", serializeScalar(obj.success)); + if (obj.result != null) result.put("result", serializeScalar(obj.result)); + if (obj.exitCode != null) result.put("exitCode", serializeScalar(obj.exitCode)); + if (obj.durationMs != null) result.put("durationMs", serializeScalar(obj.durationMs)); + if (obj.errorKind != null) result.put("errorKind", serializeScalar(obj.errorKind)); + if (obj.telemetry != null) result.put("telemetry", serializeScalar(obj.telemetry)); + if (obj.redaction != null) result.put("redaction", obj.redaction.save(ctx)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static ToolExecutionCompletePayload fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static ToolExecutionCompletePayload fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static ToolExecutionCompletePayload fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static ToolExecutionCompletePayload fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolExecutionStartPayload.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolExecutionStartPayload.java new file mode 100644 index 000000000..ae3db40f0 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolExecutionStartPayload.java @@ -0,0 +1,107 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class ToolExecutionStartPayload { + public static final String SHORTHAND_PROPERTY = null; + + public String requestId = null; + public String toolCallId = null; + public String toolName = ""; + public Map arguments = null; + public String workingDirectory = null; + public RedactionMetadata redaction = null; + + public ToolExecutionStartPayload() { } + + @SuppressWarnings("unchecked") + public static ToolExecutionStartPayload load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new ToolExecutionStartPayload()); + } + ToolExecutionStartPayload result = new ToolExecutionStartPayload(); + ToolExecutionStartPayload.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(ToolExecutionStartPayload result, Map map, LoadContext ctx) { + if (map.containsKey("requestId") && map.get("requestId") != null) { + result.requestId = String.valueOf(map.get("requestId")); + } + if (map.containsKey("toolCallId") && map.get("toolCallId") != null) { + result.toolCallId = String.valueOf(map.get("toolCallId")); + } + if (map.containsKey("toolName") && map.get("toolName") != null) { + result.toolName = String.valueOf(map.get("toolName")); + } + if (map.containsKey("arguments") && map.get("arguments") != null) { + if (map.get("arguments") instanceof Map dict) { + result.arguments = copyMap(dict); + } + } + if (map.containsKey("workingDirectory") && map.get("workingDirectory") != null) { + result.workingDirectory = String.valueOf(map.get("workingDirectory")); + } + if (map.containsKey("redaction") && map.get("redaction") != null) { + result.redaction = RedactionMetadata.load(map.get("redaction"), ctx); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + ToolExecutionStartPayload obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.requestId != null) result.put("requestId", serializeScalar(obj.requestId)); + if (obj.toolCallId != null) result.put("toolCallId", serializeScalar(obj.toolCallId)); + if (obj.toolName != null) result.put("toolName", serializeScalar(obj.toolName)); + if (obj.arguments != null) result.put("arguments", serializeScalar(obj.arguments)); + if (obj.workingDirectory != null) result.put("workingDirectory", serializeScalar(obj.workingDirectory)); + if (obj.redaction != null) result.put("redaction", obj.redaction.save(ctx)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static ToolExecutionStartPayload fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static ToolExecutionStartPayload fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static ToolExecutionStartPayload fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static ToolExecutionStartPayload fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolResult.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolResult.java new file mode 100644 index 000000000..8d309019a --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolResult.java @@ -0,0 +1,113 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class ToolResult { + public static final String SHORTHAND_PROPERTY = null; + + public List parts = new ArrayList<>(); + public ToolResultStatus status = null; + public String errorKind = null; + public String errorMessage = null; + public Double durationMs = null; + + public ToolResult() { } + + @SuppressWarnings("unchecked") + public static ToolResult load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new ToolResult()); + } + ToolResult result = new ToolResult(); + ToolResult.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(ToolResult result, Map map, LoadContext ctx) { + if (map.containsKey("parts") && map.get("parts") != null) { + result.parts = ModelCollections.loadList( + map.get("parts"), "parts", ContentPart.SHORTHAND_PROPERTY, ContentPart::load, ctx); + } + if (map.containsKey("status") && map.get("status") != null) { + result.status = ToolResultStatus.fromValue(String.valueOf(map.get("status"))); + } + if (map.containsKey("errorKind") && map.get("errorKind") != null) { + result.errorKind = String.valueOf(map.get("errorKind")); + } + if (map.containsKey("errorMessage") && map.get("errorMessage") != null) { + result.errorMessage = String.valueOf(map.get("errorMessage")); + } + if (map.containsKey("durationMs") && map.get("durationMs") != null) { + result.durationMs = (map.get("durationMs") instanceof Number n ? n.doubleValue() : Double.parseDouble(String.valueOf(map.get("durationMs")))); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + ToolResult obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.parts != null) { + List items = new ArrayList<>(); + for (ContentPart item : obj.parts) items.add(item.save(ctx)); + result.put("parts", items); + } + if (obj.status != null) result.put("status", obj.status.value); + if (obj.errorKind != null) result.put("errorKind", serializeScalar(obj.errorKind)); + if (obj.errorMessage != null) result.put("errorMessage", serializeScalar(obj.errorMessage)); + if (obj.durationMs != null) result.put("durationMs", serializeScalar(obj.durationMs)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static ToolResult fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static ToolResult fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static ToolResult fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static ToolResult fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + public static ToolResult text(String value) { + return new ToolResult() {{ this.parts = new java.util.ArrayList<>(java.util.Arrays.asList(new TextPart() {{ this.value = value; }})); }}; + } + + public String text() { + return ToolResultMethods.text(this); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolResultMethods.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolResultMethods.java new file mode 100644 index 000000000..779af40e4 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolResultMethods.java @@ -0,0 +1,26 @@ +// Typra extension seam. This file is created once and is safe to edit. +package com.microsoft.prompty.model; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * Hand-written implementations for the {@code @method} declarations on + * {@link ToolResult}. + * + *

The emitter creates this file once, when missing, and never rewrites it, + * so it is the designated home for these bodies. Behaviour mirrors the Rust + * reference implementation in {@code runtime/rust/prompty/src/model_ext.rs}. + */ +public final class ToolResultMethods { + private ToolResultMethods() { } + + /** Concatenates every {@link TextPart} value, joined by newline. */ + public static String text(ToolResult self) { + List parts = self.parts == null ? List.of() : self.parts; + return parts.stream() + .filter(TextPart.class::isInstance) + .map(part -> ((TextPart) part).value) + .collect(Collectors.joining("\n")); + } +} \ No newline at end of file diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolResultPayload.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolResultPayload.java new file mode 100644 index 000000000..f13c0a803 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolResultPayload.java @@ -0,0 +1,85 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class ToolResultPayload { + public static final String SHORTHAND_PROPERTY = null; + + public String name = ""; + public ToolResult result = null; + + public ToolResultPayload() { } + + @SuppressWarnings("unchecked") + public static ToolResultPayload load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new ToolResultPayload()); + } + ToolResultPayload result = new ToolResultPayload(); + ToolResultPayload.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(ToolResultPayload result, Map map, LoadContext ctx) { + if (map.containsKey("name") && map.get("name") != null) { + result.name = String.valueOf(map.get("name")); + } + if (map.containsKey("result") && map.get("result") != null) { + result.result = ToolResult.load(map.get("result"), ctx); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + ToolResultPayload obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.name != null) result.put("name", serializeScalar(obj.name)); + if (obj.result != null) result.put("result", obj.result.save(ctx)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static ToolResultPayload fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static ToolResultPayload fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static ToolResultPayload fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static ToolResultPayload fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolResultStatus.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolResultStatus.java new file mode 100644 index 000000000..5c86c3c64 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolResultStatus.java @@ -0,0 +1,20 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +public enum ToolResultStatus { + SUCCESS("success"), + ERROR("error"), + CANCELLED("cancelled"), + TIMEOUT("timeout"), + ; + + public final String value; + ToolResultStatus(String value) { this.value = value; } + public static ToolResultStatus fromValue(String value) { + for (ToolResultStatus item : values()) { + if (item.value.equals(value)) return item; + } + return valueOf(value.toUpperCase().replace("-", "_")); + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TraceFile.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TraceFile.java new file mode 100644 index 000000000..9392c961c --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TraceFile.java @@ -0,0 +1,90 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class TraceFile { + public static final String SHORTHAND_PROPERTY = null; + + public String runtime = ""; + public String version = ""; + public TraceSpan trace = null; + + public TraceFile() { } + + @SuppressWarnings("unchecked") + public static TraceFile load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new TraceFile()); + } + TraceFile result = new TraceFile(); + TraceFile.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(TraceFile result, Map map, LoadContext ctx) { + if (map.containsKey("runtime") && map.get("runtime") != null) { + result.runtime = String.valueOf(map.get("runtime")); + } + if (map.containsKey("version") && map.get("version") != null) { + result.version = String.valueOf(map.get("version")); + } + if (map.containsKey("trace") && map.get("trace") != null) { + result.trace = TraceSpan.load(map.get("trace"), ctx); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + TraceFile obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.runtime != null) result.put("runtime", serializeScalar(obj.runtime)); + if (obj.version != null) result.put("version", serializeScalar(obj.version)); + if (obj.trace != null) result.put("trace", obj.trace.save(ctx)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static TraceFile fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static TraceFile fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static TraceFile fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static TraceFile fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TraceSpan.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TraceSpan.java new file mode 100644 index 000000000..d560aa8d8 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TraceSpan.java @@ -0,0 +1,129 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class TraceSpan { + public static final String SHORTHAND_PROPERTY = null; + + public String name = ""; + public TraceTime __time = null; + public String signature = null; + public Map inputs = null; + public Object output = null; + public String error = null; + public TokenUsage __usage = null; + public Map attributes = null; + public List __frames = null; + + public TraceSpan() { } + + @SuppressWarnings("unchecked") + public static TraceSpan load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new TraceSpan()); + } + TraceSpan result = new TraceSpan(); + TraceSpan.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(TraceSpan result, Map map, LoadContext ctx) { + if (map.containsKey("name") && map.get("name") != null) { + result.name = String.valueOf(map.get("name")); + } + if (map.containsKey("__time") && map.get("__time") != null) { + result.__time = TraceTime.load(map.get("__time"), ctx); + } + if (map.containsKey("signature") && map.get("signature") != null) { + result.signature = String.valueOf(map.get("signature")); + } + if (map.containsKey("inputs") && map.get("inputs") != null) { + if (map.get("inputs") instanceof Map dict) { + result.inputs = copyMap(dict); + } + } + if (map.containsKey("output") && map.get("output") != null) { + result.output = map.get("output"); + } + if (map.containsKey("error") && map.get("error") != null) { + result.error = String.valueOf(map.get("error")); + } + if (map.containsKey("__usage") && map.get("__usage") != null) { + result.__usage = TokenUsage.load(map.get("__usage"), ctx); + } + if (map.containsKey("attributes") && map.get("attributes") != null) { + if (map.get("attributes") instanceof Map dict) { + result.attributes = copyMap(dict); + } + } + if (map.containsKey("__frames") && map.get("__frames") != null) { + result.__frames = new ArrayList<>(); + if (map.get("__frames") instanceof Iterable values) { + for (Object item : values) { + result.__frames.add(item); + } + } + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + TraceSpan obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.name != null) result.put("name", serializeScalar(obj.name)); + if (obj.__time != null) result.put("__time", obj.__time.save(ctx)); + if (obj.signature != null) result.put("signature", serializeScalar(obj.signature)); + if (obj.inputs != null) result.put("inputs", serializeScalar(obj.inputs)); + if (obj.output != null) result.put("output", serializeScalar(obj.output)); + if (obj.error != null) result.put("error", serializeScalar(obj.error)); + if (obj.__usage != null) result.put("__usage", obj.__usage.save(ctx)); + if (obj.attributes != null) result.put("attributes", serializeScalar(obj.attributes)); + if (obj.__frames != null) result.put("__frames", new ArrayList<>(obj.__frames)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static TraceSpan fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static TraceSpan fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static TraceSpan fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static TraceSpan fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TraceTime.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TraceTime.java new file mode 100644 index 000000000..c5a302478 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TraceTime.java @@ -0,0 +1,90 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class TraceTime { + public static final String SHORTHAND_PROPERTY = null; + + public String start = ""; + public String end = ""; + public Double duration = 0.0d; + + public TraceTime() { } + + @SuppressWarnings("unchecked") + public static TraceTime load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new TraceTime()); + } + TraceTime result = new TraceTime(); + TraceTime.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(TraceTime result, Map map, LoadContext ctx) { + if (map.containsKey("start") && map.get("start") != null) { + result.start = String.valueOf(map.get("start")); + } + if (map.containsKey("end") && map.get("end") != null) { + result.end = String.valueOf(map.get("end")); + } + if (map.containsKey("duration") && map.get("duration") != null) { + result.duration = (map.get("duration") instanceof Number n ? n.doubleValue() : Double.parseDouble(String.valueOf(map.get("duration")))); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + TraceTime obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.start != null) result.put("start", serializeScalar(obj.start)); + if (obj.end != null) result.put("end", serializeScalar(obj.end)); + if (obj.duration != null) result.put("duration", serializeScalar(obj.duration)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static TraceTime fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static TraceTime fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static TraceTime fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static TraceTime fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TrajectoryEvent.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TrajectoryEvent.java new file mode 100644 index 000000000..a9a1c1122 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TrajectoryEvent.java @@ -0,0 +1,122 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class TrajectoryEvent { + public static final String SHORTHAND_PROPERTY = null; + + public String id = null; + public String sessionId = null; + public String turnId = null; + public String toolCallId = null; + public Integer turnIndex = null; + public String eventType = ""; + public Map data = null; + public String createdAt = null; + public RedactionMetadata redaction = null; + + public TrajectoryEvent() { } + + @SuppressWarnings("unchecked") + public static TrajectoryEvent load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new TrajectoryEvent()); + } + TrajectoryEvent result = new TrajectoryEvent(); + TrajectoryEvent.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(TrajectoryEvent result, Map map, LoadContext ctx) { + if (map.containsKey("id") && map.get("id") != null) { + result.id = String.valueOf(map.get("id")); + } + if (map.containsKey("sessionId") && map.get("sessionId") != null) { + result.sessionId = String.valueOf(map.get("sessionId")); + } + if (map.containsKey("turnId") && map.get("turnId") != null) { + result.turnId = String.valueOf(map.get("turnId")); + } + if (map.containsKey("toolCallId") && map.get("toolCallId") != null) { + result.toolCallId = String.valueOf(map.get("toolCallId")); + } + if (map.containsKey("turnIndex") && map.get("turnIndex") != null) { + result.turnIndex = (map.get("turnIndex") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("turnIndex")))); + } + if (map.containsKey("eventType") && map.get("eventType") != null) { + result.eventType = String.valueOf(map.get("eventType")); + } + if (map.containsKey("data") && map.get("data") != null) { + if (map.get("data") instanceof Map dict) { + result.data = copyMap(dict); + } + } + if (map.containsKey("createdAt") && map.get("createdAt") != null) { + result.createdAt = String.valueOf(map.get("createdAt")); + } + if (map.containsKey("redaction") && map.get("redaction") != null) { + result.redaction = RedactionMetadata.load(map.get("redaction"), ctx); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + TrajectoryEvent obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.id != null) result.put("id", serializeScalar(obj.id)); + if (obj.sessionId != null) result.put("sessionId", serializeScalar(obj.sessionId)); + if (obj.turnId != null) result.put("turnId", serializeScalar(obj.turnId)); + if (obj.toolCallId != null) result.put("toolCallId", serializeScalar(obj.toolCallId)); + if (obj.turnIndex != null) result.put("turnIndex", serializeScalar(obj.turnIndex)); + if (obj.eventType != null) result.put("eventType", serializeScalar(obj.eventType)); + if (obj.data != null) result.put("data", serializeScalar(obj.data)); + if (obj.createdAt != null) result.put("createdAt", serializeScalar(obj.createdAt)); + if (obj.redaction != null) result.put("redaction", obj.redaction.save(ctx)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static TrajectoryEvent fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static TrajectoryEvent fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static TrajectoryEvent fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static TrajectoryEvent fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnCommit.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnCommit.java new file mode 100644 index 000000000..60f4e4f97 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnCommit.java @@ -0,0 +1,125 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class TurnCommit { + public static final String SHORTHAND_PROPERTY = null; + + public String sessionId = ""; + public String turnId = ""; + public EngineTurnStatus status = EngineTurnStatus.SUCCESS; + public Object output = null; + public List messages = new ArrayList<>(); + public Integer iterations = 0; + public Long lastSequence = 0L; + public InvocationContextState contextState = null; + public ModelReconciliationState modelReconciliation = null; + + public TurnCommit() { } + + @SuppressWarnings("unchecked") + public static TurnCommit load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new TurnCommit()); + } + TurnCommit result = new TurnCommit(); + TurnCommit.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(TurnCommit result, Map map, LoadContext ctx) { + if (map.containsKey("sessionId") && map.get("sessionId") != null) { + result.sessionId = String.valueOf(map.get("sessionId")); + } + if (map.containsKey("turnId") && map.get("turnId") != null) { + result.turnId = String.valueOf(map.get("turnId")); + } + if (map.containsKey("status") && map.get("status") != null) { + result.status = EngineTurnStatus.fromValue(String.valueOf(map.get("status"))); + } + if (map.containsKey("output") && map.get("output") != null) { + result.output = map.get("output"); + } + if (map.containsKey("messages") && map.get("messages") != null) { + result.messages = ModelCollections.loadList( + map.get("messages"), "messages", Message.SHORTHAND_PROPERTY, Message::load, ctx); + } + if (map.containsKey("iterations") && map.get("iterations") != null) { + result.iterations = (map.get("iterations") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("iterations")))); + } + if (map.containsKey("lastSequence") && map.get("lastSequence") != null) { + result.lastSequence = (map.get("lastSequence") instanceof Number n ? n.longValue() : Long.parseLong(String.valueOf(map.get("lastSequence")))); + } + if (map.containsKey("contextState") && map.get("contextState") != null) { + result.contextState = InvocationContextState.load(map.get("contextState"), ctx); + } + if (map.containsKey("modelReconciliation") && map.get("modelReconciliation") != null) { + result.modelReconciliation = ModelReconciliationState.load(map.get("modelReconciliation"), ctx); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + TurnCommit obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.sessionId != null) result.put("sessionId", serializeScalar(obj.sessionId)); + if (obj.turnId != null) result.put("turnId", serializeScalar(obj.turnId)); + result.put("status", obj.status.value); + if (obj.output != null) result.put("output", serializeScalar(obj.output)); + if (obj.messages != null) { + List items = new ArrayList<>(); + for (Message item : obj.messages) items.add(item.save(ctx)); + result.put("messages", items); + } + if (obj.iterations != null) result.put("iterations", serializeScalar(obj.iterations)); + if (obj.lastSequence != null) result.put("lastSequence", serializeScalar(obj.lastSequence)); + if (obj.contextState != null) result.put("contextState", obj.contextState.save(ctx)); + if (obj.modelReconciliation != null) result.put("modelReconciliation", obj.modelReconciliation.save(ctx)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static TurnCommit fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static TurnCommit fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static TurnCommit fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static TurnCommit fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnEndPayload.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnEndPayload.java new file mode 100644 index 000000000..3db654ed4 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnEndPayload.java @@ -0,0 +1,95 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class TurnEndPayload { + public static final String SHORTHAND_PROPERTY = null; + + public Integer iterations = null; + public TurnStatus status = null; + public Object response = null; + public Double durationMs = null; + + public TurnEndPayload() { } + + @SuppressWarnings("unchecked") + public static TurnEndPayload load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new TurnEndPayload()); + } + TurnEndPayload result = new TurnEndPayload(); + TurnEndPayload.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(TurnEndPayload result, Map map, LoadContext ctx) { + if (map.containsKey("iterations") && map.get("iterations") != null) { + result.iterations = (map.get("iterations") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("iterations")))); + } + if (map.containsKey("status") && map.get("status") != null) { + result.status = TurnStatus.fromValue(String.valueOf(map.get("status"))); + } + if (map.containsKey("response") && map.get("response") != null) { + result.response = map.get("response"); + } + if (map.containsKey("durationMs") && map.get("durationMs") != null) { + result.durationMs = (map.get("durationMs") instanceof Number n ? n.doubleValue() : Double.parseDouble(String.valueOf(map.get("durationMs")))); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + TurnEndPayload obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.iterations != null) result.put("iterations", serializeScalar(obj.iterations)); + if (obj.status != null) result.put("status", obj.status.value); + if (obj.response != null) result.put("response", serializeScalar(obj.response)); + if (obj.durationMs != null) result.put("durationMs", serializeScalar(obj.durationMs)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static TurnEndPayload fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static TurnEndPayload fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static TurnEndPayload fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static TurnEndPayload fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnEngineResult.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnEngineResult.java new file mode 100644 index 000000000..cffd52bcd --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnEngineResult.java @@ -0,0 +1,104 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class TurnEngineResult { + public static final String SHORTHAND_PROPERTY = null; + + public TurnCommit commit = null; + public List snapshots = null; + public List toolResults = null; + public String postCommitError = null; + + public TurnEngineResult() { } + + @SuppressWarnings("unchecked") + public static TurnEngineResult load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new TurnEngineResult()); + } + TurnEngineResult result = new TurnEngineResult(); + TurnEngineResult.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(TurnEngineResult result, Map map, LoadContext ctx) { + if (map.containsKey("commit") && map.get("commit") != null) { + result.commit = TurnCommit.load(map.get("commit"), ctx); + } + if (map.containsKey("snapshots") && map.get("snapshots") != null) { + result.snapshots = ModelCollections.loadList( + map.get("snapshots"), "snapshots", ModelInvocationContextSnapshot.SHORTHAND_PROPERTY, ModelInvocationContextSnapshot::load, ctx); + } + if (map.containsKey("toolResults") && map.get("toolResults") != null) { + result.toolResults = ModelCollections.loadList( + map.get("toolResults"), "toolResults", ModelToolResult.SHORTHAND_PROPERTY, ModelToolResult::load, ctx); + } + if (map.containsKey("postCommitError") && map.get("postCommitError") != null) { + result.postCommitError = String.valueOf(map.get("postCommitError")); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + TurnEngineResult obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.commit != null) result.put("commit", obj.commit.save(ctx)); + if (obj.snapshots != null) { + List items = new ArrayList<>(); + for (ModelInvocationContextSnapshot item : obj.snapshots) items.add(item.save(ctx)); + result.put("snapshots", items); + } + if (obj.toolResults != null) { + result.put("toolResults", ModelCollections.saveList( + obj.toolResults, ModelToolResult.SHORTHAND_PROPERTY, item -> item.save(ctx), ctx)); + } + if (obj.postCommitError != null) result.put("postCommitError", serializeScalar(obj.postCommitError)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static TurnEngineResult fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static TurnEngineResult fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static TurnEngineResult fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static TurnEngineResult fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnEvent.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnEvent.java new file mode 100644 index 000000000..35679b631 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnEvent.java @@ -0,0 +1,117 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class TurnEvent { + public static final String SHORTHAND_PROPERTY = null; + + public String id = ""; + public TurnEventType type = TurnEventType.TURN_START; + public String timestamp = ""; + public String turnId = null; + public Integer iteration = null; + public String parentId = null; + public String spanId = null; + public Map payload = new LinkedHashMap<>(); + + public TurnEvent() { } + + @SuppressWarnings("unchecked") + public static TurnEvent load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new TurnEvent()); + } + TurnEvent result = new TurnEvent(); + TurnEvent.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(TurnEvent result, Map map, LoadContext ctx) { + if (map.containsKey("id") && map.get("id") != null) { + result.id = String.valueOf(map.get("id")); + } + if (map.containsKey("type") && map.get("type") != null) { + result.type = TurnEventType.fromValue(String.valueOf(map.get("type"))); + } + if (map.containsKey("timestamp") && map.get("timestamp") != null) { + result.timestamp = String.valueOf(map.get("timestamp")); + } + if (map.containsKey("turnId") && map.get("turnId") != null) { + result.turnId = String.valueOf(map.get("turnId")); + } + if (map.containsKey("iteration") && map.get("iteration") != null) { + result.iteration = (map.get("iteration") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("iteration")))); + } + if (map.containsKey("parentId") && map.get("parentId") != null) { + result.parentId = String.valueOf(map.get("parentId")); + } + if (map.containsKey("spanId") && map.get("spanId") != null) { + result.spanId = String.valueOf(map.get("spanId")); + } + if (map.containsKey("payload") && map.get("payload") != null) { + if (map.get("payload") instanceof Map dict) { + result.payload = copyMap(dict); + } + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + TurnEvent obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.id != null) result.put("id", serializeScalar(obj.id)); + result.put("type", obj.type.value); + if (obj.timestamp != null) result.put("timestamp", serializeScalar(obj.timestamp)); + if (obj.turnId != null) result.put("turnId", serializeScalar(obj.turnId)); + if (obj.iteration != null) result.put("iteration", serializeScalar(obj.iteration)); + if (obj.parentId != null) result.put("parentId", serializeScalar(obj.parentId)); + if (obj.spanId != null) result.put("spanId", serializeScalar(obj.spanId)); + if (obj.payload != null) result.put("payload", serializeScalar(obj.payload)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static TurnEvent fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static TurnEvent fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static TurnEvent fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static TurnEvent fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnEventType.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnEventType.java new file mode 100644 index 000000000..28a104e91 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnEventType.java @@ -0,0 +1,40 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +public enum TurnEventType { + TURN_START("turn_start"), + TURN_END("turn_end"), + LLM_START("llm_start"), + LLM_COMPLETE("llm_complete"), + RETRY("retry"), + PERMISSION_REQUESTED("permission_requested"), + PERMISSION_COMPLETED("permission_completed"), + TOKEN("token"), + THINKING("thinking"), + TOOL_CALL_START("tool_call_start"), + TOOL_CALL_COMPLETE("tool_call_complete"), + TOOL_EXECUTION_START("tool_execution_start"), + TOOL_EXECUTION_COMPLETE("tool_execution_complete"), + TOOL_RESULT("tool_result"), + HOOK_START("hook_start"), + HOOK_END("hook_end"), + STATUS("status"), + MESSAGES_UPDATED("messages_updated"), + DONE("done"), + ERROR("error"), + CANCELLED("cancelled"), + COMPACTION_START("compaction_start"), + COMPACTION_COMPLETE("compaction_complete"), + COMPACTION_FAILED("compaction_failed"), + ; + + public final String value; + TurnEventType(String value) { this.value = value; } + public static TurnEventType fromValue(String value) { + for (TurnEventType item : values()) { + if (item.value.equals(value)) return item; + } + return valueOf(value.toUpperCase().replace("-", "_")); + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnModelRequest.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnModelRequest.java new file mode 100644 index 000000000..274d59b3f --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnModelRequest.java @@ -0,0 +1,112 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class TurnModelRequest { + public static final String SHORTHAND_PROPERTY = null; + + public String sessionId = ""; + public String turnId = ""; + public Integer iteration = 0; + public Map inputs = null; + public TurnOptions options = null; + public List toolResults = null; + + public TurnModelRequest() { } + + @SuppressWarnings("unchecked") + public static TurnModelRequest load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new TurnModelRequest()); + } + TurnModelRequest result = new TurnModelRequest(); + TurnModelRequest.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(TurnModelRequest result, Map map, LoadContext ctx) { + if (map.containsKey("sessionId") && map.get("sessionId") != null) { + result.sessionId = String.valueOf(map.get("sessionId")); + } + if (map.containsKey("turnId") && map.get("turnId") != null) { + result.turnId = String.valueOf(map.get("turnId")); + } + if (map.containsKey("iteration") && map.get("iteration") != null) { + result.iteration = (map.get("iteration") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("iteration")))); + } + if (map.containsKey("inputs") && map.get("inputs") != null) { + if (map.get("inputs") instanceof Map dict) { + result.inputs = copyMap(dict); + } + } + if (map.containsKey("options") && map.get("options") != null) { + result.options = TurnOptions.load(map.get("options"), ctx); + } + if (map.containsKey("toolResults") && map.get("toolResults") != null) { + result.toolResults = ModelCollections.loadList( + map.get("toolResults"), "toolResults", HostToolResult.SHORTHAND_PROPERTY, HostToolResult::load, ctx); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + TurnModelRequest obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.sessionId != null) result.put("sessionId", serializeScalar(obj.sessionId)); + if (obj.turnId != null) result.put("turnId", serializeScalar(obj.turnId)); + if (obj.iteration != null) result.put("iteration", serializeScalar(obj.iteration)); + if (obj.inputs != null) result.put("inputs", serializeScalar(obj.inputs)); + if (obj.options != null) result.put("options", obj.options.save(ctx)); + if (obj.toolResults != null) { + List items = new ArrayList<>(); + for (HostToolResult item : obj.toolResults) items.add(item.save(ctx)); + result.put("toolResults", items); + } + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static TurnModelRequest fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static TurnModelRequest fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static TurnModelRequest fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static TurnModelRequest fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnModelResponse.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnModelResponse.java new file mode 100644 index 000000000..a52dd0211 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnModelResponse.java @@ -0,0 +1,102 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class TurnModelResponse { + public static final String SHORTHAND_PROPERTY = null; + + public Object output = null; + public InvocationUsage usage = null; + public List toolRequests = null; + public Map checkpointState = null; + + public TurnModelResponse() { } + + @SuppressWarnings("unchecked") + public static TurnModelResponse load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new TurnModelResponse()); + } + TurnModelResponse result = new TurnModelResponse(); + TurnModelResponse.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(TurnModelResponse result, Map map, LoadContext ctx) { + if (map.containsKey("output") && map.get("output") != null) { + result.output = map.get("output"); + } + if (map.containsKey("usage") && map.get("usage") != null) { + result.usage = InvocationUsage.load(map.get("usage"), ctx); + } + if (map.containsKey("toolRequests") && map.get("toolRequests") != null) { + result.toolRequests = ModelCollections.loadList( + map.get("toolRequests"), "toolRequests", HostToolRequest.SHORTHAND_PROPERTY, HostToolRequest::load, ctx); + } + if (map.containsKey("checkpointState") && map.get("checkpointState") != null) { + if (map.get("checkpointState") instanceof Map dict) { + result.checkpointState = copyMap(dict); + } + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + TurnModelResponse obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.output != null) result.put("output", serializeScalar(obj.output)); + if (obj.usage != null) result.put("usage", obj.usage.save(ctx)); + if (obj.toolRequests != null) { + List items = new ArrayList<>(); + for (HostToolRequest item : obj.toolRequests) items.add(item.save(ctx)); + result.put("toolRequests", items); + } + if (obj.checkpointState != null) result.put("checkpointState", serializeScalar(obj.checkpointState)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static TurnModelResponse fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static TurnModelResponse fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static TurnModelResponse fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static TurnModelResponse fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnOptions.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnOptions.java new file mode 100644 index 000000000..0e7f324b2 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnOptions.java @@ -0,0 +1,110 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class TurnOptions { + public static final String SHORTHAND_PROPERTY = null; + + public Integer maxIterations = null; + public Integer maxLlmRetries = null; + public Integer contextBudget = null; + public Boolean parallelToolCalls = null; + public Boolean raw = null; + public Integer turn = null; + public CompactionConfig compaction = null; + + public TurnOptions() { } + + @SuppressWarnings("unchecked") + public static TurnOptions load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new TurnOptions()); + } + TurnOptions result = new TurnOptions(); + TurnOptions.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(TurnOptions result, Map map, LoadContext ctx) { + if (map.containsKey("maxIterations") && map.get("maxIterations") != null) { + result.maxIterations = (map.get("maxIterations") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("maxIterations")))); + } + if (map.containsKey("maxLlmRetries") && map.get("maxLlmRetries") != null) { + result.maxLlmRetries = (map.get("maxLlmRetries") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("maxLlmRetries")))); + } + if (map.containsKey("contextBudget") && map.get("contextBudget") != null) { + result.contextBudget = (map.get("contextBudget") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("contextBudget")))); + } + if (map.containsKey("parallelToolCalls") && map.get("parallelToolCalls") != null) { + result.parallelToolCalls = (map.get("parallelToolCalls") instanceof Boolean b ? b : Boolean.parseBoolean(String.valueOf(map.get("parallelToolCalls")))); + } + if (map.containsKey("raw") && map.get("raw") != null) { + result.raw = (map.get("raw") instanceof Boolean b ? b : Boolean.parseBoolean(String.valueOf(map.get("raw")))); + } + if (map.containsKey("turn") && map.get("turn") != null) { + result.turn = (map.get("turn") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("turn")))); + } + if (map.containsKey("compaction") && map.get("compaction") != null) { + result.compaction = CompactionConfig.load(map.get("compaction"), ctx); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + TurnOptions obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.maxIterations != null) result.put("maxIterations", serializeScalar(obj.maxIterations)); + if (obj.maxLlmRetries != null) result.put("maxLlmRetries", serializeScalar(obj.maxLlmRetries)); + if (obj.contextBudget != null) result.put("contextBudget", serializeScalar(obj.contextBudget)); + if (obj.parallelToolCalls != null) result.put("parallelToolCalls", serializeScalar(obj.parallelToolCalls)); + if (obj.raw != null) result.put("raw", serializeScalar(obj.raw)); + if (obj.turn != null) result.put("turn", serializeScalar(obj.turn)); + if (obj.compaction != null) result.put("compaction", obj.compaction.save(ctx)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static TurnOptions fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static TurnOptions fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static TurnOptions fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static TurnOptions fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnStartPayload.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnStartPayload.java new file mode 100644 index 000000000..a2da211a3 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnStartPayload.java @@ -0,0 +1,92 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class TurnStartPayload { + public static final String SHORTHAND_PROPERTY = null; + + public String agent = null; + public Map inputs = null; + public Integer maxIterations = null; + + public TurnStartPayload() { } + + @SuppressWarnings("unchecked") + public static TurnStartPayload load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new TurnStartPayload()); + } + TurnStartPayload result = new TurnStartPayload(); + TurnStartPayload.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(TurnStartPayload result, Map map, LoadContext ctx) { + if (map.containsKey("agent") && map.get("agent") != null) { + result.agent = String.valueOf(map.get("agent")); + } + if (map.containsKey("inputs") && map.get("inputs") != null) { + if (map.get("inputs") instanceof Map dict) { + result.inputs = copyMap(dict); + } + } + if (map.containsKey("maxIterations") && map.get("maxIterations") != null) { + result.maxIterations = (map.get("maxIterations") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("maxIterations")))); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + TurnStartPayload obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.agent != null) result.put("agent", serializeScalar(obj.agent)); + if (obj.inputs != null) result.put("inputs", serializeScalar(obj.inputs)); + if (obj.maxIterations != null) result.put("maxIterations", serializeScalar(obj.maxIterations)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static TurnStartPayload fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static TurnStartPayload fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static TurnStartPayload fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static TurnStartPayload fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnStatus.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnStatus.java new file mode 100644 index 000000000..589a81058 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnStatus.java @@ -0,0 +1,19 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +public enum TurnStatus { + SUCCESS("success"), + ERROR("error"), + CANCELLED("cancelled"), + ; + + public final String value; + TurnStatus(String value) { this.value = value; } + public static TurnStatus fromValue(String value) { + for (TurnStatus item : values()) { + if (item.value.equals(value)) return item; + } + return valueOf(value.toUpperCase().replace("-", "_")); + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnSummary.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnSummary.java new file mode 100644 index 000000000..0cd24696f --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnSummary.java @@ -0,0 +1,115 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class TurnSummary { + public static final String SHORTHAND_PROPERTY = null; + + public String turnId = ""; + public String status = ""; + public Integer iterations = 0; + public Integer llmCalls = null; + public Integer toolCalls = null; + public Integer retries = null; + public TokenUsage usage = null; + public Double durationMs = null; + + public TurnSummary() { } + + @SuppressWarnings("unchecked") + public static TurnSummary load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new TurnSummary()); + } + TurnSummary result = new TurnSummary(); + TurnSummary.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(TurnSummary result, Map map, LoadContext ctx) { + if (map.containsKey("turnId") && map.get("turnId") != null) { + result.turnId = String.valueOf(map.get("turnId")); + } + if (map.containsKey("status") && map.get("status") != null) { + result.status = String.valueOf(map.get("status")); + } + if (map.containsKey("iterations") && map.get("iterations") != null) { + result.iterations = (map.get("iterations") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("iterations")))); + } + if (map.containsKey("llmCalls") && map.get("llmCalls") != null) { + result.llmCalls = (map.get("llmCalls") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("llmCalls")))); + } + if (map.containsKey("toolCalls") && map.get("toolCalls") != null) { + result.toolCalls = (map.get("toolCalls") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("toolCalls")))); + } + if (map.containsKey("retries") && map.get("retries") != null) { + result.retries = (map.get("retries") instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(map.get("retries")))); + } + if (map.containsKey("usage") && map.get("usage") != null) { + result.usage = TokenUsage.load(map.get("usage"), ctx); + } + if (map.containsKey("durationMs") && map.get("durationMs") != null) { + result.durationMs = (map.get("durationMs") instanceof Number n ? n.doubleValue() : Double.parseDouble(String.valueOf(map.get("durationMs")))); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + TurnSummary obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.turnId != null) result.put("turnId", serializeScalar(obj.turnId)); + if (obj.status != null) result.put("status", serializeScalar(obj.status)); + if (obj.iterations != null) result.put("iterations", serializeScalar(obj.iterations)); + if (obj.llmCalls != null) result.put("llmCalls", serializeScalar(obj.llmCalls)); + if (obj.toolCalls != null) result.put("toolCalls", serializeScalar(obj.toolCalls)); + if (obj.retries != null) result.put("retries", serializeScalar(obj.retries)); + if (obj.usage != null) result.put("usage", obj.usage.save(ctx)); + if (obj.durationMs != null) result.put("durationMs", serializeScalar(obj.durationMs)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static TurnSummary fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static TurnSummary fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static TurnSummary fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static TurnSummary fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnTrace.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnTrace.java new file mode 100644 index 000000000..bfa6b2a04 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnTrace.java @@ -0,0 +1,105 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class TurnTrace { + public static final String SHORTHAND_PROPERTY = null; + + public String version = "1"; + public String runtime = null; + public String promptyVersion = null; + public List events = new ArrayList<>(); + public TurnSummary summary = null; + + public TurnTrace() { } + + @SuppressWarnings("unchecked") + public static TurnTrace load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new TurnTrace()); + } + TurnTrace result = new TurnTrace(); + TurnTrace.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(TurnTrace result, Map map, LoadContext ctx) { + if (map.containsKey("version") && map.get("version") != null) { + result.version = String.valueOf(map.get("version")); + } + if (map.containsKey("runtime") && map.get("runtime") != null) { + result.runtime = String.valueOf(map.get("runtime")); + } + if (map.containsKey("promptyVersion") && map.get("promptyVersion") != null) { + result.promptyVersion = String.valueOf(map.get("promptyVersion")); + } + if (map.containsKey("events") && map.get("events") != null) { + result.events = ModelCollections.loadList( + map.get("events"), "events", TurnEvent.SHORTHAND_PROPERTY, TurnEvent::load, ctx); + } + if (map.containsKey("summary") && map.get("summary") != null) { + result.summary = TurnSummary.load(map.get("summary"), ctx); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + TurnTrace obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.version != null) result.put("version", serializeScalar(obj.version)); + if (obj.runtime != null) result.put("runtime", serializeScalar(obj.runtime)); + if (obj.promptyVersion != null) result.put("promptyVersion", serializeScalar(obj.promptyVersion)); + if (obj.events != null) { + List items = new ArrayList<>(); + for (TurnEvent item : obj.events) items.add(item.save(ctx)); + result.put("events", items); + } + if (obj.summary != null) result.put("summary", obj.summary.save(ctx)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static TurnTrace fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static TurnTrace fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static TurnTrace fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static TurnTrace fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TypraJson.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TypraJson.java new file mode 100644 index 000000000..15c4fdc27 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TypraJson.java @@ -0,0 +1,202 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.Iterator; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public final class TypraJson { + private TypraJson() { } + + public static String stringify(Object value) { + if (value == null) return "null"; + if (value instanceof String s) return "\"" + escape(s) + "\""; + if (value instanceof Number || value instanceof Boolean) return String.valueOf(value); + if (value instanceof Map map) { + StringBuilder builder = new StringBuilder("{"); + Iterator> iterator = map.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + builder.append(stringify(String.valueOf(entry.getKey()))).append(':').append(stringify(entry.getValue())); + if (iterator.hasNext()) builder.append(','); + } + return builder.append('}').toString(); + } + if (value instanceof Iterable iterable) { + StringBuilder builder = new StringBuilder("["); + Iterator iterator = iterable.iterator(); + while (iterator.hasNext()) { + builder.append(stringify(iterator.next())); + if (iterator.hasNext()) builder.append(','); + } + return builder.append(']').toString(); + } + return stringify(String.valueOf(value)); + } + + public static Object parse(String json) { + Parser parser = new Parser(json); + Object value = parser.parseValue(); + parser.skipWhitespace(); + if (!parser.isAtEnd()) { + throw new IllegalArgumentException("Unexpected trailing JSON content at offset " + parser.index); + } + return value; + } + + private static String escape(String value) { + return value.replace("\\", "\\\\").replace("\"", "\\\"").replace("\r", "\\r").replace("\n", "\\n"); + } + + private static final class Parser { + private final String input; + private int index; + + Parser(String input) { + this.input = input == null ? "" : input; + } + + boolean isAtEnd() { + return index >= input.length(); + } + + void skipWhitespace() { + while (!isAtEnd() && Character.isWhitespace(input.charAt(index))) index++; + } + + Object parseValue() { + skipWhitespace(); + if (isAtEnd()) throw error("Unexpected end of JSON input"); + char ch = input.charAt(index); + if (ch == '{') return parseObject(); + if (ch == '[') return parseArray(); + if (ch == '"') return parseString(); + if (ch == 't') return parseLiteral("true", Boolean.TRUE); + if (ch == 'f') return parseLiteral("false", Boolean.FALSE); + if (ch == 'n') return parseLiteral("null", null); + if (ch == '-' || Character.isDigit(ch)) return parseNumber(); + throw error("Unexpected JSON token"); + } + + private Map parseObject() { + expect('{'); + Map result = new LinkedHashMap<>(); + skipWhitespace(); + if (tryConsume('}')) return result; + while (true) { + skipWhitespace(); + String key = parseString(); + skipWhitespace(); + expect(':'); + result.put(key, parseValue()); + skipWhitespace(); + if (tryConsume('}')) return result; + expect(','); + } + } + + private List parseArray() { + expect('['); + List result = new ArrayList<>(); + skipWhitespace(); + if (tryConsume(']')) return result; + while (true) { + result.add(parseValue()); + skipWhitespace(); + if (tryConsume(']')) return result; + expect(','); + } + } + + private String parseString() { + expect('"'); + StringBuilder result = new StringBuilder(); + while (!isAtEnd()) { + char ch = input.charAt(index++); + if (ch == '"') return result.toString(); + if (ch != '\\') { + result.append(ch); + continue; + } + if (isAtEnd()) throw error("Unterminated JSON escape sequence"); + char escaped = input.charAt(index++); + switch (escaped) { + case '"' -> result.append('"'); + case '\\' -> result.append('\\'); + case '/' -> result.append('/'); + case 'b' -> result.append('\b'); + case 'f' -> result.append('\f'); + case 'n' -> result.append('\n'); + case 'r' -> result.append('\r'); + case 't' -> result.append('\t'); + case 'u' -> result.append(parseUnicodeEscape()); + default -> throw error("Unsupported JSON escape sequence"); + } + } + throw error("Unterminated JSON string"); + } + + private char parseUnicodeEscape() { + if (index + 4 > input.length()) throw error("Incomplete unicode escape"); + String hex = input.substring(index, index + 4); + index += 4; + try { + return (char) Integer.parseInt(hex, 16); + } catch (NumberFormatException error) { + throw error("Invalid unicode escape"); + } + } + + private Object parseLiteral(String literal, Object value) { + if (!input.startsWith(literal, index)) throw error("Invalid JSON literal"); + index += literal.length(); + return value; + } + + private Number parseNumber() { + int start = index; + if (input.charAt(index) == '-') index++; + while (!isAtEnd() && Character.isDigit(input.charAt(index))) index++; + boolean floating = false; + if (!isAtEnd() && input.charAt(index) == '.') { + floating = true; + index++; + while (!isAtEnd() && Character.isDigit(input.charAt(index))) index++; + } + if (!isAtEnd() && (input.charAt(index) == 'e' || input.charAt(index) == 'E')) { + floating = true; + index++; + if (!isAtEnd() && (input.charAt(index) == '+' || input.charAt(index) == '-')) index++; + while (!isAtEnd() && Character.isDigit(input.charAt(index))) index++; + } + String text = input.substring(start, index); + try { + if (floating) return Double.parseDouble(text); + long longValue = Long.parseLong(text); + return longValue >= Integer.MIN_VALUE && longValue <= Integer.MAX_VALUE ? (int) longValue : longValue; + } catch (NumberFormatException error) { + throw error("Invalid JSON number"); + } + } + + private boolean tryConsume(char expected) { + if (!isAtEnd() && input.charAt(index) == expected) { + index++; + return true; + } + return false; + } + + private void expect(char expected) { + if (isAtEnd() || input.charAt(index) != expected) throw error("Expected '" + expected + "'"); + index++; + } + + private IllegalArgumentException error(String message) { + return new IllegalArgumentException(message + " at offset " + index); + } + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TypraMaps.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TypraMaps.java new file mode 100644 index 000000000..8384c63bf --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TypraMaps.java @@ -0,0 +1,18 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.LinkedHashMap; +import java.util.Map; + +public final class TypraMaps { + private TypraMaps() { } + + public static Map mapOf(Object... pairs) { + Map result = new LinkedHashMap<>(); + for (int i = 0; i + 1 < pairs.length; i += 2) { + result.put(String.valueOf(pairs[i]), pairs[i + 1]); + } + return result; + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TypraYaml.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TypraYaml.java new file mode 100644 index 000000000..8812418a6 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TypraYaml.java @@ -0,0 +1,341 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public final class TypraYaml { + private TypraYaml() { } + + public static String stringify(Object value) { + StringBuilder builder = new StringBuilder(); + writeValue(builder, value, 0); + return builder.toString(); + } + + public static Object parse(String yaml) { + return new Parser(yaml).parse(); + } + + private static void writeValue(StringBuilder builder, Object value, int indent) { + if (value instanceof Map map) { + writeMap(builder, map, indent); + return; + } + if (value instanceof Iterable list) { + writeList(builder, list, indent); + return; + } + appendIndent(builder, indent); + builder.append(formatScalar(value)).append('\n'); + } + + private static void writeMap(StringBuilder builder, Map map, int indent) { + if (map.isEmpty()) { + appendIndent(builder, indent); + builder.append("{}").append('\n'); + return; + } + for (Map.Entry entry : map.entrySet()) { + appendIndent(builder, indent); + String key = String.valueOf(entry.getKey()); + Object value = entry.getValue(); + if (value instanceof Map nestedMap) { + if (nestedMap.isEmpty()) { + builder.append(formatKey(key)).append(": {}").append('\n'); + } else { + builder.append(formatKey(key)).append(':').append('\n'); + writeMap(builder, nestedMap, indent + 2); + } + } else if (value instanceof Iterable nestedList) { + if (isIterableEmpty(nestedList)) { + builder.append(formatKey(key)).append(": []").append('\n'); + } else { + builder.append(formatKey(key)).append(':').append('\n'); + writeList(builder, nestedList, indent + 2); + } + } else { + builder.append(formatKey(key)).append(": ").append(formatScalar(value)).append('\n'); + } + } + } + + private static void writeList(StringBuilder builder, Iterable values, int indent) { + List snapshot = new ArrayList<>(); + for (Object item : values) snapshot.add(item); + if (snapshot.isEmpty()) { + appendIndent(builder, indent); + builder.append("[]").append('\n'); + return; + } + for (Object item : snapshot) { + if (item instanceof Map mapItem) { + if (mapItem.isEmpty()) { + appendIndent(builder, indent); + builder.append("- {}").append('\n'); + } else { + appendIndent(builder, indent); + builder.append('-').append('\n'); + writeMap(builder, mapItem, indent + 2); + } + } else if (item instanceof Iterable listItem) { + if (isIterableEmpty(listItem)) { + appendIndent(builder, indent); + builder.append("- []").append('\n'); + } else { + appendIndent(builder, indent); + builder.append('-').append('\n'); + writeList(builder, listItem, indent + 2); + } + } else { + appendIndent(builder, indent); + builder.append("- ").append(formatScalar(item)).append('\n'); + } + } + } + + private static boolean isIterableEmpty(Iterable values) { + return !values.iterator().hasNext(); + } + + private static void appendIndent(StringBuilder builder, int indent) { + for (int i = 0; i < indent; i++) builder.append(' '); + } + + private static String formatScalar(Object value) { + if (value == null) return "null"; + if (value instanceof Number || value instanceof Boolean) return String.valueOf(value); + String text = String.valueOf(value); + if (isPlainString(text)) return text; + return "\"" + escape(text) + "\""; + } + + private static String formatKey(String key) { + if (isPlainKey(key)) return key; + return "\"" + escape(key) + "\""; + } + + private static boolean isPlainString(String value) { + if (value == null || value.isEmpty()) return false; + String lowered = value.toLowerCase(); + if ("null".equals(lowered) || "~".equals(value) || "true".equals(lowered) || "false".equals(lowered)) return false; + if (looksNumeric(value)) return false; + for (int i = 0; i < value.length(); i++) { + char ch = value.charAt(i); + boolean alphaNum = (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9'); + if (!alphaNum && ch != '_' && ch != '-' && ch != '.' && ch != '/' ) return false; + } + return true; + } + + private static boolean isPlainKey(String value) { + if (value == null || value.isEmpty()) return false; + for (int i = 0; i < value.length(); i++) { + char ch = value.charAt(i); + boolean alphaNum = (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9'); + if (!alphaNum && ch != '_' && ch != '-' && ch != '.' && ch != '/') return false; + } + return true; + } + + private static boolean looksNumeric(String value) { + if (value == null || value.isEmpty()) return false; + char first = value.charAt(0); + if (!((first >= '0' && first <= '9') || first == '-' || first == '+')) return false; + try { + if (value.contains(".") || value.contains("e") || value.contains("E")) { + Double.parseDouble(value); + } else { + Long.parseLong(value); + } + return true; + } catch (NumberFormatException error) { + return false; + } + } + + private static String escape(String value) { + return value.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t"); + } + + private static final class Parser { + private final List lines = new ArrayList<>(); + private int index; + + Parser(String yaml) { + String input = yaml == null ? "" : yaml; + String[] rawLines = input.replace("\r\n", "\n").replace('\r', '\n').split("\n", -1); + for (String rawLine : rawLines) { + String line = rawLine.stripTrailing(); + if (line.trim().isEmpty()) continue; + int indent = 0; + while (indent < line.length() && line.charAt(indent) == ' ') indent++; + lines.add(new Line(indent, line.substring(indent).trim())); + } + } + + Object parse() { + if (lines.isEmpty()) return new LinkedHashMap(); + String first = lines.get(0).content; + if (first.startsWith("{") || first.startsWith("[")) return TypraJson.parse(first); + if (lines.size() == 1 && !first.startsWith("-")) { + if (first.startsWith("\"") || first.startsWith("'")) return parseScalar(first); + if (!first.contains(": ")) return parseScalar(first); + } + Object value = parseBlock(lines.get(0).indent); + if (index < lines.size()) throw error("Unexpected trailing YAML content"); + return value; + } + + private Object parseBlock(int indent) { + if (index >= lines.size()) return null; + Line line = lines.get(index); + if (line.indent < indent) return null; + if (line.content.startsWith("-")) return parseList(indent); + return parseMap(indent); + } + + private Map parseMap(int indent) { + Map result = new LinkedHashMap<>(); + while (index < lines.size()) { + Line line = lines.get(index); + if (line.indent < indent) break; + if (line.indent > indent) throw error("Unexpected indentation"); + if (line.content.startsWith("-")) throw error("Unexpected list item in map"); + int separator = findMapSeparator(line.content); + if (separator < 0) throw error("Expected ':' in map entry"); + String key = parseKey(line.content.substring(0, separator).trim()); + String remainder = line.content.substring(separator + 1).trim(); + index++; + if (!remainder.isEmpty()) { + result.put(key, parseScalar(remainder)); + continue; + } + if (index >= lines.size() || lines.get(index).indent <= indent) { + result.put(key, null); + continue; + } + result.put(key, parseBlock(lines.get(index).indent)); + } + return result; + } + + private List parseList(int indent) { + List result = new ArrayList<>(); + while (index < lines.size()) { + Line line = lines.get(index); + if (line.indent < indent) break; + if (line.indent > indent) throw error("Unexpected indentation"); + if (!line.content.startsWith("-")) break; + String remainder = line.content.length() == 1 ? "" : line.content.substring(1).trim(); + index++; + if (!remainder.isEmpty()) { + result.add(parseScalar(remainder)); + continue; + } + if (index >= lines.size() || lines.get(index).indent <= indent) { + result.add(null); + continue; + } + result.add(parseBlock(lines.get(index).indent)); + } + return result; + } + + private Object parseScalar(String text) { + if (text == null || text.isEmpty()) return ""; + if ("null".equalsIgnoreCase(text) || "~".equals(text)) return null; + if ("true".equalsIgnoreCase(text)) return Boolean.TRUE; + if ("false".equalsIgnoreCase(text)) return Boolean.FALSE; + if ("{}".equals(text)) return new LinkedHashMap(); + if ("[]".equals(text)) return new ArrayList(); + if (text.startsWith("\"") && text.endsWith("\"") && text.length() >= 2) return unescapeDoubleQuoted(text.substring(1, text.length() - 1)); + if (text.startsWith("'") && text.endsWith("'") && text.length() >= 2) return text.substring(1, text.length() - 1).replace("''", "'"); + if (looksNumeric(text)) { + try { + if (text.contains(".") || text.contains("e") || text.contains("E")) return Double.parseDouble(text); + long longValue = Long.parseLong(text); + return longValue >= Integer.MIN_VALUE && longValue <= Integer.MAX_VALUE ? (int) longValue : longValue; + } catch (NumberFormatException error) { + return text; + } + } + return text; + } + + private String unescapeDoubleQuoted(String value) { + StringBuilder result = new StringBuilder(); + for (int i = 0; i < value.length(); i++) { + char ch = value.charAt(i); + if (ch != '\\' || i + 1 >= value.length()) { + result.append(ch); + continue; + } + char next = value.charAt(++i); + switch (next) { + case 'n' -> result.append('\n'); + case 'r' -> result.append('\r'); + case 't' -> result.append('\t'); + case '\\' -> result.append('\\'); + case '"' -> result.append('"'); + default -> result.append(next); + } + } + return result.toString(); + } + + private String parseKey(String keyText) { + if (keyText.startsWith("\"") && keyText.endsWith("\"") && keyText.length() >= 2) { + return unescapeDoubleQuoted(keyText.substring(1, keyText.length() - 1)); + } + if (keyText.startsWith("'") && keyText.endsWith("'") && keyText.length() >= 2) { + return keyText.substring(1, keyText.length() - 1).replace("''", "'"); + } + return keyText; + } + + private int findMapSeparator(String content) { + boolean inSingle = false; + boolean inDouble = false; + boolean escaped = false; + for (int i = 0; i < content.length(); i++) { + char ch = content.charAt(i); + if (escaped) { + escaped = false; + continue; + } + if (ch == '\\' && inDouble) { + escaped = true; + continue; + } + if (ch == '"' && !inSingle) { + inDouble = !inDouble; + continue; + } + if (ch == '\'' && !inDouble) { + inSingle = !inSingle; + continue; + } + if (ch == ':' && !inSingle && !inDouble) return i; + } + return -1; + } + + private IllegalArgumentException error(String message) { + return new IllegalArgumentException(message + " at line " + (index + 1)); + } + } + + private static final class Line { + private final int indent; + private final String content; + + private Line(int indent, String content) { + this.indent = indent; + this.content = content; + } + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/UnionProperty.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/UnionProperty.java new file mode 100644 index 000000000..8e2699497 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/UnionProperty.java @@ -0,0 +1,98 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class UnionProperty extends Property { + public static final String SHORTHAND_PROPERTY = null; + + public List oneOf = null; + public List anyOf = null; + + public UnionProperty() { + this.kind = "union"; + } + + @SuppressWarnings("unchecked") + public static UnionProperty load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new UnionProperty()); + } + UnionProperty result = new UnionProperty(); + UnionProperty.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(UnionProperty result, Map map, LoadContext ctx) { + Property.loadBaseInto(result, map, ctx); + if (map.containsKey("oneOf") && map.get("oneOf") != null) { + result.oneOf = ModelCollections.loadList( + map.get("oneOf"), "oneOf", Property.SHORTHAND_PROPERTY, Property::load, ctx); + } + if (map.containsKey("anyOf") && map.get("anyOf") != null) { + result.anyOf = ModelCollections.loadList( + map.get("anyOf"), "anyOf", Property.SHORTHAND_PROPERTY, Property::load, ctx); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + UnionProperty obj = ctx.processObject(this); + Map result = super.save(ctx); + if (obj.oneOf != null) { + List items = new ArrayList<>(); + for (Property item : obj.oneOf) items.add(item.save(ctx)); + result.put("oneOf", items); + } + if (obj.anyOf != null) { + List items = new ArrayList<>(); + for (Property item : obj.anyOf) items.add(item.save(ctx)); + result.put("anyOf", items); + } + return result; + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static UnionProperty fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static UnionProperty fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static UnionProperty fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static UnionProperty fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/UsageChunk.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/UsageChunk.java new file mode 100644 index 000000000..72a43cd86 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/UsageChunk.java @@ -0,0 +1,83 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class UsageChunk extends StreamChunk { + public static final String SHORTHAND_PROPERTY = null; + + public InvocationUsage usage = null; + + public UsageChunk() { + this.kind = "usage"; + } + + @SuppressWarnings("unchecked") + public static UsageChunk load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new UsageChunk()); + } + UsageChunk result = new UsageChunk(); + UsageChunk.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(UsageChunk result, Map map, LoadContext ctx) { + StreamChunk.loadBaseInto(result, map, ctx); + if (map.containsKey("usage") && map.get("usage") != null) { + result.usage = InvocationUsage.load(map.get("usage"), ctx); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + UsageChunk obj = ctx.processObject(this); + Map result = super.save(ctx); + if (obj.usage != null) result.put("usage", obj.usage.save(ctx)); + return result; + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static UsageChunk fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static UsageChunk fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static UsageChunk fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static UsageChunk fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ValidationError.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ValidationError.java new file mode 100644 index 000000000..c8ef58b7b --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ValidationError.java @@ -0,0 +1,90 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class ValidationError { + public static final String SHORTHAND_PROPERTY = null; + + public String message = ""; + public String property = ""; + public String constraint = ""; + + public ValidationError() { } + + @SuppressWarnings("unchecked") + public static ValidationError load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new ValidationError()); + } + ValidationError result = new ValidationError(); + ValidationError.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(ValidationError result, Map map, LoadContext ctx) { + if (map.containsKey("message") && map.get("message") != null) { + result.message = String.valueOf(map.get("message")); + } + if (map.containsKey("property") && map.get("property") != null) { + result.property = String.valueOf(map.get("property")); + } + if (map.containsKey("constraint") && map.get("constraint") != null) { + result.constraint = String.valueOf(map.get("constraint")); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + ValidationError obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.message != null) result.put("message", serializeScalar(obj.message)); + if (obj.property != null) result.put("property", serializeScalar(obj.property)); + if (obj.constraint != null) result.put("constraint", serializeScalar(obj.constraint)); + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static ValidationError fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static ValidationError fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static ValidationError fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static ValidationError fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ValidationResult.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ValidationResult.java new file mode 100644 index 000000000..2871d2a9c --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ValidationResult.java @@ -0,0 +1,90 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class ValidationResult { + public static final String SHORTHAND_PROPERTY = null; + + public Boolean valid = false; + public List errors = new ArrayList<>(); + + public ValidationResult() { } + + @SuppressWarnings("unchecked") + public static ValidationResult load(Object input, LoadContext context) { + LoadContext ctx = context == null ? new LoadContext() : context; + Object data = ctx.processInput(input); + if (!(data instanceof Map map)) { + return ctx.processOutput(new ValidationResult()); + } + ValidationResult result = new ValidationResult(); + ValidationResult.loadBaseInto(result, map, ctx); + return ctx.processOutput(result); + } + + static void loadBaseInto(ValidationResult result, Map map, LoadContext ctx) { + if (map.containsKey("valid") && map.get("valid") != null) { + result.valid = (map.get("valid") instanceof Boolean b ? b : Boolean.parseBoolean(String.valueOf(map.get("valid")))); + } + if (map.containsKey("errors") && map.get("errors") != null) { + result.errors = ModelCollections.loadList( + map.get("errors"), "errors", ValidationError.SHORTHAND_PROPERTY, ValidationError::load, ctx); + } + } + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + ValidationResult obj = ctx.processObject(this); + Map result = new LinkedHashMap<>(); + if (obj.valid != null) result.put("valid", serializeScalar(obj.valid)); + if (obj.errors != null) { + List items = new ArrayList<>(); + for (ValidationError item : obj.errors) items.add(item.save(ctx)); + result.put("errors", items); + } + return ctx.processDict(result); + } + + public String toYaml() { + return TypraYaml.stringify(save(new SaveContext())); + } + + public String toJson() { + return TypraJson.stringify(save(new SaveContext())); + } + + public static ValidationResult fromYaml(String yaml) { + return fromYaml(yaml, new LoadContext()); + } + + public static ValidationResult fromYaml(String yaml, LoadContext context) { + return load(TypraYaml.parse(yaml), context); + } + + public static ValidationResult fromJson(String json) { + return fromJson(json, new LoadContext()); + } + + public static ValidationResult fromJson(String json, LoadContext context) { + return load(TypraJson.parse(json), context); + } + + private static Map copyMap(Map source) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + result.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return result; + } + + private static Object serializeScalar(Object value) { + if (value instanceof Enum e) return e.name().toLowerCase().replace("_", "-"); + return value; + } + +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/parsers/PromptyChatParser.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/parsers/PromptyChatParser.java new file mode 100644 index 000000000..f4ac0a4fc --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/parsers/PromptyChatParser.java @@ -0,0 +1,246 @@ +package com.microsoft.prompty.parsers; + +import com.microsoft.prompty.InvokerException; +import com.microsoft.prompty.Messages; +import com.microsoft.prompty.Nonces; +import com.microsoft.prompty.Parser; +import com.microsoft.prompty.model.Message; +import com.microsoft.prompty.model.Prompty; +import com.microsoft.prompty.model.Role; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Splits rendered text into messages at role-marker lines. + * + *

A role marker is a line that, once trimmed, is nothing but {@code system:}, {@code user:} or + * {@code assistant:} — optionally prefixed with {@code #} and optionally carrying an attribute block + * such as {@code user[name="Alice"]:}. Requiring the marker to occupy the whole line is what lets + * ordinary prose like "The user: said hello" stay content. + * + *

Registered under {@code prompty}. + * + *

Strict mode

+ * + *

Role markers are the only structural authority in a rendered prompt, so a template variable + * that happens to contain {@code "user:"} could otherwise forge a turn boundary. Strict mode closes + * that hole: {@link #preRender} stamps every marker that was genuinely present in the template with + * a random nonce, and parsing then rejects any marker that cannot produce it. Injected text cannot + * guess the nonce, so an injected marker is detected rather than obeyed. + */ +public final class PromptyChatParser implements Parser { + + /** + * Matches a role marker line, with an optional attribute block. + * + *

The attribute loop is written to be unambiguous, because Java's regex engine backtracks. A + * value is either a quoted run or a run containing no quote, comma or {@code ]}, so no character + * can be attributed to two different iterations of the loop, and the possessive quantifiers stop + * the engine from ever trying. The obvious formulation — a single {@code "?[^"]*"?} value class — + * lets the value swallow the separator and the closing bracket, which makes an unterminated block + * such as {@code user[a= a= a= …} split in exponentially many ways and hang the matcher. + * + *

The Rust, Python and TypeScript runtimes all carry that formulation. Rust is safe only by + * accident: its {@code regex} crate is a non-backtracking automaton. Python and TypeScript use + * backtracking engines and appear to share this exposure. C# is unaffected — it matches the block + * as a single lazy group instead. + */ + private static final Pattern BOUNDARY = + Pattern.compile( + "^\\s*#?\\s*(system|user|assistant)" + + "(\\[(?:\\w++\\s*+=\\s*+(?:\"[^\"]*+\"|[^\",\\]]*+)\\s*+,?\\s*+)+\\])?" + + "\\s*:\\s*$", + Pattern.CASE_INSENSITIVE); + + private static final Pattern ATTRIBUTE = Pattern.compile("(\\w+)\\s*=\\s*\"?([^\",\\]]*)\"?"); + + /** Metadata key under which {@link #preRender} returns the nonce it stamped. */ + public static final String NONCE_KEY = "nonce"; + + @Override + public java.util.Optional preRender(String template) { + String nonce = Nonces.hex(16); + + String[] lines = template.split("\n", -1); + StringBuilder sanitized = new StringBuilder(); + for (int i = 0; i < lines.length; i++) { + if (i > 0) { + sanitized.append('\n'); + } + Matcher matcher = BOUNDARY.matcher(lines[i].trim()); + if (matcher.matches()) { + // The trailing newline mirrors the other runtimes byte for byte. It produces a blank + // content line, which the content trim then discards. + sanitized + .append(matcher.group(1).toLowerCase(java.util.Locale.ROOT)) + .append("[nonce=\"") + .append(nonce) + .append("\"]:\n"); + } else { + sanitized.append(lines[i]); + } + } + + return java.util.Optional.of(new PreRender(sanitized.toString(), Map.of(NONCE_KEY, nonce))); + } + + @Override + public List parse(Prompty agent, String rendered, Map context) { + String nonce = null; + if (context != null && context.get(NONCE_KEY) instanceof String value) { + nonce = value; + } + return parseChat(rendered, nonce); + } + + /** Parse rendered text without nonce validation. */ + public static List parseChat(String rendered) { + return parseChat(rendered, null); + } + + /** + * Parse rendered text into messages, optionally enforcing an expected nonce. + * + * @param expectedNonce the nonce every role marker must carry, or null to skip validation + * @throws InvokerException with {@link InvokerException.Kind#PARSE} on a nonce mismatch + */ + public static List parseChat(String rendered, String expectedNonce) { + List messages = new ArrayList<>(); + Role currentRole = Role.SYSTEM; + List contentLines = new ArrayList<>(); + Map currentAttributes = new LinkedHashMap<>(); + boolean sawRoleMarker = false; + + for (String line : rendered.split("\n", -1)) { + Matcher matcher = BOUNDARY.matcher(line.trim()); + if (!matcher.matches()) { + contentLines.add(line); + continue; + } + + // A marker closes the preceding message. Content accumulated before the very first marker + // still becomes a message, which is how a prompt with no markers at all parses as system. + if (!contentLines.isEmpty() || sawRoleMarker) { + messages.add( + buildMessage( + currentRole, + joinAndTrim(contentLines), + currentAttributes, + sawRoleMarker ? expectedNonce : null)); + contentLines.clear(); + currentAttributes = new LinkedHashMap<>(); + } + + currentRole = roleOf(matcher.group(1)); + currentAttributes = + matcher.group(2) == null ? new LinkedHashMap<>() : parseAttributes(matcher.group(2)); + sawRoleMarker = true; + } + + if (!contentLines.isEmpty() || sawRoleMarker) { + messages.add( + buildMessage( + currentRole, + joinAndTrim(contentLines), + currentAttributes, + sawRoleMarker ? expectedNonce : null)); + } + + return messages; + } + + private static Message buildMessage( + Role role, String content, Map attributes, String expectedNonce) { + if (expectedNonce != null) { + Object raw = attributes.get(NONCE_KEY); + // Nonces are captured verbatim (see parseAttributes), so this is a plain text comparison. + String actual = raw == null ? "" : String.valueOf(raw); + if (!expectedNonce.equals(actual)) { + throw InvokerException.parse( + "Nonce mismatch — possible prompt injection detected (strict mode is enabled)." + + " A template variable may be injecting role markers."); + } + } + + Message message = Messages.withText(role, content); + for (Map.Entry entry : attributes.entrySet()) { + if (!NONCE_KEY.equals(entry.getKey())) { + message.metadata.put(entry.getKey(), entry.getValue()); + } + } + return message; + } + + /** + * Resolve a captured role name. + * + *

Matching is case-insensitive to agree with the boundary pattern, which already accepts + * {@code User:} as a marker; treating it as a marker but not as a user turn would be incoherent. + */ + private static Role roleOf(String captured) { + try { + return Role.fromValue(captured.toLowerCase(java.util.Locale.ROOT)); + } catch (IllegalArgumentException e) { + return Role.SYSTEM; + } + } + + /** Extract {@code key=value} pairs from an attribute block such as {@code [name="Alice"]}. */ + private static Map parseAttributes(String block) { + Map attributes = new LinkedHashMap<>(); + Matcher matcher = ATTRIBUTE.matcher(block); + while (matcher.find()) { + String key = matcher.group(1); + String value = matcher.group(2).trim(); + // The nonce is an opaque token, not data. Coercing it loses leading zeros on the roughly + // one-in-eighteen-thousand nonce that comes out all digits, which would then fail to match + // the nonce that was stamped and reject a legitimate render as a prompt injection. The Rust + // reference converts the coerced number back to text, which does not restore the lost zero, + // so it still carries this fault; capturing the nonce verbatim avoids it outright. + attributes.put(key, NONCE_KEY.equals(key) ? value : coerce(value)); + } + return attributes; + } + + /** Coerce an attribute value to boolean, integer or double where it parses cleanly. */ + private static Object coerce(String raw) { + if ("true".equalsIgnoreCase(raw)) { + return Boolean.TRUE; + } + if ("false".equalsIgnoreCase(raw)) { + return Boolean.FALSE; + } + try { + return Long.valueOf(raw); + } catch (NumberFormatException ignored) { + // Not an integer; fall through to the floating-point attempt. + } + try { + // Java accepts forms JSON does not — "0x1p3", "1d", "NaN" — so require a plain decimal. + if (raw.matches("[+-]?(\\d+\\.?\\d*|\\.\\d+)([eE][+-]?\\d+)?")) { + return Double.valueOf(raw); + } + } catch (NumberFormatException ignored) { + // Not a number; keep the original text. + } + return raw; + } + + /** Join content lines, dropping leading and trailing newlines but preserving spaces. */ + private static String joinAndTrim(List lines) { + String joined = String.join("\n", lines); + int start = 0; + int end = joined.length(); + while (start < end && joined.charAt(start) == '\n') { + start++; + } + while (end > start && joined.charAt(end - 1) == '\n') { + end--; + } + return joined.substring(start, end); + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/renderers/JinjaRenderer.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/renderers/JinjaRenderer.java new file mode 100644 index 000000000..8e4d815c2 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/renderers/JinjaRenderer.java @@ -0,0 +1,60 @@ +package com.microsoft.prompty.renderers; + +import com.hubspot.jinjava.Jinjava; +import com.hubspot.jinjava.JinjavaConfig; +import com.hubspot.jinjava.interpret.RenderResult; +import com.hubspot.jinjava.interpret.TemplateError; +import com.microsoft.prompty.InvokerException; +import com.microsoft.prompty.Renderer; +import com.microsoft.prompty.model.Prompty; +import java.util.List; +import java.util.Map; + +/** + * Renders Jinja/Nunjucks templates. + * + *

Registered under both {@code nunjucks} — the canonical spec name — and {@code jinja2}, the + * alias long-standing {@code .prompty} files use. + * + *

Two deliberate departures from typical web-template defaults: output is never HTML-escaped, + * because a prompt is text sent to a model rather than markup sent to a browser; and an undefined + * variable renders as the empty string rather than failing, so an optional input can simply be + * omitted. + */ +public final class JinjaRenderer implements Renderer { + + private final Jinjava jinjava; + + public JinjaRenderer() { + this.jinjava = + new Jinjava( + JinjavaConfig.newBuilder() + .withFailOnUnknownTokens(false) + .withNestedInterpretationEnabled(false) + .build()); + } + + @Override + public String render(Prompty agent, String template, Map inputs) { + RenderResult result; + try { + result = jinjava.renderForResult(template, inputs); + } catch (RuntimeException e) { + throw InvokerException.render("Template rendering failed: " + e.getMessage(), e); + } + + List errors = result.getErrors(); + if (errors != null) { + for (TemplateError error : errors) { + // An unknown variable is not an error here: the spec requires it to render as empty. + // Anything else at fatal severity is a genuine template defect and must surface. + if (error.getSeverity() == TemplateError.ErrorType.FATAL + && error.getReason() != TemplateError.ErrorReason.UNKNOWN) { + throw InvokerException.render("Template rendering failed: " + error.getMessage()); + } + } + } + + return result.getOutput(); + } +} diff --git a/runtime/java/prompty/src/main/java/com/microsoft/prompty/renderers/MustacheRenderer.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/renderers/MustacheRenderer.java new file mode 100644 index 000000000..b976782a4 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/renderers/MustacheRenderer.java @@ -0,0 +1,48 @@ +package com.microsoft.prompty.renderers; + +import com.github.mustachejava.DefaultMustacheFactory; +import com.github.mustachejava.Mustache; +import com.github.mustachejava.MustacheFactory; +import com.microsoft.prompty.InvokerException; +import com.microsoft.prompty.Renderer; +import com.microsoft.prompty.model.Prompty; +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.io.Writer; +import java.util.Map; + +/** + * Renders Mustache templates. + * + *

Registered under {@code mustache}. Output is never HTML-escaped: a prompt is text sent to a + * model, and escaping would corrupt any prompt containing angle brackets or quotes. + */ +public final class MustacheRenderer implements Renderer { + + private final MustacheFactory factory = new RawMustacheFactory(); + + @Override + public String render(Prompty agent, String template, Map inputs) { + try { + Mustache compiled = factory.compile(new StringReader(template), "prompty"); + StringWriter writer = new StringWriter(); + compiled.execute(writer, inputs).flush(); + return writer.toString(); + } catch (IOException | RuntimeException e) { + throw InvokerException.render("Template rendering failed: " + e.getMessage(), e); + } + } + + /** A factory whose {@code encode} writes values through unchanged instead of HTML-escaping. */ + private static final class RawMustacheFactory extends DefaultMustacheFactory { + @Override + public void encode(String value, Writer writer) { + try { + writer.write(value); + } catch (IOException e) { + throw new java.io.UncheckedIOException(e); + } + } + } +} diff --git a/runtime/java/prompty/src/main/resources/com/microsoft/prompty/model_capabilities.json b/runtime/java/prompty/src/main/resources/com/microsoft/prompty/model_capabilities.json new file mode 100644 index 000000000..7c7f6db19 --- /dev/null +++ b/runtime/java/prompty/src/main/resources/com/microsoft/prompty/model_capabilities.json @@ -0,0 +1,55 @@ +{ + "description": "Cross-runtime fallback capability data for provider model discovery. Some provider /models endpoints (Anthropic, Foundry) return capability fields directly; others (OpenAI) return only ids. To keep discovery results consistent across providers AND across runtimes, every Prompty runtime embeds THIS file and applies one shared rule: provider-supplied fields always win; entries here only fill fields the provider left empty (fill-only-missing). Model ids are matched by longest prefix within a provider's list. This dataset is deliberately NOT emitted from TypeSpec: it is volatile provider data (context windows, modalities, new model families) refreshed as a snapshot, whereas TypeSpec owns the structural ModelInfo contract. Fields use the canonical camelCase ModelInfo names. A missing 'contextWindow' means unknown; a present empty modality list (e.g. []) is intentional (e.g. embeddings produce no textual/image output modality).", + "match": "longest_prefix", + "providers": { + "openai": [ + { + "prefix": "gpt-4o-mini", + "contextWindow": 128000, + "inputModalities": ["text", "image"], + "outputModalities": ["text"] + }, + { + "prefix": "gpt-4o", + "contextWindow": 128000, + "inputModalities": ["text", "image"], + "outputModalities": ["text"] + }, + { + "prefix": "gpt-4-turbo", + "contextWindow": 128000, + "inputModalities": ["text", "image"], + "outputModalities": ["text"] + }, + { + "prefix": "gpt-4", + "contextWindow": 8192, + "inputModalities": ["text"], + "outputModalities": ["text"] + }, + { + "prefix": "gpt-3.5-turbo", + "contextWindow": 16385, + "inputModalities": ["text"], + "outputModalities": ["text"] + }, + { + "prefix": "text-embedding-3-small", + "contextWindow": 8191, + "inputModalities": ["text"], + "outputModalities": [] + }, + { + "prefix": "text-embedding-3-large", + "contextWindow": 8191, + "inputModalities": ["text"], + "outputModalities": [] + }, + { + "prefix": "dall-e-3", + "inputModalities": ["text"], + "outputModalities": ["image"] + } + ] + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/AgentVectorsTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/AgentVectorsTest.java new file mode 100644 index 000000000..1aafdf49f --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/AgentVectorsTest.java @@ -0,0 +1,1087 @@ +package com.microsoft.prompty; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.microsoft.prompty.model.LoadContext; +import com.microsoft.prompty.model.Prompty; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +/** + * Grades the agent turn against the shared cross-runtime agent vectors. + * + *

Each vector scripts a whole conversation: the responses the model would give, the tool calls + * it would make, and the answer it should arrive at. A mock executor replays the scripted responses + * in order, so what is being graded is the loop's decisions — when to call a tool, what to send + * back, when to stop — rather than any provider's behaviour. + * + *

Following the Rust driver, the vector's messages are turned back into agent instructions so + * they arrive through the ordinary render and parse path. That keeps the test honest about the + * whole pipeline instead of injecting a conversation the runtime never built. + */ +class AgentVectorsTest { + + private static final List> VECTORS = + SpecVectors.readArray("agent/agent_vectors.json"); + + @SuppressWarnings("unchecked") + private static Map asMap(Object value) { + return value instanceof Map map ? (Map) map : Map.of(); + } + + // ------------------------------------------------------------------------- + // Vector lookup and agent construction + // ------------------------------------------------------------------------- + + private static Map vector(String name) { + for (Map vector : VECTORS) { + if (name.equals(vector.get("name"))) { + return vector; + } + } + throw new AssertionError("no agent vector named '" + name + "'"); + } + + private static Map input(Map vector) { + return asMap(vector.get("input")); + } + + private static Map expected(Map vector) { + return asMap(vector.get("expected")); + } + + private static List sequence(Map vector) { + return vector.get("sequence") instanceof List steps ? List.copyOf(steps) : List.of(); + } + + private static String expectedResult(Map vector) { + return (String) expected(vector).get("result"); + } + + /** A unique registry key per vector, so concurrently running tests cannot collide. */ + private static String mockKey(String name) { + return "specmock_" + name; + } + + /** + * Build the agent a vector describes. + * + *

The vector's messages become the agent's instructions in role-marker form, so {@code + * prepare} regenerates them through the real renderer and parser. + */ + private static Prompty buildAgent(Map vector, String providerKey) { + Map data = new LinkedHashMap<>(); + data.put("name", "agent_test_" + vector.get("name")); + data.put("kind", "prompt"); + data.put("model", Map.of("id", "gpt-4", "provider", providerKey)); + data.put("instructions", instructions(vector)); + if (input(vector).get("tools") != null) { + data.put("tools", input(vector).get("tools")); + } + data.put( + "template", + Map.of("format", Map.of("kind", "nunjucks"), "parser", Map.of("kind", "prompty"))); + return Prompty.load(data, new LoadContext()); + } + + private static String instructions(Map vector) { + List blocks = new ArrayList<>(); + if (input(vector).get("messages") instanceof List messages) { + for (Object entry : messages) { + Map message = asMap(entry); + String role = message.get("role") instanceof String text ? text : "user"; + String content = message.get("content") instanceof String text ? text : ""; + blocks.add(role + ":\n" + content); + } + } + return String.join("\n\n", blocks); + } + + // ------------------------------------------------------------------------- + // Mock provider + // ------------------------------------------------------------------------- + + /** Replays the vector's scripted model responses, one per invocation. */ + private static final class MockExecutor implements Executor { + private final List responses; + private final AtomicInteger next = new AtomicInteger(); + + /** + * The conversation handed to the model on each call. + * + *

Recorded because several vectors describe work that only shows up in what the model is + * asked — trimming, steering — and is invisible in the answer a scripted response gives back. + */ + private final List> received = + Collections.synchronizedList(new ArrayList<>()); + + MockExecutor(List responses) { + this.responses = responses; + } + + @Override + public Object execute(Prompty agent, List messages) { + received.add(List.copyOf(messages)); + int index = next.getAndIncrement(); + if (index >= responses.size()) { + throw InvokerException.execute( + "MockExecutor: no more responses (requested index " + index + ")"); + } + return responses.get(index); + } + + List lastCall() { + assertFalse(received.isEmpty(), "the model was never called"); + return received.get(received.size() - 1); + } + } + + /** + * Reads a scripted OpenAI-shaped response the way the real processor would. + * + *

Returns the established {@code {id, name, arguments}} list for a tool-call round and the + * assistant text otherwise, which is the contract {@link Processor#processWithContext} builds on. + */ + private static final class MockProcessor implements Processor { + @Override + public Object process(Prompty agent, Object response) { + Map message = firstMessage(response); + if (message.get("tool_calls") instanceof List calls && !calls.isEmpty()) { + List toolCalls = new ArrayList<>(); + for (Object entry : calls) { + Map call = asMap(entry); + Map function = asMap(call.get("function")); + toolCalls.add( + Map.of( + "id", String.valueOf(call.get("id")), + "name", String.valueOf(function.get("name")), + "arguments", + function.get("arguments") == null ? "{}" : function.get("arguments"))); + } + return toolCalls; + } + Object content = message.get("content"); + return content instanceof String text ? text : ""; + } + + private static Map firstMessage(Object response) { + Map body = asMap(response); + if (body.get("choices") instanceof List choices && !choices.isEmpty()) { + return asMap(asMap(choices.get(0)).get("message")); + } + return Map.of(); + } + } + + private static MockExecutor registerMocks(String name, List responses) { + String key = mockKey(name); + MockExecutor executor = new MockExecutor(responses); + Registry.registerExecutor(key, executor); + Registry.registerProcessor(key, new MockProcessor()); + return executor; + } + + private static List responses(Map vector) { + List responses = new ArrayList<>(); + for (Object step : sequence(vector)) { + responses.add(asMap(step).get("llm_response")); + } + return responses; + } + + // ------------------------------------------------------------------------- + // Tool handlers + // ------------------------------------------------------------------------- + + /** + * Build handlers that hand back the vector's canned results in order. + * + *

Results are keyed by tool name rather than call id because a vector may call the same tool + * more than once; each name gets its own queue so repeated calls stay distinguishable. + */ + private static Map toolHandlers(Map vector) { + Map> queues = new LinkedHashMap<>(); + + for (Object step : sequence(vector)) { + Map current = asMap(step); + if (!(current.get("tool_results") instanceof List results)) { + continue; + } + List calls = + current.get("expected_tool_calls") instanceof List expected ? expected : List.of(); + for (Object entry : results) { + Map result = asMap(entry); + String callId = String.valueOf(result.get("tool_call_id")); + String value = result.get("result") instanceof String text ? text : ""; + String name = "unknown"; + for (Object call : calls) { + Map expectedCall = asMap(call); + if (callId.equals(String.valueOf(expectedCall.get("id")))) { + name = String.valueOf(expectedCall.get("name")); + break; + } + } + queues.computeIfAbsent(name, unused -> new ArrayList<>()).add(value); + } + } + + // A tool the vector declares but never scripts a result for still needs a handler, or the + // dispatcher would report it missing and change what the vector is testing. + if (input(vector).get("tool_functions") instanceof Map functions) { + for (Object name : functions.keySet()) { + queues.computeIfAbsent(String.valueOf(name), unused -> new ArrayList<>()); + } + } + + Map handlers = new LinkedHashMap<>(); + queues.forEach( + (name, queue) -> { + AtomicInteger index = new AtomicInteger(); + handlers.put( + name, + arguments -> { + int i = index.getAndIncrement(); + return i < queue.size() ? queue.get(i) : "(mock result #" + i + " for " + name + ")"; + }); + }); + return handlers; + } + + // ------------------------------------------------------------------------- + // Turn options assembled from a vector's extension configuration + // ------------------------------------------------------------------------- + + /** Build the guardrails, steering and budget a vector configures. */ + private static TurnOptions.Builder extensionOptions( + Map vector, Map tools) { + Map in = input(vector); + TurnOptions.Builder builder = TurnOptions.builder().tools(tools); + + if (in.get("context_budget") instanceof Number budget) { + builder.contextBudget(budget.intValue()); + } + if (Boolean.TRUE.equals(in.get("parallel_tool_calls"))) { + builder.parallelToolCalls(true); + } + + if (in.get("guardrails") instanceof Map configured) { + Map config = asMap(configured); + Guardrails guardrails = Guardrails.none(); + + if (config.get("input") instanceof Map rule) { + Map input = asMap(rule); + guardrails = guardrails.withInput((messages, agent) -> decide(input)); + } + if (config.get("output") instanceof Map rule) { + Map output = asMap(rule); + guardrails = guardrails.withOutput((result, agent) -> decide(output)); + } + if (config.get("tool") instanceof Map rule) { + Map tool = asMap(rule); + List denied = + tool.get("deny_tools") instanceof List names ? names : Collections.emptyList(); + String reason = tool.get("reason") instanceof String text ? text : "Tool denied"; + guardrails = + guardrails.withTool( + (name, arguments, agent) -> + denied.contains(name) ? GuardrailResult.deny(reason) : GuardrailResult.allow()); + } + builder.guardrails(guardrails); + } + + if (in.get("steering") instanceof Map configured) { + Steering steering = new Steering(); + Map config = asMap(configured); + if (config.get("messages") instanceof List messages) { + // Every steering message in these vectors targets iteration 2, and the queue drains at the + // start of each iteration, so pre-loading delivers them exactly where the vector expects. + for (Object entry : messages) { + Map message = asMap(entry); + steering.send(message.get("text") instanceof String text ? text : ""); + } + } + builder.steering(steering); + } + + return builder; + } + + private static GuardrailResult decide(Map rule) { + if ("deny".equals(rule.get("action"))) { + return GuardrailResult.deny(rule.get("reason") instanceof String text ? text : ""); + } + return GuardrailResult.allow(); + } + + // ------------------------------------------------------------------------- + // Runners + // ------------------------------------------------------------------------- + + /** What a turn produced, together with the conversations the model was actually handed. */ + private record Run(Object result, MockExecutor executor) {} + + private static Object run(String name) { + return runVector(name, null, null).result(); + } + + private static Object run( + String name, Map toolOverride, List eventLog) { + return runVector(name, toolOverride, eventLog).result(); + } + + private static Run runVector( + String name, Map toolOverride, List eventLog) { + Map vector = vector(name); + MockExecutor executor = registerMocks(name, responses(vector)); + Map tools = toolOverride != null ? toolOverride : toolHandlers(vector); + TurnOptions.Builder builder = extensionOptions(vector, tools); + if (eventLog != null) { + builder.onEvent(event -> eventLog.add(event.type())); + } + Object result = Pipeline.turn(buildAgent(vector, mockKey(name)), Map.of(), builder.build()); + return new Run(result, executor); + } + + private static String runForText(String name) { + Object result = run(name); + return assertInstanceOf(String.class, result); + } + + // ------------------------------------------------------------------------- + // Conversation inspection helpers + // ------------------------------------------------------------------------- + + private static List roles(List messages) { + List result = new ArrayList<>(); + for (com.microsoft.prompty.model.Message message : messages) { + result.add(message.role == null ? "" : message.role.name().toLowerCase(Locale.ROOT)); + } + return result; + } + + private static List texts(List messages) { + List result = new ArrayList<>(); + for (com.microsoft.prompty.model.Message message : messages) { + result.add(Messages.text(message)); + } + return result; + } + + /** The system messages a vector declares, in order. */ + private static List declaredSystemTexts(Map vector) { + List result = new ArrayList<>(); + if (input(vector).get("messages") instanceof List messages) { + for (Object entry : messages) { + Map message = asMap(entry); + if ("system".equals(message.get("role"))) { + result.add(String.valueOf(message.get("content"))); + } + } + } + return result; + } + + private static boolean hasContextSummary(List messages) { + return texts(messages).stream().anyMatch(text -> text.startsWith("[Context summary:")); + } + + // ========================================================================= + // Basic agent loop + // ========================================================================= + + @Nested + @DisplayName("basic agent loop") + class BasicLoop { + + @Test + @DisplayName("a response with no tool calls completes in one iteration") + void noToolCalls() { + assertEquals(expectedResult(vector("no_tool_calls")), runForText("no_tool_calls")); + } + + @Test + @DisplayName("a single tool call is executed and its result fed back") + void singleToolCall() { + assertEquals(expectedResult(vector("single_tool_call")), runForText("single_tool_call")); + } + + @Test + @DisplayName("several tool calls in one response all execute before the next model call") + void multipleToolCallsSingleTurn() { + assertEquals( + expectedResult(vector("multiple_tool_calls_single_turn")), + runForText("multiple_tool_calls_single_turn")); + } + + @Test + @DisplayName("tool calls spread across turns keep the conversation coherent") + void multiTurnToolCalls() { + assertEquals( + expectedResult(vector("multi_turn_tool_calls")), runForText("multi_turn_tool_calls")); + } + + @Test + @DisplayName("tool results are formatted as the provider expects") + void toolResultMessageFormat() { + assertEquals( + expectedResult(vector("tool_result_message_format")), + runForText("tool_result_message_format")); + } + + @Test + @DisplayName("the assistant message records the tool calls it made") + void assistantToolCallsMetadata() { + assertEquals( + expectedResult(vector("assistant_tool_calls_metadata")), + runForText("assistant_tool_calls_metadata")); + } + + @Test + @DisplayName("an empty tool result is still sent back rather than dropped") + void emptyToolResult() { + assertEquals(expectedResult(vector("empty_tool_result")), runForText("empty_tool_result")); + } + + @Test + @DisplayName("a handler that blocks is awaited before the loop continues") + void asyncToolFunction() { + // Java tool handlers are synchronous, so the vector's async handler is modelled as one that + // blocks. What the vector actually pins down is that the loop waits for the result. + Map tools = + Map.of( + "lookup", + arguments -> { + try { + Thread.sleep(5); + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + } + return "found: test data"; + }); + assertEquals("I found: test data", run("async_tool_function", tools, null)); + } + } + + // ========================================================================= + // Errors and limits + // ========================================================================= + + @Nested + @DisplayName("errors and limits") + class Errors { + + @Test + @DisplayName("an unregistered tool reports back to the model instead of failing the turn") + void toolNotRegistered() { + Map vector = vector("tool_not_registered_error"); + List responses = new ArrayList<>(responses(vector)); + // The vector stops at the failed call; the loop needs one more response to settle on. + responses.add( + Map.of( + "choices", + List.of( + Map.of( + "index", + 0, + "message", + Map.of("role", "assistant", "content", "I could not find that tool."), + "finish_reason", + "stop")))); + registerMocks("tool_not_registered_error", responses); + String key = mockKey("tool_not_registered_error"); + + Object result = + Pipeline.turn( + buildAgent(vector, key), + Map.of(), + TurnOptions.builder().tools(Map.of("get_weather", arguments -> "72\u00b0F")).build()); + + assertInstanceOf(String.class, result); + } + + @Test + @DisplayName("a loop that never settles fails once it hits the iteration limit") + void maxIterationsExceeded() { + Map vector = vector("max_iterations_exceeded"); + registerMocks("max_iterations_exceeded", responses(vector)); + String key = mockKey("max_iterations_exceeded"); + + InvokerException failure = + assertThrows( + InvokerException.class, + () -> + Pipeline.turn( + buildAgent(vector, key), + Map.of(), + TurnOptions.builder() + .tools(toolHandlers(vector)) + .maxIterations(10) + .build())); + + String message = failure.getMessage().toLowerCase(Locale.ROOT); + assertTrue( + message.contains("max iterations") || message.contains("exceeded"), + "expected an iteration-limit message, got: " + failure.getMessage()); + } + } + + // ========================================================================= + // Events + // ========================================================================= + + @Nested + @DisplayName("events") + class Events { + + @Test + @DisplayName("a tool loop reports each call and finishes with done before turn_end") + void basicToolLoop() { + List events = new ArrayList<>(); + Object result = run("events_basic_tool_loop", null, events); + + assertEquals(expectedResult(vector("events_basic_tool_loop")), result); + assertTrue(events.contains("tool_call_start"), "missing tool_call_start in " + events); + assertTrue(events.contains("tool_result"), "missing tool_result in " + events); + + int done = events.indexOf("done"); + int turnEnd = events.indexOf("turn_end"); + assertTrue(done >= 0, "missing done in " + events); + assertTrue(turnEnd >= 0, "missing turn_end in " + events); + // A caller that stops listening at turn_end must still have seen the answer. + assertTrue(done < turnEnd, "done should precede turn_end, got " + events); + } + + @Test + @DisplayName("a turn with no tool calls reports no tool events") + void noTools() { + List events = new ArrayList<>(); + Object result = run("events_no_tools", null, events); + + assertEquals("2 + 2 equals 4.", result); + assertTrue(events.contains("done"), "missing done in " + events); + assertFalse(events.contains("tool_call_start"), "unexpected tool_call_start in " + events); + } + + @Test + @DisplayName("a failing tool is still reported as started") + void errorLogged() { + List events = new ArrayList<>(); + Map tools = + Map.of( + "get_weather", + arguments -> { + throw new IllegalStateException("Weather service unavailable"); + }); + + Object result = run("events_error_logged", tools, events); + + assertInstanceOf(String.class, result); + assertTrue(events.contains("tool_call_start"), "missing tool_call_start in " + events); + assertTrue(events.contains("tool_call_complete"), "missing tool_call_complete in " + events); + } + + @Test + @DisplayName("turn_start precedes every other event") + void turnStartIsFirst() { + List events = new ArrayList<>(); + run("events_basic_tool_loop", null, events); + assertEquals("turn_start", events.get(0), "turn_start should lead, got " + events); + } + + @Test + @DisplayName("each model call is bracketed by llm_start and llm_complete") + void modelCallsAreBracketed() { + List events = new ArrayList<>(); + run("events_basic_tool_loop", null, events); + + long starts = events.stream().filter("llm_start"::equals).count(); + long completes = events.stream().filter("llm_complete"::equals).count(); + assertEquals(starts, completes, "unbalanced llm events in " + events); + assertTrue(starts > 0, "expected at least one model call in " + events); + assertTrue( + events.indexOf("llm_start") < events.indexOf("llm_complete"), + "llm_start should precede llm_complete, got " + events); + } + } + + // ========================================================================= + // Cancellation + // ========================================================================= + + @Nested + @DisplayName("cancellation") + class Cancellation { + + @Test + @DisplayName("a turn cancelled before it starts never calls the model") + void beforeLlm() { + Map vector = vector("cancellation_before_llm"); + MockExecutor executor = registerMocks("cancellation_before_llm", responses(vector)); + String key = mockKey("cancellation_before_llm"); + + List events = new ArrayList<>(); + CancellationToken cancellation = CancellationToken.create(); + cancellation.cancel(); + + InvokerException failure = + assertThrows( + InvokerException.class, + () -> + Pipeline.turn( + buildAgent(vector, key), + Map.of(), + TurnOptions.builder() + .tools(toolHandlers(vector)) + .cancellation(cancellation) + .onEvent(event -> events.add(event.type())) + .build())); + + assertTrue( + failure.getMessage().toLowerCase(Locale.ROOT).contains("cancel"), + "expected a cancellation message, got: " + failure.getMessage()); + assertTrue(events.contains("cancelled"), "missing cancelled event in " + events); + // The point of cancelling up front is that no request is ever sent. + assertTrue( + executor.received.isEmpty(), + "the model should never have been called, but was " + executor.received.size() + " time(s)"); + } + + @Test + @DisplayName("cancelling during a tool round stops before the next model call") + void betweenIterations() { + Map vector = vector("cancellation_between_iterations"); + registerMocks("cancellation_between_iterations", responses(vector)); + String key = mockKey("cancellation_between_iterations"); + + List events = new ArrayList<>(); + CancellationToken cancellation = CancellationToken.create(); + AtomicInteger calls = new AtomicInteger(); + + InvokerException failure = + assertThrows( + InvokerException.class, + () -> + Pipeline.turn( + buildAgent(vector, key), + Map.of(), + TurnOptions.builder() + .tools( + Map.of( + "get_weather", + arguments -> { + if (calls.getAndIncrement() == 0) { + cancellation.cancel(); + } + return "72\u00b0F sunny"; + })) + .cancellation(cancellation) + .onEvent(event -> events.add(event.type())) + .build())); + + assertTrue( + failure.getMessage().toLowerCase(Locale.ROOT).contains("cancel"), + "expected a cancellation message, got: " + failure.getMessage()); + assertTrue(events.contains("cancelled"), "missing cancelled event in " + events); + } + + @Test + @DisplayName("cancelling mid-round stops the remaining tool calls") + void betweenTools() { + Map vector = vector("cancellation_between_tools"); + registerMocks("cancellation_between_tools", responses(vector)); + String key = mockKey("cancellation_between_tools"); + + CancellationToken cancellation = CancellationToken.create(); + AtomicInteger calls = new AtomicInteger(); + + InvokerException failure = + assertThrows( + InvokerException.class, + () -> + Pipeline.turn( + buildAgent(vector, key), + Map.of(), + TurnOptions.builder() + .tools( + Map.of( + "get_weather", + arguments -> { + if (calls.getAndIncrement() == 0) { + cancellation.cancel(); + } + return "72\u00b0F sunny"; + })) + .cancellation(cancellation) + .build())); + + assertTrue( + failure.getMessage().toLowerCase(Locale.ROOT).contains("cancel"), + "expected a cancellation message, got: " + failure.getMessage()); + assertEquals(1, calls.get(), "only the first tool call should have run"); + } + } + + // ========================================================================= + // Bindings + // ========================================================================= + + @Nested + @DisplayName("bindings") + class Bindings { + + @Test + @DisplayName("bound inputs are injected into the arguments the model supplied") + void injected() { + Map vector = vector("bindings_injected"); + registerMocks("bindings_injected", responses(vector)); + String key = mockKey("bindings_injected"); + + AtomicReference captured = new AtomicReference<>(); + Object result = + Pipeline.turn( + buildAgent(vector, key), + asMap(input(vector).get("parent_inputs")), + TurnOptions.builder() + .tools( + Map.of( + "get_weather", + arguments -> { + captured.set(arguments); + return "22\u00b0C sunny"; + })) + .build()); + + assertEquals(expectedResult(vector), result); + + Object expectedArgs = + asMap(asMap(sequence(vector).get(0)).get("expected_execution_args")) + .get("get_weather"); + assertNotNull(captured.get(), "the tool handler was never called"); + SpecVectors.assertEquivalent("bindings_injected arguments", expectedArgs, captured.get()); + } + } + + // ========================================================================= + // Context trimming + // ========================================================================= + + @Nested + @DisplayName("context trimming") + class ContextTrimming { + + @Test + @DisplayName("a conversation over budget is trimmed down and summarised") + void trimBasic() { + Map vector = vector("context_trim_basic"); + Run run = runVector("context_trim_basic", null, null); + + assertEquals(expectedResult(vector), run.result()); + + // The vector declares ten messages against a 500-character budget, so the model must be + // handed fewer than it started with, with the dropped span replaced by a summary. + List sent = run.executor().received.get(0); + int declared = ((List) input(vector).get("messages")).size(); + assertTrue( + sent.size() < declared, + "expected trimming below " + declared + " messages, got " + roles(sent)); + assertTrue(hasContextSummary(sent), "expected a context summary in " + texts(sent)); + + // The question actually being answered has to survive, or the answer means nothing. + assertEquals( + "Finally, what is the weather in Paris today?", + texts(sent).get(sent.size() - 1), + "the most recent user message should be kept"); + } + + @Test + @DisplayName("a conversation within budget is handed over untouched") + void noTrimWhenFits() { + Map vector = vector("context_no_trim_when_fits"); + Run run = runVector("context_no_trim_when_fits", null, null); + + assertEquals(expectedResult(vector), run.result()); + + // The vector records trimmed_messages as null: nothing should have been dropped or added. + List sent = run.executor().received.get(0); + int declared = ((List) input(vector).get("messages")).size(); + assertEquals(declared, sent.size(), "expected no trimming, got " + roles(sent)); + assertFalse(hasContextSummary(sent), "expected no summary in " + texts(sent)); + } + + @Test + @DisplayName("every system message survives trimming") + void preservesSystemMessages() { + Map vector = vector("context_preserves_system_messages"); + Run run = runVector("context_preserves_system_messages", null, null); + + assertEquals(expectedResult(vector), run.result()); + + List sent = run.executor().received.get(0); + int declared = ((List) input(vector).get("messages")).size(); + assertTrue(sent.size() < declared, "expected trimming, got " + roles(sent)); + + // System messages carry the instructions the whole turn depends on, so a budget squeeze + // must never be paid for out of them. + List texts = texts(sent); + for (String system : declaredSystemTexts(vector)) { + assertTrue(texts.contains(system), "system message dropped: '" + system + "' from " + texts); + } + } + } + + // ========================================================================= + // Guardrails + // ========================================================================= + + @Nested + @DisplayName("guardrails") + class GuardrailVectors { + + @Test + @DisplayName("a denied input fails the turn before any model call") + void inputDeny() { + InvokerException failure = + assertThrows(InvokerException.class, () -> run("guardrail_input_deny")); + String message = failure.getMessage(); + assertTrue( + message.contains("guardrail") || message.contains("Guardrail") || message.contains("denied"), + "expected a guardrail message, got: " + message); + assertTrue(message.contains("PII"), "expected the denial reason, got: " + message); + } + + @Test + @DisplayName("a denied output fails the turn after the model responded") + void outputDeny() { + InvokerException failure = + assertThrows(InvokerException.class, () -> run("guardrail_output_deny")); + String message = failure.getMessage(); + assertTrue( + message.contains("guardrail") || message.contains("Guardrail") || message.contains("denied"), + "expected a guardrail message, got: " + message); + assertTrue(message.contains("harmful"), "expected the denial reason, got: " + message); + } + + @Test + @DisplayName("a denied tool reports back to the model while the others still run") + void toolDeny() { + assertEquals(expectedResult(vector("guardrail_tool_deny")), runForText("guardrail_tool_deny")); + } + + @Test + @DisplayName("guardrails that all pass leave the turn unchanged") + void allPass() { + assertEquals(expectedResult(vector("guardrail_all_pass")), runForText("guardrail_all_pass")); + } + + @Test + @DisplayName("a denied tool is never actually executed") + void deniedToolDoesNotRun() { + Map vector = vector("guardrail_tool_deny"); + registerMocks("guardrail_tool_deny", responses(vector)); + String key = mockKey("guardrail_tool_deny"); + + AtomicInteger dangerous = new AtomicInteger(); + Map tools = new LinkedHashMap<>(toolHandlers(vector)); + tools.put( + "dangerous_tool", + arguments -> { + dangerous.incrementAndGet(); + return "executed"; + }); + + Pipeline.turn( + buildAgent(vector, key), Map.of(), extensionOptions(vector, tools).build()); + + assertEquals(0, dangerous.get(), "the denied tool should never have run"); + } + } + + // ========================================================================= + // Steering + // ========================================================================= + + @Nested + @DisplayName("steering") + class SteeringVectors { + + /** The steering texts a vector queues, in the order it queues them. */ + private List steeringTexts(Map vector) { + List result = new ArrayList<>(); + Map steering = asMap(input(vector).get("steering")); + if (steering.get("messages") instanceof List messages) { + for (Object entry : messages) { + result.add(String.valueOf(asMap(entry).get("text"))); + } + } + return result; + } + + @Test + @DisplayName("a queued message reaches the model on the next iteration") + void injectMessage() { + Map vector = vector("steering_inject_message"); + Run run = runVector("steering_inject_message", null, null); + + assertEquals(expectedResult(vector), run.result()); + assertSteeringDelivered(vector, run); + } + + @Test + @DisplayName("several queued messages are all delivered, in order") + void multipleMessages() { + Map vector = vector("steering_multiple_messages"); + Run run = runVector("steering_multiple_messages", null, null); + + assertEquals(expectedResult(vector), run.result()); + assertSteeringDelivered(vector, run); + } + + /** + * Check the steering actually reached the model rather than merely being accepted. + * + *

The vector annotates each message with the iteration it targets, but no runtime models + * that: {@code Steering} is a plain queue that the policy drains whenever it next runs. So + * what is checked here is delivery and ordering, which is the contract that exists, rather + * than the iteration boundary, which is not. + */ + private void assertSteeringDelivered(Map vector, Run run) { + List queued = steeringTexts(vector); + assertFalse(queued.isEmpty(), "the vector queues no steering messages"); + + List delivered = texts(run.executor().received.get(0)); + int previous = -1; + for (String text : queued) { + int at = delivered.indexOf(text); + assertTrue(at >= 0, "steering never delivered: '" + text + "' not in " + delivered); + assertTrue(at > previous, "steering delivered out of order: '" + text + "' in " + delivered); + previous = at; + } + } + + @Test + @DisplayName("injected steering is announced to the caller") + void announcesInjection() { + List events = new ArrayList<>(); + run("steering_inject_message", null, events); + assertTrue(events.contains("status"), "expected a status event in " + events); + } + } + + // ========================================================================= + // Parallel tool calls + // ========================================================================= + + @Nested + @DisplayName("parallel tool calls") + class ParallelTools { + + /** + * The vector's expected text names the Rust runtime because it was recorded there. Everything + * else about the message is shared, so the runtime name is the one part that is dropped. + */ + private String sharedExpectation(Map vector) { + return ((String) expected(vector).get("rust_expected_error")).replace("Rust ", ""); + } + + @Test + @DisplayName("requesting parallel tool calls is rejected as invalid") + void basic() { + Map vector = vector("parallel_tools_basic"); + InvokerException failure = + assertThrows(InvokerException.class, () -> run("parallel_tools_basic")); + + assertEquals(InvokerException.Kind.VALIDATION, failure.kind()); + assertTrue( + failure.getMessage().contains(sharedExpectation(vector)), + "expected the shared rejection message, got: " + failure.getMessage()); + } + + @Test + @DisplayName("the rejection happens before any other configuration is considered") + void withGuardrailDeny() { + Map vector = vector("parallel_tools_with_guardrail_deny"); + InvokerException failure = + assertThrows(InvokerException.class, () -> run("parallel_tools_with_guardrail_deny")); + + assertEquals(InvokerException.Kind.VALIDATION, failure.kind()); + assertTrue( + failure.getMessage().contains(sharedExpectation(vector)), + "expected the shared rejection message, got: " + failure.getMessage()); + } + + @Test + @DisplayName("a rejected turn still reports a start and an end") + void reportsTerminalEvents() { + List events = new ArrayList<>(); + assertThrows( + InvokerException.class, () -> run("parallel_tools_basic", null, events)); + assertEquals(List.of("turn_start", "error", "turn_end"), events); + } + } + + // ========================================================================= + // Coverage guard + // ========================================================================= + + @Test + @DisplayName("every agent vector is exercised") + void everyVectorIsCovered() { + List covered = + List.of( + "no_tool_calls", + "single_tool_call", + "multiple_tool_calls_single_turn", + "multi_turn_tool_calls", + "max_iterations_exceeded", + "bindings_injected", + "tool_not_registered_error", + "tool_result_message_format", + "assistant_tool_calls_metadata", + "empty_tool_result", + "async_tool_function", + "events_basic_tool_loop", + "events_no_tools", + "events_error_logged", + "cancellation_before_llm", + "cancellation_between_tools", + "cancellation_between_iterations", + "context_trim_basic", + "context_no_trim_when_fits", + "context_preserves_system_messages", + "guardrail_input_deny", + "guardrail_output_deny", + "guardrail_tool_deny", + "guardrail_all_pass", + "steering_inject_message", + "steering_multiple_messages", + "parallel_tools_basic", + "parallel_tools_with_guardrail_deny"); + + List declared = new ArrayList<>(); + for (Map vector : VECTORS) { + declared.add(String.valueOf(vector.get("name"))); + } + + List missing = new ArrayList<>(declared); + missing.removeAll(covered); + assertTrue(missing.isEmpty(), "agent vectors with no test: " + missing); + + List unknown = new ArrayList<>(covered); + unknown.removeAll(declared); + assertTrue(unknown.isEmpty(), "tests naming a vector that no longer exists: " + unknown); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/ConnectionsTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/ConnectionsTest.java new file mode 100644 index 000000000..6abf421ea --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/ConnectionsTest.java @@ -0,0 +1,92 @@ +package com.microsoft.prompty; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.microsoft.prompty.model.ApiKeyConnection; +import com.microsoft.prompty.model.Connection; +import com.microsoft.prompty.model.ReferenceConnection; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +/** Named connection resolution — the indirection that keeps secrets out of {@code .prompty} files. */ +class ConnectionsTest { + + @AfterEach + void reset() { + Connections.clear(); + } + + private static ApiKeyConnection apiKey(String key) { + ApiKeyConnection connection = new ApiKeyConnection(); + connection.apiKey = key; + connection.endpoint = "https://example.invalid"; + return connection; + } + + private static ReferenceConnection reference(String name) { + ReferenceConnection connection = new ReferenceConnection(); + connection.name = name; + return connection; + } + + @Test + void concreteConnectionsPassThroughUntouched() { + Connection connection = apiKey("sk-test"); + assertSame(connection, Connections.resolve(connection)); + } + + @Test + void referencesResolveThroughTheRegistry() { + ApiKeyConnection target = apiKey("sk-test"); + Connections.register("prod", target); + + assertSame(target, Connections.resolve(reference("prod"))); + } + + @Test + void referenceChainsResolveToTheirEndpoint() { + ApiKeyConnection target = apiKey("sk-test"); + Connections.register("base", target); + Connections.register("alias", reference("base")); + + assertSame(target, Connections.resolve(reference("alias"))); + } + + @Test + void unknownReferencesFailLoudly() { + // Silently falling back to an anonymous connection would surface much later as a confusing + // authentication error against the wrong endpoint. + InvokerException error = + assertThrows(InvokerException.class, () -> Connections.resolve(reference("missing"))); + assertTrue(error.getMessage().contains("missing")); + } + + @Test + void emptyReferenceNamesFailLoudly() { + assertThrows(InvokerException.class, () -> Connections.resolve(reference(""))); + } + + @Test + void referenceCyclesAreDetectedRatherThanHanging() { + Connections.register("a", reference("b")); + Connections.register("b", reference("a")); + + InvokerException error = + assertThrows(InvokerException.class, () -> Connections.resolve(reference("a"))); + assertTrue(error.getMessage().toLowerCase().contains("cycle")); + } + + @Test + void registrationsCanBeReplacedAndRemoved() { + Connections.register("prod", apiKey("first")); + Connections.register("prod", apiKey("second")); + assertEquals("second", ((ApiKeyConnection) Connections.resolve(reference("prod"))).apiKey); + + assertTrue(Connections.unregister("prod")); + assertNotNull(assertThrows(InvokerException.class, () -> Connections.resolve(reference("prod")))); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/DiscoveryTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/DiscoveryTest.java new file mode 100644 index 000000000..c1225a4ef --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/DiscoveryTest.java @@ -0,0 +1,159 @@ +package com.microsoft.prompty; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.microsoft.prompty.model.ModelInfo; +import com.microsoft.prompty.model.TypraJson; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +/** Covers the shared capability dataset and the rule that applies it. */ +class DiscoveryTest { + + @Nested + @DisplayName("prefix matching") + class PrefixMatching { + + @Test + @DisplayName("a dated id matches its family at the separator") + void datedIdMatchesFamily() { + assertNotNull(Discovery.lookup("openai", "gpt-4-0613")); + } + + @Test + @DisplayName("the most specific family wins") + void longestPrefixWins() { + Discovery.Capabilities mini = Discovery.lookup("openai", "gpt-4o-mini-2024-07-18"); + assertNotNull(mini); + assertEquals(128000, mini.contextWindow()); + // gpt-4 also prefixes this id but is shorter, and its window is different; if ordering were + // wrong this would read 8192. + assertEquals(List.of("text", "image"), mini.inputModalities()); + } + + @Test + @DisplayName("a longer family name is not captured by a shorter one") + void requiresATokenBoundary() { + // The character after `gpt-4` is alphanumeric, so this is a different family and must not + // inherit gpt-4's context window. + assertNull(Discovery.lookup("openai", "gpt-45")); + assertFalse(Discovery.prefixMatches("gpt-45", "gpt-4")); + assertTrue(Discovery.prefixMatches("gpt-4", "gpt-4")); + assertTrue(Discovery.prefixMatches("gpt-4.1", "gpt-4")); + } + + @Test + @DisplayName("an unknown id or provider has no entry") + void unknownLookupsAreEmpty() { + assertNull(Discovery.lookup("openai", "some-custom-model")); + assertNull(Discovery.lookup("nonexistent", "gpt-4o")); + assertNull(Discovery.lookup("openai", null)); + } + + @Test + @DisplayName("an empty modality list is a real answer, not a missing one") + void embeddingsDeclareNoOutputModality() { + Discovery.Capabilities caps = Discovery.lookup("openai", "text-embedding-3-small"); + assertNotNull(caps); + assertEquals(8191, caps.contextWindow()); + assertEquals(List.of(), caps.outputModalities()); + } + } + + @Nested + @DisplayName("enrichment") + class Enrichment { + + @Test + @DisplayName("empty fields are filled") + void fillsMissingFields() { + ModelInfo info = new ModelInfo(); + info.id = "gpt-4o"; + Discovery.enrich("openai", info); + assertEquals(128000, info.contextWindow); + assertEquals(List.of("text", "image"), info.inputModalities); + assertEquals(List.of("text"), info.outputModalities); + } + + @Test + @DisplayName("what the provider supplied is never overwritten") + void providerFieldsWin() { + ModelInfo info = new ModelInfo(); + info.id = "gpt-4o"; + info.contextWindow = 999; + info.inputModalities = new java.util.ArrayList<>(List.of("text")); + Discovery.enrich("openai", info); + assertEquals(999, info.contextWindow); + assertEquals(List.of("text"), info.inputModalities); + // Only the field left empty is filled. + assertEquals(List.of("text"), info.outputModalities); + } + + @Test + @DisplayName("an empty list the provider chose to send still wins") + void providerEmptyListWins() { + ModelInfo info = new ModelInfo(); + info.id = "gpt-4o"; + info.inputModalities = new java.util.ArrayList<>(); + Discovery.enrich("openai", info); + assertEquals(List.of(), info.inputModalities); + } + + @Test + @DisplayName("an unknown model is left alone") + void unknownModelIsUntouched() { + ModelInfo info = new ModelInfo(); + info.id = "ft:custom:user-123"; + Discovery.enrich("openai", info); + assertNull(info.contextWindow); + assertNull(info.inputModalities); + assertNull(info.outputModalities); + } + + @Test + @DisplayName("a filled list is a copy, so the shared table cannot be mutated") + void fillsWithACopy() { + ModelInfo first = new ModelInfo(); + first.id = "gpt-4o"; + Discovery.enrich("openai", first); + first.inputModalities.clear(); + + ModelInfo second = new ModelInfo(); + second.id = "gpt-4o"; + Discovery.enrich("openai", second); + assertEquals(List.of("text", "image"), second.inputModalities); + } + } + + @Test + @DisplayName("the vendored dataset still matches the shared source") + void vendoredCopyMatchesSpec() throws Exception { + Path canonical = SpecVectors.repoRoot().resolve("spec/data/model_capabilities.json"); + if (!Files.exists(canonical)) { + // Running from a published jar rather than a checkout; there is nothing to compare against. + return; + } + + String vendored; + try (InputStream stream = Discovery.class.getResourceAsStream(Discovery.RESOURCE)) { + assertNotNull(stream, "the vendored dataset is missing from the jar"); + vendored = new String(stream.readAllBytes(), StandardCharsets.UTF_8); + } + + assertEquals( + TypraJson.parse(Files.readString(canonical)), + TypraJson.parse(vendored), + "runtime/java/prompty/src/main/resources/com/microsoft/prompty/model_capabilities.json has" + + " drifted from spec/data/model_capabilities.json — re-copy the canonical file"); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/ExtensionSeamTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/ExtensionSeamTest.java new file mode 100644 index 000000000..b09c29573 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/ExtensionSeamTest.java @@ -0,0 +1,125 @@ +package com.microsoft.prompty; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +import com.microsoft.prompty.model.ContentPart; +import com.microsoft.prompty.model.ImagePart; +import com.microsoft.prompty.model.Message; +import com.microsoft.prompty.model.TextPart; +import com.microsoft.prompty.model.ToolResult; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +/** + * Covers the hand-written {@code @method} implementations that live in the + * emitter's extension seams. + * + *

The seams are created as throwing stubs when missing, and the emitter + * never rewrites them, so an unimplemented seam is invisible to the generated + * suites and fails only at runtime. These tests pin the behaviour to the Rust + * reference in {@code runtime/rust/prompty/src/model_ext.rs}. + */ +class ExtensionSeamTest { + + private static TextPart text(String value) { + TextPart part = new TextPart(); + part.value = value; + return part; + } + + private static ImagePart image(String source) { + ImagePart part = new ImagePart(); + part.source = source; + return part; + } + + private static Message message(ContentPart... parts) { + Message message = new Message(); + message.parts = new ArrayList<>(List.of(parts)); + return message; + } + + @Test + void textConcatenatesTextParts() { + assertEquals("first\nsecond", message(text("first"), text("second")).text()); + } + + @Test + void textIgnoresNonTextParts() { + assertEquals("caption", message(text("caption"), image("https://example.com/i.png")).text()); + } + + @Test + void textIsEmptyWithoutTextParts() { + assertEquals("", message(image("https://example.com/i.png")).text()); + assertEquals("", message().text()); + } + + @Test + void textToleratesNullParts() { + Message message = new Message(); + message.parts = null; + assertEquals("", message.text()); + } + + @Test + void toTextContentReturnsStringWhenEveryPartIsText() { + Object content = message(text("simple")).toTextContent(); + assertInstanceOf(String.class, content); + assertEquals("simple", content); + } + + @Test + void toTextContentJoinsMultipleTextParts() { + assertEquals("one\ntwo", message(text("one"), text("two")).toTextContent()); + } + + @Test + void toTextContentReturnsWireFormWhenAnyPartIsRich() { + Object content = message(text("Hello"), image("data:image/png;base64,abc")).toTextContent(); + assertInstanceOf(List.class, content); + + List parts = (List) content; + assertEquals(2, parts.size()); + assertEquals("text", ((Map) parts.get(0)).get("kind")); + assertEquals("image", ((Map) parts.get(1)).get("kind")); + } + + @Test + void toTextContentReturnsEmptyStringForNoParts() { + // An empty part list vacuously satisfies "every part is text", which is the + // behaviour the Rust reference relies on to keep empty messages scalar. + assertEquals("", message().toTextContent()); + } + + @Test + void toolResultTextConcatenatesTextParts() { + ToolResult result = new ToolResult(); + result.parts = new ArrayList<>(List.of(text("72\u00b0F"), image("https://example.com/i.png"), text("sunny"))); + assertEquals("72\u00b0F\nsunny", result.text()); + } + + @Test + void toolResultTextToleratesNullParts() { + ToolResult result = new ToolResult(); + result.parts = null; + assertEquals("", result.text()); + } + + @Test + void seamsAreReachableThroughTheGeneratedEntryPoints() { + // The generated methods delegate to the seam classes, which the emitter + // creates as throwing stubs when absent and never overwrites. Assert against + // literals so an unimplemented seam fails here rather than at runtime. + Message message = message(text("delegated")); + assertEquals("delegated", message.text()); + assertEquals("delegated", message.toTextContent()); + + ToolResult result = new ToolResult(); + result.parts = new ArrayList<>(List.of(text("delegated"))); + assertEquals("delegated", result.text()); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/GeneratedExamplesTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/GeneratedExamplesTest.java new file mode 100644 index 000000000..faf8d0d31 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/GeneratedExamplesTest.java @@ -0,0 +1,97 @@ +package com.microsoft.prompty; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.DynamicTest.dynamicTest; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; + +/** + * Runs every example the Typra emitter generates for the model layer. + * + *

The emitter emits one {@code GeneratedTest} class per model type, each exposing a + * package-private {@code run()}, plus a {@code TypraGeneratedTests} class that calls them from a + * {@code main()}. JUnit does not discover either shape, so this factory turns each generated class + * into its own dynamic test — a failure then names the offending model type instead of collapsing + * the whole set into one red test. + * + *

The classes are discovered from the compiled output directory rather than from a generated + * registry, so nothing outside the emitter's own output has to be kept in sync when the schema + * gains or loses a type. + */ +final class GeneratedExamplesTest { + + /** The emitter currently produces ~147 example classes; guard against silent discovery of none. */ + private static final int MINIMUM_EXPECTED = 100; + + private static final String RUNNER = "com.microsoft.prompty.model.TypraGeneratedTests"; + + @TestFactory + Stream generatedModelExamples() throws Exception { + List classNames = discover(); + assertTrue( + classNames.size() >= MINIMUM_EXPECTED, + "discovered only " + + classNames.size() + + " generated example classes, expected at least " + + MINIMUM_EXPECTED + + "; the emitter output layout changed"); + + return classNames.stream() + .map( + className -> + dynamicTest( + className.substring(className.lastIndexOf('.') + 1), () -> invokeRun(className))); + } + + private static List discover() throws Exception { + Path packageDir = + Paths.get( + Class.forName(RUNNER) + .getProtectionDomain() + .getCodeSource() + .getLocation() + .toURI()) + .resolve("com/microsoft/prompty/model"); + + List names = new ArrayList<>(); + try (Stream entries = Files.list(packageDir)) { + for (Path entry : entries.toList()) { + String file = entry.getFileName().toString(); + // Nested classes carry a '$'; only the top-level example classes expose run(). + if (file.endsWith("GeneratedTest.class") && !file.contains("$")) { + names.add("com.microsoft.prompty.model." + file.substring(0, file.length() - ".class".length())); + } + } + } + Collections.sort(names); + return names; + } + + private static void invokeRun(String className) throws Exception { + Method run = Class.forName(className).getDeclaredMethod("run"); + run.setAccessible(true); + try { + run.invoke(null); + } catch (InvocationTargetException e) { + // Surface the generated assertion failure itself, not the reflection wrapper. + Throwable cause = e.getCause(); + if (cause instanceof Error error) { + throw error; + } + if (cause instanceof Exception exception) { + throw exception; + } + throw e; + } + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/HttpTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/HttpTest.java new file mode 100644 index 000000000..7da620c53 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/HttpTest.java @@ -0,0 +1,280 @@ +package com.microsoft.prompty; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.sun.net.httpserver.HttpServer; +import java.io.Closeable; +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Transport behaviour, exercised against a loopback server. + * + *

Mocking the client would only prove the mock behaves as written. The parts that actually break + * in production — SSE framing, error classification, connection refusal — only show up when real + * sockets are involved, so this uses a real one. + */ +class HttpTest { + + private HttpServer server; + private String baseUrl; + + @BeforeEach + void startServer() throws IOException { + server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); + server.start(); + baseUrl = "http://127.0.0.1:" + server.getAddress().getPort(); + } + + @AfterEach + void stopServer() { + server.stop(0); + } + + private void respond(String path, int status, String body) { + server.createContext( + path, + exchange -> { + byte[] bytes = body.getBytes(StandardCharsets.UTF_8); + exchange.sendResponseHeaders(status, bytes.length); + try (OutputStream out = exchange.getResponseBody()) { + out.write(bytes); + } + }); + } + + private static List collect(Iterator stream) { + List chunks = new ArrayList<>(); + stream.forEachRemaining(chunks::add); + return chunks; + } + + /** Echo back the request's Content-Type and body so a form POST can be inspected as sent. */ + private void echoRequest(String path, int status) { + server.createContext( + path, + exchange -> { + String contentType = exchange.getRequestHeaders().getFirst("Content-Type"); + String requestBody = + new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8); + byte[] bytes = + (contentType + "\n" + requestBody).getBytes(StandardCharsets.UTF_8); + exchange.sendResponseHeaders(status, bytes.length); + try (OutputStream out = exchange.getResponseBody()) { + out.write(bytes); + } + }); + } + + @Test + void formPostsUseTheFormMediaTypeAndEncoding() { + echoRequest("/form", 200); + + Http.FormResult result = + Http.postForm( + "test", + baseUrl + "/form", + new java.util.LinkedHashMap<>( + Map.of("grant_type", "authorization_code", "code", "a b&c"))); + + assertTrue(result.isSuccess()); + String[] lines = result.body().split("\n", 2); + // OAuth token endpoints reject a JSON body outright, so the media type is not incidental. + assertEquals("application/x-www-form-urlencoded", lines[0]); + assertTrue(lines[1].contains("grant_type=authorization_code"), "body was: " + lines[1]); + // A space becomes '+' and an ampersand must be escaped, or it would split the field. + assertTrue(lines[1].contains("code=a+b%26c"), "body was: " + lines[1]); + } + + @Test + void formPostsReturnErrorStatusesInsteadOfThrowing() { + respond("/pending", 400, "{\"error\":\"authorization_pending\"}"); + + // A device-code poll reports "not yet" as an HTTP error, so throwing here would make the + // ordinary path of a sign-in indistinguishable from a failure. + Http.FormResult result = Http.postForm("test", baseUrl + "/pending", Map.of()); + + assertEquals(400, result.status()); + assertFalse(result.isSuccess()); + assertTrue(result.body().contains("authorization_pending")); + } + + @Test + void formPostsStillRaiseWhenTheExchangeNeverHappens() { + assertThrows( + InvokerException.class, () -> Http.postForm("test", "http://127.0.0.1:1/never", Map.of())); + } + + @Test + void anEmptyFormEncodesToAnEmptyBody() { + assertEquals("", Http.encodeForm(Map.of())); + } + + @Test + void jsonResponsesAreParsed() { + respond("/ok", 200, "{\"answer\":42}"); + + Object response = Http.postJson("test", baseUrl + "/ok", Map.of(), Map.of("q", "life")); + assertEquals(42L, ((Number) Streams.pointer(response, "answer")).longValue()); + } + + @Test + void errorStatusesRaiseDeterminateFailures() { + respond("/bad", 400, "{\"error\":{\"message\":\"nope\"}}"); + + InvokerException error = + assertThrows( + InvokerException.class, + () -> Http.postJson("test", baseUrl + "/bad", Map.of(), Map.of())); + + // The server rejected the request outright, so nothing happened and a retry is safe. + assertEquals(InvokerException.Kind.EXECUTE, error.kind()); + assertTrue(error.getMessage().contains("nope")); + } + + @Test + void unreachableEndpointsRaiseDeterminateFailures() { + // Port 1 on loopback refuses immediately: the request provably never reached a server. + InvokerException error = + assertThrows( + InvokerException.class, + () -> Http.postJson("test", "http://127.0.0.1:1/never", Map.of(), Map.of())); + + assertEquals(InvokerException.Kind.EXECUTE, error.kind()); + } + + @Test + void serverSentEventsAreFramedIntoChunks() { + respond( + "/stream", + 200, + """ + data: {"seq":1} + + data: {"seq":2} + + data: [DONE] + + """); + + List chunks = collect(Http.postSse("test", baseUrl + "/stream", Map.of(), Map.of())); + + // `[DONE]` is a terminator, not a chunk. + assertEquals(2, chunks.size()); + assertEquals(1L, ((Number) Streams.pointer(chunks.get(0), "seq")).longValue()); + assertEquals(2L, ((Number) Streams.pointer(chunks.get(1), "seq")).longValue()); + } + + @Test + void streamCommentsAndBlankLinesAreIgnored() { + respond( + "/comments", + 200, + """ + : keep-alive + + event: ping + + data: {"seq":1} + + """); + + List chunks = collect(Http.postSse("test", baseUrl + "/comments", Map.of(), Map.of())); + assertEquals(1, chunks.size()); + } + + @Test + void unparseableStreamPayloadsSurfaceAsErrorChunksRatherThanThrowing() { + respond("/broken", 200, "data: {not json}\n\n"); + + List chunks = collect(Http.postSse("test", baseUrl + "/broken", Map.of(), Map.of())); + + // A caller mid-iteration cannot recover from an exception, so the failure travels in-band. + assertEquals(1, chunks.size()); + assertEquals("sse_parse_error", Streams.pointer(chunks.get(0), "error", "type")); + } + + @Test + void streamErrorStatusesRaiseBeforeIteration() { + respond("/denied", 401, "{\"error\":{\"message\":\"unauthorized\"}}"); + + InvokerException error = + assertThrows( + InvokerException.class, + () -> collect(Http.postSse("test", baseUrl + "/denied", Map.of(), Map.of()))); + assertFalse(error.getMessage().isEmpty()); + } + + @Test + void abandonedStreamsCanBeClosedToReleaseTheConnection() { + respond( + "/long", + 200, + """ + data: {"seq":1} + + data: {"seq":2} + + data: {"seq":3} + + """); + + Iterator stream = Http.postSse("test", baseUrl + "/long", Map.of(), Map.of()); + assertTrue(stream.hasNext()); + stream.next(); + + // Java has no destructor, so a stream stopped early has to be closed explicitly or the + // connection stays checked out of the pool until the collector eventually notices. + assertInstanceOf(Closeable.class, stream); + Streams.close(stream); + + // Reading past a closed stream must report exhaustion rather than blocking or throwing. + assertFalse(stream.hasNext()); + } + + @Test + void closingPropagatesThroughStreamWrappers() { + respond("/wrapped", 200, "data: {\"seq\":1}\n\ndata: {\"seq\":2}\n\n"); + + Iterator source = Http.postSse("test", baseUrl + "/wrapped", Map.of(), Map.of()); + CancellationToken token = new CancellationToken(); + Iterator wrapped = Streams.cancellable(Streams.peeking(source, chunk -> {}), token); + + assertTrue(wrapped.hasNext()); + wrapped.next(); + + // The connection to release sits at the bottom of the chain, so closure has to travel down it. + Streams.close(wrapped); + assertFalse(source.hasNext()); + } + + @Test + void cancellationReleasesTheUnderlyingStream() { + respond("/cancel", 200, "data: {\"seq\":1}\n\ndata: {\"seq\":2}\n\n"); + + Iterator source = Http.postSse("test", baseUrl + "/cancel", Map.of(), Map.of()); + CancellationToken token = new CancellationToken(); + Iterator stream = Streams.cancellable(source, token); + + assertTrue(stream.hasNext()); + stream.next(); + token.cancel(); + + // Cancellation is a normal outcome, and the common one; leaking on it would exhaust the pool. + assertFalse(stream.hasNext()); + assertFalse(source.hasNext()); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/LiveStreamFailureTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/LiveStreamFailureTest.java new file mode 100644 index 000000000..0181c2e56 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/LiveStreamFailureTest.java @@ -0,0 +1,141 @@ +package com.microsoft.prompty; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.microsoft.prompty.model.LoadContext; +import com.microsoft.prompty.model.Message; +import com.microsoft.prompty.model.Prompty; +import com.microsoft.prompty.model.StreamChunk; +import com.microsoft.prompty.model.TextChunk; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * Covers how a streaming response that dies part-way through is reported. + * + *

A stream can fail in two very different ways. Either the provider definitively rejected the + * request, or the connection dropped after the request was accepted and nobody knows whether it ran. + * The second case is the dangerous one: the model may already have answered and its tools may + * already have fired, so retrying would repeat the work and the charge. The turn has to carry that + * distinction out to the engine, which is the only thing that can decide between retrying and + * reconciling. + */ +class LiveStreamFailureTest { + + private static final String PROVIDER = "streamfailuretest"; + + /** Opens a stream of one text chunk followed by whatever failure the test wants. */ + private static final class ScriptedExecutor implements Executor { + private int opened; + + @Override + public Object execute(Prompty agent, List messages) { + throw new AssertionError("the turn should have streamed rather than called execute"); + } + + @Override + public Iterator executeStream(Prompty agent, List messages) { + opened++; + return List.of("chunk").iterator(); + } + } + + /** Turns the executor's placeholder chunk into a text chunk, then the scripted failure. */ + private record ScriptedProcessor(StreamFailure failure) implements Processor { + @Override + public Object process(Prompty agent, Object response) { + return ""; + } + + @Override + public Iterator processStream(Prompty agent, Iterator response) { + List chunks = new ArrayList<>(); + TextChunk partial = new TextChunk(); + partial.value = "partial"; + chunks.add(partial); + chunks.add(failure); + return chunks.iterator(); + } + } + + private static Prompty streamingAgent(String provider) { + Map model = new LinkedHashMap<>(); + model.put("id", "test-model"); + model.put("provider", provider); + model.put("options", Map.of("additionalProperties", Map.of("stream", true))); + + Map data = new LinkedHashMap<>(); + data.put("kind", "prompt"); + data.put("name", "stream_failure_test"); + data.put("model", model); + data.put("instructions", "system:\nYou are helpful.\n\nuser:\nHello."); + data.put( + "template", Map.of("format", Map.of("kind", "nunjucks"), "parser", Map.of("kind", "prompty"))); + return Prompty.load(data, new LoadContext()); + } + + /** A turn's outcome together with how many times the stream was actually opened. */ + private record Attempt(InvokerException failure, int opened) {} + + private static Attempt runWith(StreamFailure failure, String provider) { + ScriptedExecutor executor = new ScriptedExecutor(); + Registry.registerExecutor(provider, executor); + Registry.registerProcessor(provider, new ScriptedProcessor(failure)); + InvokerException thrown = + assertThrows( + InvokerException.class, + () -> Pipeline.turn(streamingAgent(provider), Map.of(), TurnOptions.defaults())); + return new Attempt(thrown, executor.opened); + } + + @Test + @DisplayName("a stream that fails with an unknown outcome is reported as indeterminate") + void indeterminateStreamFailureKeepsItsKind() { + Attempt attempt = + runWith(StreamFailure.indeterminate("SSE stream error: connection reset"), PROVIDER + "_i"); + InvokerException failure = attempt.failure(); + + assertEquals( + InvokerException.Kind.EXECUTE_INDETERMINATE, + failure.kind(), + "expected an indeterminate failure, got " + failure.kind() + ": " + failure.getMessage()); + assertTrue( + failure.getMessage().contains("connection reset"), + "the provider's reason should survive: " + failure.getMessage()); + + // The metadata is what a host needs to reconcile the effect against the provider. + assertEquals(PROVIDER + "_i", failure.metadata().get("provider")); + assertEquals("stream_transport", failure.metadata().get("phase")); + + // The consequence that matters: the request is never replayed. The model may already have + // answered and its tools may already have fired, so a retry would repeat both. + assertEquals(1, attempt.opened(), "an indeterminate stream failure must not be retried"); + } + + @Test + @DisplayName("a stream that fails definitively is retried and then reported as exhausted") + void determinateStreamFailureIsRetried() { + Attempt attempt = + runWith(StreamFailure.determinate("model rejected the request"), PROVIDER + "_d"); + InvokerException failure = attempt.failure(); + + // A definite failure is safe to replay, so it takes the ordinary retry path instead. + assertEquals( + InvokerException.Kind.EXECUTE_RETRY_EXHAUSTED, + failure.kind(), + "expected retry exhaustion, got " + failure.kind() + ": " + failure.getMessage()); + assertTrue( + failure.getMessage().contains("model rejected the request"), + "the provider's reason should survive: " + failure.getMessage()); + assertTrue( + attempt.opened() > 1, + "a determinate stream failure should have been retried, opened " + attempt.opened()); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/LoadVectorsTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/LoadVectorsTest.java new file mode 100644 index 000000000..66ee03630 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/LoadVectorsTest.java @@ -0,0 +1,225 @@ +package com.microsoft.prompty; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.DynamicTest.dynamicTest; + +import com.microsoft.prompty.model.Prompty; +import com.microsoft.prompty.model.SaveContext; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.Yaml; + +/** + * Runs the shared {@code spec/vectors/load} suite against the Java loader. + * + *

These are the same cases every other Prompty runtime is held to. Passing them is what makes a + * {@code .prompty} file portable: the same file, the same environment, the same resulting agent, no + * matter which language reads it. + */ +class LoadVectorsTest { + + /** Finds the names a vector reads through {@code ${env:NAME}} or {@code ${env:NAME:default}}. */ + private static final Pattern ENV_REFERENCE = Pattern.compile("\\$\\{env:([A-Za-z_][A-Za-z0-9_]*)"); + + @TestFactory + List loadVectors() { + List tests = new ArrayList<>(); + for (Map testCase : SpecVectors.readArray("load/load_vectors.json")) { + String name = SpecVectors.string(testCase, "name"); + tests.add(dynamicTest(name, () -> runCase(name, testCase))); + } + return tests; + } + + private void runCase(String name, Map testCase) { + Map input = SpecVectors.map(testCase, "input"); + Map expected = SpecVectors.map(testCase, "expected"); + Map env = SpecVectors.map(input, "env"); + + List applied = setEnv(input, env); + try { + if (expected.containsKey("error")) { + runErrorCase(name, input, expected); + } else if (expected.containsKey("validated_inputs")) { + runValidationCase(name, input, expected); + } else { + runFieldCase(name, input, expected); + } + } finally { + clearEnv(applied); + } + } + + // ---------------------------------------------------------------- case kinds + + private void runFieldCase(String name, Map input, Map expected) { + Prompty agent = load(input); + // Ask for the long form: named collections as arrays and no shorthand collapsing, which is the + // shape the shared vectors describe. Both forms round-trip to the same agent; the vectors just + // pick the one that is unambiguous to write down. + SaveContext saveContext = new SaveContext(); + saveContext.collectionFormat = "array"; + saveContext.useShorthand = false; + Map actual = agent.save(saveContext); + + for (Map.Entry entry : expected.entrySet()) { + String key = entry.getKey(); + Object want = entry.getValue(); + + if ("kind".equals(key)) { + // `kind` is consumed while loading — it selects the model type rather than becoming a + // field. A successful load of a vector that asks for "prompt" is the assertion. + assertEquals("prompt", want, "[" + name + "] vectors should only load prompt agents"); + continue; + } + if ("instructions".equals(key)) { + assertEquals(want, agent.instructions, "[" + name + "] instructions"); + continue; + } + Object got = actual.get(key); + SpecVectors.assertMatches("[" + name + "] " + key, want, got); + } + } + + private void runErrorCase(String name, Map input, Map expected) { + Throwable thrown = null; + try { + Prompty agent = load(input); + Map inputs = SpecVectors.map(input, "inputs"); + Pipeline.validateInputs(agent, inputs); + } catch (RuntimeException e) { + thrown = e; + } + + SpecVectors.assertErrorMatches("[" + name + "]", SpecVectors.string(expected, "error"), thrown); + + String field = SpecVectors.string(expected, "error_field"); + if (field != null) { + assertTrue( + thrown.getMessage() != null && thrown.getMessage().contains(field), + "[" + name + "] error should name the offending field \"" + field + "\": " + thrown.getMessage()); + } + } + + private void runValidationCase(String name, Map input, Map expected) { + Prompty agent = load(input); + Map validated = Pipeline.validateInputs(agent, SpecVectors.map(input, "inputs")); + SpecVectors.assertMatches("[" + name + "] validated_inputs", expected.get("validated_inputs"), validated); + + // The vector lists the whole expected result, so anything extra is a defect: an example value + // leaking through as a default would silently change what the model is asked. + Object want = expected.get("validated_inputs"); + if (want instanceof Map wantMap) { + assertEquals(wantMap.size(), validated.size(), "[" + name + "] unexpected extra validated inputs: " + validated); + } + } + + // ---------------------------------------------------------------- loading + + private Prompty load(Map input) { + String fixture = SpecVectors.string(input, "fixture"); + if (fixture != null) { + return Loader.load(SpecVectors.fixtures().resolve(fixture)); + } + + String raw = SpecVectors.string(input, "frontmatter_raw"); + Map files = SpecVectors.map(input, "files"); + Path root = tempRoot(); + + if (raw == null) { + Object frontmatter = input.get("frontmatter"); + raw = "---\n" + toYaml(frontmatter) + "---\n"; + } + + for (Map.Entry file : files.entrySet()) { + Path target = root.resolve(file.getKey()); + Object content = file.getValue(); + write(target, content instanceof String text ? text : com.microsoft.prompty.model.TypraJson.stringify(content)); + } + + // `${file:}` references resolve relative to the agent's own directory, so the vector's virtual + // files have to sit beside a virtual agent path rather than beside the test's working directory. + return Loader.loadFromString(raw, root.resolve("virtual.prompty")); + } + + private static String toYaml(Object frontmatter) { + if (frontmatter == null) { + return ""; + } + DumperOptions options = new DumperOptions(); + options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + return new Yaml(options).dump(frontmatter); + } + + private Path tempRoot() { + try { + Path dir = Files.createTempDirectory("prompty-load-vectors"); + dir.toFile().deleteOnExit(); + return dir; + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private static void write(Path target, String content) { + try { + Path parent = target.getParent(); + if (parent != null) { + Files.createDirectories(parent); + } + Files.writeString(target, content, StandardCharsets.UTF_8); + target.toFile().deleteOnExit(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + // ---------------------------------------------------------------- environment + + private static List setEnv(Map input, Map env) { + List keys = new ArrayList<>(); + for (Map.Entry entry : env.entrySet()) { + Environment.set(entry.getKey(), String.valueOf(entry.getValue())); + keys.add(entry.getKey()); + } + + // A vector that references a variable it does not supply -- `${env:NONEXISTENT}` -- is asserting + // that the variable is unset. Say so explicitly: a JVM cannot remove a name from its own + // environment, so without a mask the assertion would quietly evaporate on any machine that + // happens to export it. + Matcher references = ENV_REFERENCE.matcher(String.valueOf(input)); + while (references.find()) { + String name = references.group(1); + if (!env.containsKey(name)) { + Environment.mask(name); + keys.add(name); + } + } + return keys; + } + + private static void clearEnv(List keys) { + for (String key : keys) { + Environment.clear(key); + } + } + + /** Guards against a silent regression where every vector is skipped. */ + @org.junit.jupiter.api.Test + void suiteIsNotEmpty() { + List> cases = SpecVectors.readArray("load/load_vectors.json"); + assertTrue(cases.size() >= 25, "expected the full load vector suite, got " + cases.size()); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/MemoryTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/MemoryTest.java new file mode 100644 index 000000000..7d4b08152 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/MemoryTest.java @@ -0,0 +1,314 @@ +package com.microsoft.prompty; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.microsoft.prompty.model.MemoryCategory; +import com.microsoft.prompty.model.MemoryEntry; +import com.microsoft.prompty.model.MemoryStore; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; + +/** Grades tiered recall, injection, and eviction against the reference runtime's rules. */ +class MemoryTest { + + private static MemoryEntry entry(String content, MemoryCategory category, String... tags) { + MemoryEntry memory = new MemoryEntry(); + memory.content = content; + memory.category = category; + memory.tags = tags.length == 0 ? null : new ArrayList<>(List.of(tags)); + return memory; + } + + private static MemoryStore storeOf(MemoryEntry... entries) { + MemoryStore store = new MemoryStore(); + store.entries = new ArrayList<>(List.of(entries)); + return store; + } + + private static List contents(List results) { + return results.stream().map(scored -> scored.entry().content).toList(); + } + + @Test + void tagMatchesOutrankContentMatches() { + MemoryStore store = + storeOf( + entry("the deploy pipeline runs nightly", MemoryCategory.ARCHIVAL), + entry("nothing relevant here", MemoryCategory.ARCHIVAL, "deploy")); + + List results = Memory.recall(store, "deploy", 0); + + assertEquals(List.of("nothing relevant here", "the deploy pipeline runs nightly"), contents(results)); + assertEquals(3.0, results.get(0).score(), 1e-9, "a tag hit is worth 3"); + assertEquals(2.0, results.get(1).score(), 1e-9, "a content hit is worth 2"); + } + + @Test + void coreMemoriesGetABoostOnlyWhenTheyMatch() { + MemoryStore store = + storeOf( + entry("deploy on fridays", MemoryCategory.CORE), + entry("deploy on mondays", MemoryCategory.ARCHIVAL)); + + List results = Memory.recall(store, "deploy", 0); + + assertEquals(3.0, results.get(0).score(), 1e-9, "content hit plus the core boost"); + assertEquals(2.0, results.get(1).score(), 1e-9); + + // A core memory that matched nothing is not surfaced at all, so the boost cannot lift it. + assertTrue(Memory.recall(store, "unrelated", 0).isEmpty()); + } + + @Test + void aMemoryMatchingBothContentAndTagsScoresBoth() { + MemoryStore store = storeOf(entry("deploy nightly", MemoryCategory.CORE, "deploy")); + + Memory.Scored only = Memory.recall(store, "deploy", 0).get(0); + + assertEquals(6.0, only.score(), 1e-9, "2 content + 3 tag + 1 core"); + assertEquals(1, only.keywordMatches(), "one distinct keyword matched, however many ways"); + } + + @Test + void tiesFallBackToInsertionOrder() { + MemoryStore store = + storeOf( + entry("deploy first", MemoryCategory.ARCHIVAL), + entry("deploy second", MemoryCategory.ARCHIVAL), + entry("deploy third", MemoryCategory.ARCHIVAL)); + + assertEquals( + List.of("deploy first", "deploy second", "deploy third"), + contents(Memory.recall(store, "deploy", 0))); + } + + @Test + void queryTokensAreNormalizedAndDeduplicated() { + MemoryStore store = storeOf(entry("Deploy the service", MemoryCategory.ARCHIVAL)); + + // Case, surrounding punctuation, and a repeat of the same word must not change the score. + assertEquals(2.0, Memory.recall(store, "DEPLOY", 0).get(0).score(), 1e-9); + assertEquals(2.0, Memory.recall(store, "(deploy)", 0).get(0).score(), 1e-9); + assertEquals(2.0, Memory.recall(store, "deploy deploy", 0).get(0).score(), 1e-9); + assertEquals(4.0, Memory.recall(store, "deploy service", 0).get(0).score(), 1e-9); + } + + @Test + void anEmptyQueryReturnsEverythingUnranked() { + MemoryStore store = + storeOf( + entry("first", MemoryCategory.CORE), + entry("second", MemoryCategory.ARCHIVAL)); + + for (String query : new String[] {"", " ", "!!!", null}) { + List results = Memory.recall(store, query, 0); + assertEquals(List.of("first", "second"), contents(results), "query: " + query); + assertEquals(0.0, results.get(0).score(), 1e-9); + } + } + + @Test + void limitCapsResultsAndZeroMeansUnlimited() { + MemoryStore store = + storeOf( + entry("deploy one", MemoryCategory.ARCHIVAL), + entry("deploy two", MemoryCategory.ARCHIVAL), + entry("deploy three", MemoryCategory.ARCHIVAL)); + + assertEquals(2, Memory.recall(store, "deploy", 2).size()); + assertEquals(3, Memory.recall(store, "deploy", 0).size()); + assertEquals(3, Memory.recall(store, "deploy", 99).size()); + } + + @Test + void rememberReplacesACoreFactWithTheSameTags() { + MemoryStore store = storeOf(); + Memory.remember(store, entry("prefers tabs", MemoryCategory.CORE, "style"), 0); + Memory.remember(store, entry("prefers spaces", MemoryCategory.CORE, "style"), 0); + + assertEquals(1, store.entries.size(), "a restated fact replaces rather than accumulates"); + assertEquals("prefers spaces", store.entries.get(0).content); + } + + @Test + void rememberKeepsCoreFactsScopedByDifferentTags() { + MemoryStore store = storeOf(); + Memory.remember(store, entry("prefers tabs", MemoryCategory.CORE, "style"), 0); + Memory.remember(store, entry("deploys on friday", MemoryCategory.CORE, "process"), 0); + + assertEquals(2, store.entries.size(), "different scopes are different facts"); + } + + @Test + void anAbsentTagListEqualsAnEmptyOne() { + MemoryStore store = storeOf(); + MemoryEntry untagged = entry("first", MemoryCategory.CORE); + MemoryEntry emptyTags = entry("second", MemoryCategory.CORE); + emptyTags.tags = new ArrayList<>(); + + Memory.remember(store, untagged, 0); + Memory.remember(store, emptyTags, 0); + + assertEquals(1, store.entries.size(), "no tags and empty tags are the same scope"); + assertEquals("second", store.entries.get(0).content); + } + + @Test + void rememberOnlyDeduplicatesCoreMemories() { + MemoryStore store = storeOf(); + Memory.remember(store, entry("first summary", MemoryCategory.ARCHIVAL, "session"), 0); + Memory.remember(store, entry("second summary", MemoryCategory.ARCHIVAL, "session"), 0); + + assertEquals(2, store.entries.size(), "archival memories accumulate"); + } + + @Test + void evictionTakesArchivalMemoriesFirst() { + MemoryStore store = + storeOf( + entry("core fact", MemoryCategory.CORE), + entry("old summary", MemoryCategory.ARCHIVAL), + entry("an insight", MemoryCategory.INSIGHT)); + + assertEquals(1, Memory.evictToCap(store, 2)); + assertEquals( + List.of("core fact", "an insight"), + store.entries.stream().map(memory -> memory.content).toList(), + "the summary goes before anything else"); + } + + @Test + void evictionFallsBackToTheOldestWhenNothingIsArchival() { + MemoryStore store = + storeOf( + entry("oldest", MemoryCategory.CORE), + entry("middle", MemoryCategory.INSIGHT), + entry("newest", MemoryCategory.CORE)); + + assertEquals(2, Memory.evictToCap(store, 1)); + assertEquals(List.of("newest"), store.entries.stream().map(memory -> memory.content).toList()); + } + + @Test + void aZeroCapMeansNoCap() { + MemoryStore store = + storeOf(entry("a", MemoryCategory.ARCHIVAL), entry("b", MemoryCategory.ARCHIVAL)); + + assertEquals(0, Memory.evictToCap(store, 0)); + assertEquals(2, store.entries.size()); + + Memory.remember(store, entry("c", MemoryCategory.ARCHIVAL), 0); + assertEquals(3, store.entries.size(), "remember respects an uncapped store"); + } + + @Test + void rememberEnforcesTheCap() { + MemoryStore store = storeOf(); + for (String content : List.of("a", "b", "c", "d")) { + Memory.remember(store, entry(content, MemoryCategory.ARCHIVAL), 2); + } + + assertEquals(List.of("c", "d"), store.entries.stream().map(memory -> memory.content).toList()); + } + + @Test + void mutationHelpersActOnTheRightEntry() { + MemoryStore store = + storeOf(entry("first", MemoryCategory.CORE, "keep"), entry("second", MemoryCategory.ARCHIVAL)); + + Memory.updateContent(store, 0, "rewritten"); + assertEquals("rewritten", store.entries.get(0).content); + assertEquals(List.of("keep"), store.entries.get(0).tags, "content-only edits preserve tags"); + assertEquals(MemoryCategory.CORE, store.entries.get(0).category); + + Memory.update(store, 1, entry("replaced", MemoryCategory.INSIGHT)); + assertEquals(MemoryCategory.INSIGHT, store.entries.get(1).category); + + assertEquals("replaced", Memory.remove(store, 1).content); + assertEquals(1, store.entries.size()); + } + + @Test + void mutationHelpersRejectAnOutOfRangeIndex() { + MemoryStore store = storeOf(entry("only", MemoryCategory.CORE)); + + assertThrows(IndexOutOfBoundsException.class, () -> Memory.remove(store, 1)); + assertThrows(IndexOutOfBoundsException.class, () -> Memory.remove(store, -1)); + assertThrows(IndexOutOfBoundsException.class, () -> Memory.updateContent(store, 5, "x")); + assertEquals(1, store.entries.size(), "a rejected mutation changes nothing"); + } + + @Test + void clearTargetsATierOrEverything() { + MemoryStore store = + storeOf( + entry("core", MemoryCategory.CORE), + entry("summary", MemoryCategory.ARCHIVAL), + entry("another summary", MemoryCategory.ARCHIVAL)); + + assertEquals(2, Memory.clear(store, MemoryCategory.ARCHIVAL)); + assertEquals(1, store.entries.size()); + assertEquals(1, Memory.clear(store, null)); + assertTrue(store.entries.isEmpty()); + assertEquals(0, Memory.clear(store, null), "clearing an empty store removes nothing"); + } + + @Test + void onlyCoreMemoriesAreInjectedIntoTheSystemPrompt() { + MemoryStore store = + storeOf( + entry("prefers tabs", MemoryCategory.CORE), + entry("a summary", MemoryCategory.ARCHIVAL), + entry("deploys on friday", MemoryCategory.CORE)); + + assertEquals( + "## Memory\n- prefers tabs\n- deploys on friday\n", Memory.formatForSystemPrompt(store)); + } + + @Test + void anEmptyMemoryBlockIsOmittedEntirely() { + assertEquals("", Memory.formatForSystemPrompt(storeOf())); + assertEquals( + "", + Memory.formatForSystemPrompt(storeOf(entry("a summary", MemoryCategory.ARCHIVAL))), + "a store with no core memories injects nothing rather than an empty heading"); + } + + @Test + void recallResultsFormatWithTierAndTags() { + MemoryStore store = + storeOf( + entry("deploy on friday", MemoryCategory.CORE, "process", "release"), + entry("deploy notes", MemoryCategory.ARCHIVAL)); + + assertEquals( + "1. [core] deploy on friday\n tags: process, release\n2. [archival] deploy notes\n", + Memory.formatRecallResults(Memory.recall(store, "deploy", 0))); + assertEquals("", Memory.formatRecallResults(List.of())); + } + + @Test + void addAppliesNoTierPolicy() { + MemoryStore store = storeOf(); + Memory.add(store, entry("prefers tabs", MemoryCategory.CORE, "style")); + Memory.add(store, entry("prefers spaces", MemoryCategory.CORE, "style")); + + assertEquals(2, store.entries.size(), "add is the escape hatch that does not deduplicate"); + } + + @Test + void coreMemoriesArePreservedInInsertionOrder() { + MemoryStore store = + storeOf( + entry("first", MemoryCategory.CORE), + entry("summary", MemoryCategory.ARCHIVAL), + entry("second", MemoryCategory.CORE)); + + assertEquals( + List.of("first", "second"), + Memory.coreMemories(store).stream().map(memory -> memory.content).toList()); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/ModelNormalizationTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/ModelNormalizationTest.java new file mode 100644 index 000000000..fe6ab8556 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/ModelNormalizationTest.java @@ -0,0 +1,533 @@ +package com.microsoft.prompty; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.microsoft.prompty.model.ApiKeyConnection; +import com.microsoft.prompty.model.Connection; +import com.microsoft.prompty.model.CustomTool; +import com.microsoft.prompty.model.FunctionTool; +import com.microsoft.prompty.model.Property; +import com.microsoft.prompty.model.Prompty; +import com.microsoft.prompty.model.ReplayJournalRecord; +import com.microsoft.prompty.model.SaveContext; +import com.microsoft.prompty.model.SessionEvent; +import com.microsoft.prompty.model.SessionEventType; +import com.microsoft.prompty.model.Tool; +import com.microsoft.prompty.model.ToolResult; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +/** + * Locks in the behaviour that {@code schema/scripts/normalize-java-output.mjs} adds on top of the + * Typra Java emitter output. + * + *

The emitter currently ships several defects that would otherwise make the Java model diverge + * from the C# and Rust runtimes. The generated example suites do not cover them, so these + * assertions guard the normalization pass itself: if a future emitter release changes shape and the + * pass silently stops matching, these tests fail. + */ +class ModelNormalizationTest { + + @Nested + @DisplayName("named-dictionary collections") + class NamedDictionaries { + + @Test + @DisplayName("inputs declared as a name-keyed dict load with the key as name") + void loadsNameKeyedDictionary() { + Prompty prompty = + Prompty.fromJson( + """ + { + "kind": "prompt", + "inputs": { + "firstName": {"kind": "string", "description": "Given name"}, + "age": {"kind": "integer"} + } + } + """); + + assertNotNull(prompty.inputs); + assertEquals(2, prompty.inputs.size()); + assertEquals("firstName", prompty.inputs.get(0).name); + assertEquals("string", prompty.inputs.get(0).kind); + assertEquals("Given name", prompty.inputs.get(0).description); + assertEquals("age", prompty.inputs.get(1).name); + assertEquals("integer", prompty.inputs.get(1).kind); + } + + @Test + @DisplayName("scalar dict values widen through the shorthand property") + void widensScalarDictionaryValues() { + Prompty prompty = + Prompty.fromJson("{\"kind\": \"prompt\", \"inputs\": {\"question\": \"What is 2 + 2?\"}}"); + + // The generated layer only injects the key as `name` and widens the scalar through the + // element's shorthand property, matching Prompty.Core's FunctionTool.LoadParameters. + // Inferring `kind` and populating `default` is the loader's job (spec §4.3 step 6d). + assertNotNull(prompty.inputs); + assertEquals(1, prompty.inputs.size()); + assertEquals("question", prompty.inputs.get(0).name); + assertEquals("What is 2 + 2?", prompty.inputs.get(0).example); + } + + @Test + @DisplayName("the flat list form still loads") + void loadsFlatList() { + Prompty prompty = + Prompty.fromJson( + "{\"kind\": \"prompt\", \"inputs\": [{\"name\": \"firstName\", \"kind\": \"string\"}]}"); + + assertNotNull(prompty.inputs); + assertEquals(1, prompty.inputs.size()); + assertEquals("firstName", prompty.inputs.get(0).name); + } + + @Test + @DisplayName("a nested array value is rejected with an actionable message") + void rejectsNestedArrayValues() { + IllegalArgumentException error = + assertThrows( + IllegalArgumentException.class, + () -> Prompty.fromJson("{\"kind\": \"prompt\", \"inputs\": {\"firstName\": [1, 2]}}")); + + assertTrue(error.getMessage().contains("'inputs'"), error.getMessage()); + assertTrue(error.getMessage().contains("firstName"), error.getMessage()); + // The canonical `recursive-array-valued-entry-rejection` contract requires the + // diagnostic to name the category as well as the path. + assertTrue(error.getMessage().contains("array"), error.getMessage()); + } + + @Test + @DisplayName("an array-valued entry is rejected at every nested collection boundary") + void rejectsNestedArrayValuesRecursively() { + // `recursive-array-valued-entry-rejection` applies at every named-collection + // boundary, not just the top-level one, so the two reachable nested boundaries + // are asserted directly: a collection inside a list element, and one reached + // through a subclass field. + IllegalArgumentException insideListElement = + assertThrows( + IllegalArgumentException.class, + () -> + Prompty.fromJson( + """ + { + "kind": "prompt", + "tools": [{"name": "t", "kind": "function", "parameters": {"toolArg": [1, 2]}}] + } + """)); + + assertTrue(insideListElement.getMessage().contains("'parameters'"), insideListElement.getMessage()); + assertTrue(insideListElement.getMessage().contains("toolArg"), insideListElement.getMessage()); + assertTrue(insideListElement.getMessage().contains("array"), insideListElement.getMessage()); + + IllegalArgumentException throughSubclass = + assertThrows( + IllegalArgumentException.class, + () -> + Prompty.fromJson( + """ + { + "kind": "prompt", + "inputs": {"cfg": {"kind": "object", "properties": {"nestedField": [1, 2]}}} + } + """)); + + assertTrue(throughSubclass.getMessage().contains("'properties'"), throughSubclass.getMessage()); + assertTrue(throughSubclass.getMessage().contains("nestedField"), throughSubclass.getMessage()); + assertTrue(throughSubclass.getMessage().contains("array"), throughSubclass.getMessage()); + } + + @Test + @DisplayName("tool parameters accept the name-keyed dict form") + void loadsToolParameterDictionary() { + Prompty prompty = + Prompty.fromJson( + """ + { + "kind": "prompt", + "tools": [ + { + "kind": "function", + "name": "get_weather", + "parameters": {"city": {"kind": "string", "required": true}} + } + ] + } + """); + + assertNotNull(prompty.tools); + FunctionTool tool = assertInstanceOf(FunctionTool.class, prompty.tools.get(0)); + assertNotNull(tool.parameters); + assertEquals(1, tool.parameters.size()); + assertEquals("city", tool.parameters.get(0).name); + assertEquals("string", tool.parameters.get(0).kind); + } + } + + @Nested + @DisplayName("scalar shorthand dispatch") + class ScalarShorthand { + + @Test + @DisplayName("integral values load as integer, not float") + void loadsIntegerShorthand() { + Property property = Property.fromJson("1"); + + assertEquals("integer", property.kind); + assertEquals(1, property.example); + } + + @Test + @DisplayName("fractional values load as float") + void loadsFloatShorthand() { + Property property = Property.fromJson("1.5"); + + assertEquals("float", property.kind); + assertEquals(1.5f, property.example); + } + + @Test + @DisplayName("booleans and strings keep their own branches") + void loadsOtherScalarShorthands() { + assertEquals("boolean", Property.fromJson("true").kind); + assertEquals("string", Property.fromJson("\"hello\"").kind); + } + } + + @Nested + @DisplayName("discriminated unions") + class DiscriminatedUnions { + + @Test + @DisplayName("an unknown tool kind falls back to CustomTool") + void unknownToolKindFallsBackToCustomTool() { + Tool tool = Tool.fromJson("{\"kind\": \"my_provider\", \"name\": \"whatever\"}"); + + CustomTool custom = assertInstanceOf(CustomTool.class, tool); + assertEquals("my_provider", custom.kind); + assertEquals("whatever", custom.name); + } + + @Test + @DisplayName("an unknown connection kind is rejected") + void unknownConnectionKindThrows() { + assertThrows(IllegalArgumentException.class, () -> Connection.fromJson("{\"kind\": \"nope\"}")); + } + + @Test + @DisplayName("the discriminator is matched case-insensitively") + void discriminatorIsCaseInsensitive() { + assertInstanceOf(ApiKeyConnection.class, Connection.fromJson("{\"kind\": \"Key\"}")); + } + } + + @Nested + @DisplayName("inherited properties") + class Inheritance { + + @Test + @DisplayName("a subclass load() populates base-class fields") + void subclassPopulatesBaseFields() { + FunctionTool tool = + FunctionTool.fromJson( + "{\"kind\": \"function\", \"name\": \"get_weather\", \"description\": \"Look up weather\"}"); + + // `name` and `description` are declared on Tool, not FunctionTool. + assertEquals("get_weather", tool.name); + assertEquals("Look up weather", tool.description); + assertEquals("function", tool.kind); + } + + @Test + @DisplayName("save() round-trips base-class fields") + void saveRoundTripsBaseFields() { + String json = + FunctionTool.fromJson("{\"kind\": \"function\", \"name\": \"get_weather\"}").toJson(); + + assertTrue(json.contains("\"name\""), json); + assertTrue(json.contains("get_weather"), json); + assertTrue(json.contains("function"), json); + } + } + + @Nested + @DisplayName("collection saves") + class CollectionSaves { + + @Test + @DisplayName("named collections save as a name-keyed dictionary by default") + void savesNameKeyedDictionary() { + Prompty prompty = + Prompty.fromJson( + """ + { + "kind": "prompt", + "inputs": { + "firstName": {"kind": "string", "description": "Given name"}, + "age": {"kind": "integer", "description": "Years"} + } + } + """); + + Object saved = prompty.save(new SaveContext()).get("inputs"); + + Map inputs = assertInstanceOf(Map.class, saved); + assertEquals(List.of("firstName", "age"), List.copyOf(inputs.keySet())); + Map first = assertInstanceOf(Map.class, inputs.get("firstName")); + assertEquals("string", first.get("kind")); + assertFalse(first.containsKey("name"), "the key carries the name, so it is not repeated inside"); + } + + @Test + @DisplayName("collectionFormat=array falls back to a flat list that keeps the name") + void savesArrayFormatOnRequest() { + Prompty prompty = + Prompty.fromJson("{\"kind\": \"prompt\", \"inputs\": {\"firstName\": {\"kind\": \"string\"}}}"); + SaveContext context = new SaveContext(); + context.collectionFormat = "array"; + + Object saved = prompty.save(context).get("inputs"); + + List inputs = assertInstanceOf(List.class, saved); + assertEquals(1, inputs.size()); + Map first = assertInstanceOf(Map.class, inputs.get(0)); + assertEquals("firstName", first.get("name")); + assertEquals("string", first.get("kind")); + } + + @Test + @DisplayName("a shorthand input round-trips to its expanded form, as in C# and Rust") + void shorthandExpandsOnSave() { + Prompty prompty = Prompty.fromJson("{\"kind\": \"prompt\", \"inputs\": {\"firstName\": \"Jane\"}}"); + + Map inputs = assertInstanceOf(Map.class, prompty.save(new SaveContext()).get("inputs")); + + // `kind` is a required property, so a saved Property always carries at least + // two keys and the scalar collapse in saveList never applies to it. The + // loader — not the model layer — is what infers the kind (spec 4.3 step 6d). + Map first = assertInstanceOf(Map.class, inputs.get("firstName")); + assertEquals("Jane", first.get("example")); + assertFalse(first.containsKey("name"), "the key carries the name, so it is not repeated inside"); + } + + @Test + @DisplayName("optional properties stay absent unless the wire data supplies them") + void optionalPropertiesAreNotMaterialized() { + Property property = Property.fromJson("{\"kind\": \"string\"}"); + + assertNull(property.description); + assertNull(property.required); + assertNull(property.nullable); + Map saved = property.save(new SaveContext()); + assertEquals(List.of("name", "kind"), List.copyOf(saved.keySet())); + } + + @Test + @DisplayName("unnamed entries fall back to array format rather than collapsing onto one key") + void unnamedEntriesFallBackToArray() { + Prompty prompty = + Prompty.fromJson( + """ + { + "kind": "prompt", + "inputs": [{"kind": "string"}, {"kind": "integer"}] + } + """); + + Object inputs = prompty.save(new SaveContext()).get("inputs"); + + List saved = assertInstanceOf(List.class, inputs, "unnamed members cannot be keyed by name"); + assertEquals(2, saved.size()); + } + + @Test + @DisplayName("duplicate names silently drop an entry on save") + void duplicateNamesCollapseOnSave() { + // Documents a divergence from the canonical `named-collection-lossless-fallback` + // contract, which requires the name-keyed object form only when every name is + // non-empty *and* unique, and the whole ordered array otherwise. Java currently + // applies that fallback to unnamed entries (above) but not to duplicates: both + // entries load, then the save keys them onto one name and the earlier payload is + // overwritten. Invert this test when the emitter detects duplicates before + // building the map -- `saved` should then be a two-element List. + Prompty prompty = + Prompty.fromJson( + """ + { + "kind": "prompt", + "inputs": [ + {"name": "a", "kind": "string"}, + {"name": "a", "kind": "integer"} + ] + } + """); + + assertEquals(2, prompty.inputs.size(), "both entries must survive the load"); + + Object inputs = prompty.save(new SaveContext()).get("inputs"); + + Map saved = assertInstanceOf(Map.class, inputs); + assertEquals(1, saved.size(), "the collision is the divergence being recorded"); + Map survivor = assertInstanceOf(Map.class, saved.get("a")); + assertEquals("integer", survivor.get("kind"), "the later entry overwrites the earlier one"); + } + + @Test + @DisplayName("an eligible collection loaded as a list still saves as a name-keyed object") + void namedListSavesAsObject() { + Prompty prompty = + Prompty.fromJson( + """ + { + "kind": "prompt", + "inputs": [ + {"name": "firstName", "kind": "string"}, + {"name": "age", "kind": "integer"} + ] + } + """); + + Map inputs = assertInstanceOf(Map.class, prompty.save(new SaveContext()).get("inputs")); + + assertEquals(List.of("firstName", "age"), List.copyOf(inputs.keySet())); + assertEquals("string", assertInstanceOf(Map.class, inputs.get("firstName")).get("kind")); + assertEquals("integer", assertInstanceOf(Map.class, inputs.get("age")).get("kind")); + } + + @Test + @DisplayName("a plain Property[] stays an array even when its members are named") + void plainPropertyArraysStayArrays() { + Prompty prompty = + Prompty.fromJson( + """ + { + "kind": "prompt", + "inputs": { + "choice": { + "kind": "union", + "anyOf": [ + {"name": "asText", "kind": "string"}, + {"name": "asNumber", "kind": "integer"} + ] + } + } + } + """); + + Map inputs = assertInstanceOf(Map.class, prompty.save(new SaveContext()).get("inputs")); + Map choice = assertInstanceOf(Map.class, inputs.get("choice")); + + // `anyOf` is declared `Property[]`, not the `Properties` named-collection + // alias, so it is array-only regardless of whether members carry names — + // matching UnionProperty.SaveAnyOf in the C# runtime. + List anyOf = assertInstanceOf(List.class, choice.get("anyOf")); + assertEquals(2, anyOf.size()); + assertEquals("asText", assertInstanceOf(Map.class, anyOf.get(0)).get("name")); + assertEquals("asNumber", assertInstanceOf(Map.class, anyOf.get(1)).get("name")); + } + + @Test + @DisplayName("collections whose element type has no name always save as arrays") + void unnamedElementTypesStayArrays() { + ToolResult result = + ToolResult.fromJson( + """ + { + "callId": "call_1", + "parts": [{"kind": "text", "text": "sunny"}] + } + """); + + Object parts = result.save(new SaveContext()).get("parts"); + + assertInstanceOf(List.class, parts, "ContentPart has no 'name', so object format is not available"); + } + + @Test + @DisplayName("the postSave hook runs exactly once for a derived class") + void postSaveRunsOnceForDerivedClasses() { + AtomicInteger calls = new AtomicInteger(); + SaveContext context = + new SaveContext( + null, + dict -> { + calls.incrementAndGet(); + return dict; + }); + + FunctionTool.fromJson("{\"kind\": \"function\", \"name\": \"get_weather\"}").save(context); + + assertEquals(1, calls.get(), "the base class owns the hook; subclasses must not re-run it"); + } + } + + @Nested + @DisplayName("reserved word renames") + class ReservedWords { + + @Test + @DisplayName("the wire key stays 'default' while the Java field is 'defaultValue'") + void defaultKeepsItsWireName() { + Property property = Property.fromJson("{\"kind\": \"string\", \"default\": \"Jane\"}"); + + assertEquals("Jane", property.defaultValue); + assertTrue(property.toJson().contains("\"default\""), property.toJson()); + } + + @Test + @DisplayName("string data that merely looks like a reserved word is untouched") + void stringDataIsNotRewritten() { + String scope = "https://cognitiveservices.azure.com/.default"; + Connection connection = + Connection.fromJson("{\"kind\": \"oauth\", \"scopes\": [\"" + scope + "\"]}"); + + assertTrue(connection.toJson().contains(scope), connection.toJson()); + } + } + + @Nested + @DisplayName("required enum defaults") + class RequiredEnums { + + @Test + @DisplayName("a required enum omitted from the wire data still round-trips its default") + void requiredEnumDefaultsToFirstConstant() { + SessionEvent event = SessionEvent.fromJson("{}"); + + // C# seeds required enums with the first declared constant + // (SessionEvent.cs:37) and saves them unconditionally, so an omitted + // required enum must never vanish from the saved dictionary. + assertEquals(SessionEventType.SESSION_START, event.type); + assertEquals("session_start", event.save(new SaveContext()).get("type")); + } + + @Test + @DisplayName("an explicit required enum value survives the round trip") + void explicitRequiredEnumIsPreserved() { + SessionEvent event = SessionEvent.fromJson("{\"type\": \"session_end\"}"); + + assertEquals(SessionEventType.SESSION_END, event.type); + assertEquals("session_end", event.save(new SaveContext()).get("type")); + } + + @Test + @DisplayName("an optional enum stays null and is omitted when unset") + void optionalEnumStaysAbsent() { + Map saved = ReplayJournalRecord.fromJson("{\"kind\": \"session\"}").save(new SaveContext()); + + assertEquals("session", saved.get("kind")); + assertFalse(saved.containsKey("status"), saved.toString()); + } + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/ParseVectorsTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/ParseVectorsTest.java new file mode 100644 index 000000000..614809517 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/ParseVectorsTest.java @@ -0,0 +1,203 @@ +package com.microsoft.prompty; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.DynamicTest.dynamicTest; + +import com.microsoft.prompty.model.LoadContext; +import com.microsoft.prompty.model.Message; +import com.microsoft.prompty.model.Prompty; +import com.microsoft.prompty.model.SaveContext; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; + +/** + * Runs the shared {@code spec/vectors/parse} suite against the prompty chat parser. + * + *

Parsing decides where one message ends and the next begins, which is what separates the prompt + * author's instructions from user-supplied content. The vectors cover the boundary cases that decide + * it: markers that are only markers at the start of a line, content that merely looks like a marker, + * and exactly which surrounding whitespace is significant. + */ +class ParseVectorsTest { + + private static final Pattern NONCE_MARKER = + Pattern.compile("__PROMPTY_THREAD_([a-f0-9]+)_(\\w+)__"); + + @TestFactory + List parseVectors() { + Registry.bootstrap(); + List tests = new ArrayList<>(); + for (Map testCase : SpecVectors.readArray("parse/parse_vectors.json")) { + String name = SpecVectors.string(testCase, "name"); + tests.add(dynamicTest(name, () -> runCase(name, testCase))); + } + return tests; + } + + private void runCase(String name, Map testCase) { + Map input = SpecVectors.map(testCase, "input"); + Map expected = SpecVectors.map(testCase, "expected"); + + String rendered = SpecVectors.string(input, "rendered"); + Map threadInputs = SpecVectors.map(input, "thread_inputs"); + + Prompty agent = buildAgent(threadInputs); + List messages = Pipeline.parse(agent, rendered, null); + messages = Threads.expand(messages, noncesIn(rendered, threadInputs), threadInputs); + + SpecVectors.assertMatches("[" + name + "] messages", expected.get("messages"), save(messages)); + } + + /** + * Recover the nonce markers the renderer would have produced. + * + *

These vectors start from already-rendered text, so the markers are read back out of it rather + * than generated — the point of the case is what the pipeline does with a marker, not how the + * marker was chosen. + */ + private static Map noncesIn(String rendered, Map threadInputs) { + Map nonces = new LinkedHashMap<>(); + Matcher matcher = NONCE_MARKER.matcher(rendered); + while (matcher.find()) { + String property = matcher.group(2); + if (threadInputs.containsKey(property)) { + nonces.put(property, matcher.group()); + } + } + return nonces; + } + + private static Prompty buildAgent(Map threadInputs) { + List declared = new ArrayList<>(); + for (String name : threadInputs.keySet()) { + declared.add(Map.of("name", name, "kind", "thread")); + } + + Map data = new LinkedHashMap<>(); + data.put("kind", "prompt"); + data.put("name", "test"); + data.put("model", Map.of("id", "test")); + data.put("instructions", ""); + data.put("inputs", declared); + return Prompty.load(data, new LoadContext(null, null)); + } + + /** Render messages as the vectors describe them: a role plus a list of content parts. */ + private static List save(List messages) { + SaveContext context = new SaveContext(); + context.collectionFormat = "array"; + context.useShorthand = false; + + List saved = new ArrayList<>(messages.size()); + for (Message message : messages) { + Map item = new LinkedHashMap<>(message.save(context)); + // The vectors call the parts "content", which is how every provider wire format names them. + Object parts = item.remove("parts"); + item.put("content", parts); + saved.add(item); + } + return saved; + } + + /** Guards against a silent regression where every vector is skipped. */ + @org.junit.jupiter.api.Test + void suiteIsComplete() { + assertEquals(15, SpecVectors.readArray("parse/parse_vectors.json").size(), "parse vector count"); + } + + /** + * A nonce that happens to be all digits must still validate. + * + *

Nonces are random hex, so roughly one in eighteen thousand comes out as digits only with a + * leading zero. Attribute values are coerced to numbers where they parse, and that coercion drops + * the leading zero, so the nonce no longer matches the one that was stamped and a perfectly + * legitimate render is rejected as a prompt injection. This pins the case that made the agent + * vectors fail intermittently. + */ + @org.junit.jupiter.api.Test + void allDigitNonceWithLeadingZeroStillValidates() { + String nonce = "0123456789012345"; + List messages = + com.microsoft.prompty.parsers.PromptyChatParser.parseChat( + "system[nonce=\"" + nonce + "\"]:\nYou are helpful.", nonce); + + assertEquals(1, messages.size(), "expected the marker to be accepted"); + assertEquals( + null, messages.get(0).metadata.get("nonce"), "the nonce must not leak into metadata"); + } + + /** A marker carrying the wrong nonce is still rejected once coercion is off. */ + @org.junit.jupiter.api.Test + void mismatchedNonceIsStillRejected() { + assertThrows( + InvokerException.class, + () -> + com.microsoft.prompty.parsers.PromptyChatParser.parseChat( + "system[nonce=\"0123456789012345\"]:\nHi", "0123456789012346")); + } + + /** + * Attribute-block forms that the sibling runtimes exercise must keep parsing. + * + *

The boundary pattern had to be rewritten to stop it backtracking (see {@link + * com.microsoft.prompty.parsers.PromptyChatParser}), and the risk of any such rewrite is silently + * narrowing the accepted syntax. These are the forms the Rust, Python and TypeScript parser tests + * rely on, including a quoted value containing a comma — the case a naive fix breaks. + */ + @org.junit.jupiter.params.ParameterizedTest + @org.junit.jupiter.params.provider.ValueSource( + strings = { + "user[name=\"Alice\"]", + "assistant[name=\"Bot\",temperature=0.5]", + "system[nonce=\"abc123\",name=\"Alice\"]", + "user[name=\"Alice, Bob\"]", + "user[a=x]", + "user[a=x , b=y]" + }) + void attributeFormsSharedWithSiblingRuntimesStillParse(String marker) { + List messages = + com.microsoft.prompty.parsers.PromptyChatParser.parseChat(marker + ":\nHello"); + + assertEquals(1, messages.size(), "expected " + marker + " to be recognised as a role marker"); + assertEquals("Hello", textOf(messages.get(0))); + } + + /** + * An unterminated attribute block must not hang the parser. + * + *

Rendered text carries user-supplied values, so a marker-shaped line is attacker-influenced. + * The original pattern let the value class swallow the separator and the closing bracket, so + * {@code user[a= a= a= …} could be split across the attribute loop in exponentially many ways, + * with each further repetition costing about four times the last. + * + *

The size is chosen so the two failure modes stay distinguishable. Eleven repetitions cost the + * old pattern about twelve seconds, so the two-second bound fails clearly on a regression, and the + * rewritten pattern returns in well under a millisecond, so the bound is never close on a healthy + * build. Keeping the size small also matters because {@code assertTimeoutPreemptively} interrupts + * the worker thread, and regex backtracking ignores interrupts — a regression therefore leaks a + * busy thread until the match finishes on its own, which at this size is seconds rather than never. + */ + @org.junit.jupiter.api.Test + void unterminatedAttributeBlockIsRejectedWithoutBacktracking() { + String attack = "user[a=" + " a=".repeat(11); + + List messages = + org.junit.jupiter.api.Assertions.assertTimeoutPreemptively( + java.time.Duration.ofSeconds(2), + () -> com.microsoft.prompty.parsers.PromptyChatParser.parseChat(attack)); + + assertEquals(1, messages.size(), "an unterminated block is not a marker, so it stays content"); + assertEquals(attack, textOf(messages.get(0))); + } + + private static String textOf(Message message) { + return ((com.microsoft.prompty.model.TextPart) message.parts.get(0)).value; + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/RenderVectorsTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/RenderVectorsTest.java new file mode 100644 index 000000000..ef69a37cb --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/RenderVectorsTest.java @@ -0,0 +1,115 @@ +package com.microsoft.prompty; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.DynamicTest.dynamicTest; + +import com.microsoft.prompty.model.LoadContext; +import com.microsoft.prompty.model.Prompty; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; + +/** + * Runs the shared {@code spec/vectors/render} suite against the registered template engines. + * + *

The vectors pin down the details that differ between template libraries and would otherwise + * quietly change a prompt's meaning: whether output is HTML-escaped, what an undefined variable + * renders as, which filters exist, and exactly how much whitespace survives. + */ +class RenderVectorsTest { + + @TestFactory + List renderVectors() { + Registry.bootstrap(); + List tests = new ArrayList<>(); + for (Map testCase : SpecVectors.readArray("render/render_vectors.json")) { + String name = SpecVectors.string(testCase, "name"); + tests.add(dynamicTest(name, () -> runCase(name, testCase))); + } + return tests; + } + + private void runCase(String name, Map testCase) { + Map input = SpecVectors.map(testCase, "input"); + Map expected = SpecVectors.map(testCase, "expected"); + + String template = SpecVectors.string(input, "template"); + String engine = SpecVectors.string(input, "engine"); + Map inputs = SpecVectors.map(input, "inputs"); + + Prompty agent = buildAgent(template, engine, inputs); + String rendered = Pipeline.render(agent, stripKindMarkers(inputs)); + + String exact = SpecVectors.string(expected, "rendered"); + if (exact != null) { + assertEquals(exact, rendered, "[" + name + "] rendered output"); + } + + String pattern = SpecVectors.string(expected, "nonce_pattern"); + if (pattern != null) { + assertTrue( + Pattern.compile(pattern).matcher(rendered).find(), + "[" + name + "] expected output matching /" + pattern + "/, got: " + rendered); + } + } + + /** + * Build an agent whose declared inputs match the vector's values. + * + *

Rich inputs — threads, images, files, audio — never reach the template engine as values; + * they are swapped for markers and spliced back in afterwards. Which inputs are rich is a property + * of the agent's declaration, so a vector that exercises that path signals it with a {@code _kind} + * marker on the value and the declaration is reconstructed from it here. + */ + private static Prompty buildAgent(String template, String engine, Map inputs) { + List declared = new ArrayList<>(); + for (Map.Entry entry : inputs.entrySet()) { + Map property = new LinkedHashMap<>(); + property.put("name", entry.getKey()); + property.put("kind", kindOf(entry.getValue())); + declared.add(property); + } + + Map data = new LinkedHashMap<>(); + data.put("kind", "prompt"); + data.put("name", "test"); + data.put("model", Map.of("id", "test")); + data.put("instructions", template); + data.put("inputs", declared); + data.put("template", Map.of("format", Map.of("kind", engine), "parser", Map.of("kind", "prompty"))); + return Prompty.load(data, new LoadContext(null, null)); + } + + private static String kindOf(Object value) { + if (value instanceof Map map && map.get("_kind") instanceof String kind) { + return kind; + } + return "string"; + } + + /** Unwrap the {@code _kind} marker so rich values arrive as the payload the runtime would see. */ + private static Map stripKindMarkers(Map inputs) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : inputs.entrySet()) { + Object value = entry.getValue(); + if (value instanceof Map map && map.containsKey("_kind")) { + Object messages = map.get("messages"); + result.put(entry.getKey(), messages != null ? messages : map.get("value")); + } else { + result.put(entry.getKey(), value); + } + } + return result; + } + + /** Guards against a silent regression where every vector is skipped. */ + @org.junit.jupiter.api.Test + void suiteIsComplete() { + assertEquals(23, SpecVectors.readArray("render/render_vectors.json").size(), "render vector count"); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/RuntimeTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/RuntimeTest.java new file mode 100644 index 000000000..f33183335 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/RuntimeTest.java @@ -0,0 +1,466 @@ +package com.microsoft.prompty; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import com.microsoft.prompty.model.ErrorChunk; +import com.microsoft.prompty.model.LoadContext; +import com.microsoft.prompty.model.Message; +import com.microsoft.prompty.model.ModelInvocationRequest; +import com.microsoft.prompty.model.ModelInvocationResponse; +import com.microsoft.prompty.model.Prompty; +import com.microsoft.prompty.model.Role; +import com.microsoft.prompty.model.StreamChunk; +import com.microsoft.prompty.model.TextChunk; +import com.microsoft.prompty.model.TextPart; +import com.microsoft.prompty.model.ToolCall; +import com.microsoft.prompty.model.ToolChunk; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Test; + +/** + * Unit coverage for the runtime machinery the shared vectors do not reach. + * + *

The vectors describe the parts of Prompty that must agree across languages. Cancellation, + * streaming accumulation, extension lookup, and structured-result transport are Java's own + * concerns, so they are pinned down here. + */ +class RuntimeTest { + + /** + * A name no real process supplies, used with {@link System#setProperty} to stand in for an ambient + * value. Writing it as a property rather than reaching for a real variable such as {@code PATH} + * keeps these tests deterministic on a machine started with an unusual environment. + */ + private static final String AMBIENT = "PROMPTY_TEST_AMBIENT_VALUE"; + + // ---------------------------------------------------------------- cancellation + + @Test + void cancellationRunsCallbacksOnce() { + CancellationToken token = CancellationToken.create(); + AtomicInteger calls = new AtomicInteger(); + token.onCancel(calls::incrementAndGet); + + assertFalse(token.isCancelled()); + token.cancel(); + token.cancel(); + + assertTrue(token.isCancelled()); + assertEquals(1, calls.get(), "cancelling twice should not run callbacks twice"); + } + + @Test + void cancellationRunsCallbacksRegisteredAfterTheFact() { + CancellationToken token = CancellationToken.create(); + token.cancel(); + + AtomicInteger calls = new AtomicInteger(); + token.onCancel(calls::incrementAndGet); + + assertEquals(1, calls.get(), "a callback registered after cancellation should still run"); + } + + @Test + void cancellationRunsEveryCallbackEvenWhenOneFails() { + CancellationToken token = CancellationToken.create(); + AtomicInteger calls = new AtomicInteger(); + token.onCancel( + () -> { + calls.incrementAndGet(); + throw new IllegalStateException("first callback failed"); + }); + token.onCancel(calls::incrementAndGet); + + assertThrows(IllegalStateException.class, token::cancel); + assertEquals(2, calls.get(), "a failing callback should not strand the others"); + } + + @Test + void uncancellableTokenIsShared() { + assertSame(CancellationToken.none(), CancellationToken.none()); + assertFalse(CancellationToken.none().isCancelled()); + assertThrows(IllegalStateException.class, () -> CancellationToken.none().cancel()); + } + + @Test + void cancelledTokenStopsIteration() { + CancellationToken token = CancellationToken.create(); + Iterator source = List.of(1, 2, 3).iterator(); + Iterator guarded = Streams.cancellable(source, token); + + assertEquals(1, guarded.next()); + token.cancel(); + assertFalse(guarded.hasNext(), "a cancelled stream should stop yielding"); + } + + // ---------------------------------------------------------------- streaming + + @Test + void consumeAccumulatesTextAndToolCalls() { + ToolCall call = new ToolCall(); + call.id = "call_1"; + call.name = "get_weather"; + call.arguments = "{\"city\":\"Oslo\"}"; + + Streams.Consumed consumed = Streams.consume(chunks(text("Hello "), text("world"), tool(call)), null); + + assertEquals("Hello world", consumed.text()); + assertEquals(1, consumed.toolCalls().size()); + assertEquals("get_weather", consumed.toolCalls().get(0).name); + } + + @Test + void consumeStopsAtAnErrorChunk() { + ErrorChunk failure = new ErrorChunk(); + failure.message = "upstream went away"; + + Streams.Consumed consumed = Streams.consume(chunks(text("partial"), failure, text("never")), null); + + assertEquals("partial", consumed.text(), "text after a failure should not be reported as if it arrived"); + } + + @Test + void toolCallDeltasMergeByIndex() { + List chunks = + List.of( + chunkWith(delta(0, "call_1", "get_weather", "{\"ci")), + chunkWith(delta(0, null, null, "ty\":\"Oslo\"}")), + chunkWith(delta(1, "call_2", "get_time", "{}"))); + + List merged = Streams.mergeToolCallDeltas(chunks); + + assertEquals(2, merged.size()); + assertEquals("call_1", merged.get(0).id); + assertEquals("{\"city\":\"Oslo\"}", merged.get(0).arguments, "argument fragments should concatenate in order"); + assertEquals("get_time", merged.get(1).name); + } + + // ---------------------------------------------------------------- registry + + @Test + void unknownInvokerKeyNamesTheGroupAndKey() { + Registry.bootstrap(); + InvokerException error = assertThrows(InvokerException.class, () -> Registry.executor("nonexistent")); + + assertEquals(InvokerException.Kind.NOT_FOUND, error.kind()); + assertTrue(error.getMessage().contains("nonexistent"), error.getMessage()); + assertTrue(error.getMessage().contains("executor"), error.getMessage()); + } + + @Test + void builtInRenderersAndParserAreAvailable() { + Registry.bootstrap(); + assertTrue(Registry.hasRenderer("jinja2")); + assertTrue(Registry.hasRenderer("nunjucks"), "nunjucks is the spec's default format name"); + assertTrue(Registry.hasRenderer("mustache")); + assertTrue(Registry.hasParser("prompty")); + } + + // ---------------------------------------------------------------- environment + + @Test + void explicitValuesOutrankTheAmbientEnvironment() { + // A system property is the ambient layer that a test can actually control: the process + // environment proper cannot be written from inside a JVM. + System.setProperty(AMBIENT, "ambient"); + try { + assertEquals("ambient", Environment.lookup(AMBIENT).orElseThrow()); + + Environment.set(AMBIENT, "explicit"); + assertEquals("explicit", Environment.lookup(AMBIENT).orElseThrow()); + + Environment.clear(AMBIENT); + assertEquals( + "ambient", Environment.lookup(AMBIENT).orElseThrow(), "clearing should restore the fallback"); + } finally { + Environment.clear(AMBIENT); + System.clearProperty(AMBIENT); + } + } + + @Test + void maskingReportsANameAsUnsetEvenWhenTheProcessSuppliesIt() { + // A JVM cannot unset its own environment, so the mask is the only way to express "absent". + System.setProperty(AMBIENT, "ambient"); + try { + assertTrue(Environment.lookup(AMBIENT).isPresent(), "the ambient value should be visible"); + + Environment.mask(AMBIENT); + assertTrue(Environment.lookup(AMBIENT).isEmpty(), "masking should hide the ambient value"); + + Environment.clear(AMBIENT); + assertTrue(Environment.lookup(AMBIENT).isPresent(), "clearing should restore the fallback"); + } finally { + Environment.clear(AMBIENT); + System.clearProperty(AMBIENT); + } + } + + @Test + void maskingAlsoHidesAVariableInheritedFromTheProcess() { + // The system-property case above cannot prove this one: only a real inherited variable + // exercises the last link in the chain, which is the one a host actually wants to suppress. + String inherited = System.getenv().keySet().stream().findFirst().orElse(null); + assumeTrue(inherited != null, "the process was started with an empty environment"); + try { + assertTrue(Environment.lookup(inherited).isPresent(), "the process supplies " + inherited); + Environment.mask(inherited); + assertTrue(Environment.lookup(inherited).isEmpty(), "masking should hide " + inherited); + } finally { + Environment.clear(inherited); + } + assertTrue(Environment.lookup(inherited).isPresent(), "clearing should restore the fallback"); + } + + @Test + void anExplicitValueOutranksAMaskWhicheverOrderTheyArriveIn() { + try { + Environment.mask(AMBIENT); + Environment.set(AMBIENT, "explicit"); + assertEquals( + "explicit", Environment.lookup(AMBIENT).orElseThrow(), "setting a value should lift the mask"); + + Environment.mask(AMBIENT); + assertTrue(Environment.lookup(AMBIENT).isEmpty(), "masking should drop a value set earlier"); + } finally { + Environment.clear(AMBIENT); + } + } + + @Test + void clearAllLiftsMasksAsWellAsValues() { + System.setProperty(AMBIENT, "ambient"); + try { + Environment.mask(AMBIENT); + assertTrue(Environment.lookup(AMBIENT).isEmpty()); + + Environment.clearAll(); + assertTrue(Environment.lookup(AMBIENT).isPresent(), "clearAll should lift the mask too"); + } finally { + Environment.clear(AMBIENT); + System.clearProperty(AMBIENT); + } + } + + // ---------------------------------------------------------------- structured results + + @Test + void structuredResultsSurviveTransport() { + StructuredResult result = new StructuredResult(Map.of("city", "Oslo"), "{\"city\":\"Oslo\"}"); + Object transported = result.toTransport(); + + assertTrue(StructuredResult.isWrapped(transported)); + StructuredResult restored = StructuredResult.fromTransport(transported); + + assertEquals(result.rawJson(), restored.rawJson()); + assertEquals(result.data(), restored.data()); + assertEquals(result.data(), StructuredResult.unwrap(transported)); + } + + @Test + void plainResultsPassThroughUnwrapping() { + assertEquals("just text", StructuredResult.unwrap("just text")); + assertFalse(StructuredResult.isWrapped("just text")); + } + + // ---------------------------------------------------------------- pipeline defaults + + @Test + void pipelineFallsBackToTheSpecDefaults() { + Prompty agent = Prompty.load(Map.of("kind", "prompt", "name", "t"), new LoadContext(null, null)); + + assertEquals("nunjucks", Pipeline.formatKind(agent)); + assertEquals("prompty", Pipeline.parserKind(agent)); + assertEquals("openai", Pipeline.provider(agent)); + assertTrue(Pipeline.isStrict(agent), "injection defence should be on unless a prompt opts out"); + assertFalse(Pipeline.isStreaming(agent)); + } + + @Test + void streamingIsReadFromModelOptions() { + Map data = new LinkedHashMap<>(); + data.put("kind", "prompt"); + data.put("name", "t"); + data.put( + "model", + Map.of("id", "gpt-4", "options", Map.of("additionalProperties", Map.of("stream", true)))); + Prompty agent = Prompty.load(data, new LoadContext(null, null)); + + assertTrue(Pipeline.isStreaming(agent)); + } + + @Test + void missingRequiredInputNamesTheInput() { + Map data = new LinkedHashMap<>(); + data.put("kind", "prompt"); + data.put("name", "t"); + data.put("inputs", List.of(Map.of("name", "city", "kind", "string", "required", true))); + Prompty agent = Prompty.load(data, new LoadContext(null, null)); + + InvokerException error = assertThrows(InvokerException.class, () -> Pipeline.validateInputs(agent, Map.of())); + assertTrue(error.getMessage().contains("city"), error.getMessage()); + } + + // ---------------------------------------------------------------- messages + + @Test + void messageHelpersReadAndWriteTextContent() { + Message message = Messages.user("hello"); + + assertEquals(Role.USER, message.role); + assertEquals("hello", Messages.text(message)); + assertFalse(Messages.hasRichContent(message)); + assertEquals("hello", Messages.toTextContent(message)); + } + + @Test + void richContentIsReportedAsParts() { + Message message = new Message(); + message.role = Role.USER; + message.parts = new ArrayList<>(List.of(Messages.textPart("look at this"), Messages.imagePart("https://x/y.png", null, null))); + + assertTrue(Messages.hasRichContent(message)); + assertTrue(Messages.toTextContent(message) instanceof List, "rich content cannot collapse to a string"); + } + + @Test + void toolResultsCarryTheirCallId() { + Message message = Messages.toolResult("call_1", "sunny"); + + assertEquals(Role.TOOL, message.role); + assertEquals("call_1", Messages.metadata(message).get(Messages.TOOL_CALL_ID)); + assertEquals("sunny", Messages.text(message)); + } + + // ---------------------------------------------------------------- helpers + + private static Iterator chunks(StreamChunk... items) { + return List.of(items).iterator(); + } + + private static TextChunk text(String value) { + TextChunk chunk = new TextChunk(); + chunk.value = value; + return chunk; + } + + private static ToolChunk tool(ToolCall call) { + ToolChunk chunk = new ToolChunk(); + chunk.toolCall = call; + return chunk; + } + + private static Map delta(int index, String id, String name, String arguments) { + Map function = new LinkedHashMap<>(); + if (name != null) { + function.put("name", name); + } + function.put("arguments", arguments); + + Map delta = new LinkedHashMap<>(); + delta.put("index", index); + if (id != null) { + delta.put("id", id); + } + delta.put("function", function); + return delta; + } + + /** Wrap a tool-call delta in the streaming envelope a provider sends it in. */ + private static Map chunkWith(Map toolCallDelta) { + return Map.of("choices", List.of(Map.of("delta", Map.of("tool_calls", List.of(toolCallDelta))))); + } + + /** Keeps the unused-import checker honest about what a text part looks like. */ + @Test + void textPartsCarryTheirValue() { + TextPart part = Messages.textPart("body"); + assertEquals("body", part.value); + } + + // ------------------------------------------------------------- rich inputs + + @Test + void onlyThreadInputsAreMarkedForParseTimeExpansion() { + Prompty agent = agentWithInputs(Map.of("history", "thread", "photo", "image")); + + Nonces.Prepared prepared = Nonces.prepareRenderInputs(agent, Map.of()); + + assertEquals( + Set.of("history", "photo"), prepared.nonces().keySet(), "every rich kind gets a marker"); + assertEquals( + Set.of("history"), + prepared.threadNonces().keySet(), + "image, file and audio markers are resolved during wire conversion, not during parsing"); + } + + @Test + void nonThreadMarkersSurviveExpansion() { + Prompty agent = agentWithInputs(Map.of("photo", "image")); + Nonces.Prepared prepared = Nonces.prepareRenderInputs(agent, Map.of()); + String marker = prepared.nonces().get("photo"); + List messages = List.of(Messages.withText(Role.USER, "Look at " + marker)); + + List expanded = Threads.expand(messages, prepared.threadNonces(), Map.of()); + + assertEquals(1, expanded.size()); + assertTrue( + ((TextPart) expanded.get(0).parts.get(0)).value.contains(marker), + "an image marker must reach the wire layer intact, not be swallowed as empty history"); + } + + // ------------------------------------------------------- turn-engine contract + + @Test + void toolCallRoundsRecordAnAssistantMessage() { + Processor processor = (agent, response) -> response; + Prompty agent = agentWithInputs(Map.of()); + Object output = List.of(Map.of("id", "call_1", "name", "get_weather", "arguments", "{\"city\":\"Oslo\"}")); + + ModelInvocationResponse result = + processor.processWithContext(agent, output, new ModelInvocationRequest()); + + assertEquals(1, result.toolRequests.size()); + assertEquals( + 1, + result.assistantMessages.size(), + "the turn the model took has to appear in the next request's conversation"); + Message assistant = result.assistantMessages.get(0); + assertEquals(Role.ASSISTANT, assistant.role); + assertTrue(Messages.metadata(assistant).get("tool_calls") instanceof List); + } + + @Test + void plainTextRoundsRecordTheAssistantText() { + Processor processor = (agent, response) -> response; + + ModelInvocationResponse result = + processor.processWithContext(agentWithInputs(Map.of()), "hello", new ModelInvocationRequest()); + + assertEquals(1, result.assistantMessages.size()); + assertEquals("hello", Messages.text(result.assistantMessages.get(0))); + assertTrue(result.toolRequests.isEmpty()); + } + + private static Prompty agentWithInputs(Map kindsByName) { + List inputs = new ArrayList<>(); + kindsByName.forEach((name, kind) -> inputs.add(Map.of("name", name, "kind", kind))); + Map data = new LinkedHashMap<>(); + data.put("kind", "prompt"); + data.put("name", "t"); + data.put("model", "gpt-4"); + data.put("inputs", inputs); + return Prompty.load(data, new LoadContext(null, null)); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/SpecVectorsTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/SpecVectorsTest.java new file mode 100644 index 000000000..d7ee46fc8 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/SpecVectorsTest.java @@ -0,0 +1,162 @@ +package com.microsoft.prompty; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.opentest4j.AssertionFailedError; + +/** + * Covers the vector harness itself. + * + *

Every provider suite is only as trustworthy as the comparison behind it, and a comparison that + * silently accepts everything would let all of them pass while the runtimes disagreed. These tests + * exist so the assertions used to grade cross-runtime agreement are themselves graded. + */ +class SpecVectorsTest { + + private static void equivalent(Object expected, Object actual) { + SpecVectors.assertEquivalent("test", expected, actual); + } + + private static void rejects(Object expected, Object actual) { + assertThrows(AssertionFailedError.class, () -> equivalent(expected, actual)); + } + + @Test + void anExtraFieldTheVectorDoesNotDescribeIsRejected() { + // The whole point of the exact comparison: a runtime that adds a field to every request must + // not be able to do so without a single vector noticing. + rejects(Map.of("model", "gpt-4"), Map.of("model", "gpt-4", "surprise", true)); + } + + @Test + void anExtraFieldNestedInsideAnObjectIsAlsoRejected() { + rejects( + Map.of("options", Map.of("temperature", 1)), + Map.of("options", Map.of("temperature", 1, "surprise", true))); + } + + @Test + void anExtraFieldInsideAnArrayElementIsAlsoRejected() { + rejects( + Map.of("messages", List.of(Map.of("role", "user"))), + Map.of("messages", List.of(Map.of("role", "user", "surprise", true)))); + } + + @Test + void aMissingFieldIsRejected() { + rejects(Map.of("model", "gpt-4", "stream", true), Map.of("model", "gpt-4")); + } + + @Test + void aFieldTheVectorStatesAsNullMustStillBePresent() { + // The subset matcher accepts an absent key wherever the vector states an explicit null, so + // without a key-set check this is exactly the case that slips through: the reference + // implementation compares key counts and would reject it. + Map expected = new LinkedHashMap<>(); + expected.put("model", "gpt-4"); + expected.put("response_format", null); + rejects(expected, Map.of("model", "gpt-4")); + } + + @Test + void aNullFieldNestedInsideAnArrayElementMustAlsoBePresent() { + Map block = new LinkedHashMap<>(); + block.put("type", "text"); + block.put("cache_control", null); + rejects( + Map.of("content", List.of(block)), + Map.of("content", List.of(Map.of("type", "text")))); + } + + @Test + void anExactMatchIsAccepted() { + equivalent( + Map.of("model", "gpt-4", "messages", List.of(Map.of("role", "user", "content", "hi"))), + Map.of("model", "gpt-4", "messages", List.of(Map.of("role", "user", "content", "hi")))); + } + + @Test + void keyOrderDoesNotMatter() { + Map expected = new LinkedHashMap<>(); + expected.put("a", 1); + expected.put("b", 2); + Map actual = new LinkedHashMap<>(); + actual.put("b", 2); + actual.put("a", 1); + equivalent(expected, actual); + } + + @Test + void numbersCompareByValueRatherThanByBoxedType() { + // JSON has one number type; the runtimes store them at whatever width the schema declares. + equivalent(Map.of("max_tokens", 4096), Map.of("max_tokens", 4096L)); + } + + @Test + void anExpectedNullAssertsTheFieldIsAbsent() { + Map expected = new LinkedHashMap<>(); + expected.put("response_format", null); + SpecVectors.assertMatches("test", expected, new LinkedHashMap()); + } + + @Test + void theSubsetMatchStillIgnoresFieldsTheVectorDoesNotMention() { + // assertMatches keeps its looser contract, which is what lets a load vector describe one corner + // of a prompt without restating the whole thing. + SpecVectors.assertMatches("test", Map.of("model", "gpt-4"), Map.of("model", "gpt-4", "x", 1)); + } + + @Test + void anExpectedNullIsSatisfiedByAnEmptyCollection() { + // The generated models materialize optional collections, so a `tools` the wire never supplied + // arrives as an empty list and saves as `[]` rather than vanishing. The reference runtimes do + // the same — Rust inserts the saved collection unconditionally and C# guards only on non-null — + // so a vector stating `"tools": null` has to accept it, exactly as Rust's `as_tools()` and + // Python's length check do. + Map expected = new LinkedHashMap<>(); + expected.put("tools", null); + + SpecVectors.assertMatches("test", expected, Map.of("tools", List.of())); + SpecVectors.assertMatches("test", expected, Map.of("tools", Map.of())); + } + + @Test + void anExpectedNullIsStillRejectedByACollectionThatHasEntries() { + // The relaxation must not reach a collection carrying real content: that is a runtime emitting + // something the vector says should not be there, which is the defect this comparison exists to + // catch. + Map expected = new LinkedHashMap<>(); + expected.put("tools", null); + + assertThrows( + AssertionFailedError.class, + () -> SpecVectors.assertMatches("test", expected, Map.of("tools", List.of("search")))); + assertThrows( + AssertionFailedError.class, + () -> SpecVectors.assertMatches("test", expected, Map.of("tools", Map.of("a", 1)))); + } + + @Test + void anExpectedNullIsStillRejectedByAnEmptyString() { + // An empty string is a value, not an absence. A runtime that sends `""` where the vector says + // nothing should be sent is disagreeing, and widening absence to cover it would hide that. + Map expected = new LinkedHashMap<>(); + expected.put("instructions", null); + + assertThrows( + AssertionFailedError.class, + () -> SpecVectors.assertMatches("test", expected, Map.of("instructions", ""))); + } + + @Test + void theKeySetCheckIsNotRelaxedByTheEmptyCollectionAllowance() { + // assertEquivalent still rejects a key the vector never mentions, even when the value is an + // empty collection. Only an explicit `null` in the vector opts into the allowance; a vector that + // omits the key entirely is still asserting the field is not sent at all. + rejects(Map.of("model", "gpt-4"), Map.of("model", "gpt-4", "tools", List.of())); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/engine/EngineTurnVectorsTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/engine/EngineTurnVectorsTest.java new file mode 100644 index 000000000..7dc18ed8d --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/engine/EngineTurnVectorsTest.java @@ -0,0 +1,387 @@ +package com.microsoft.prompty.engine; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.microsoft.prompty.CancellationToken; +import com.microsoft.prompty.Messages; +import com.microsoft.prompty.SpecVectors; +import com.microsoft.prompty.model.DelegatedStateReference; +import com.microsoft.prompty.model.EngineCheckpoint; +import com.microsoft.prompty.model.EngineEvent; +import com.microsoft.prompty.model.EngineEventKind; +import com.microsoft.prompty.model.EnginePermissionDecision; +import com.microsoft.prompty.model.EngineTurnStatus; +import com.microsoft.prompty.model.InvocationContextPortability; +import com.microsoft.prompty.model.InvocationContextState; +import com.microsoft.prompty.model.Message; +import com.microsoft.prompty.model.ModelInvocationRequest; +import com.microsoft.prompty.model.ModelInvocationResponse; +import com.microsoft.prompty.model.ModelToolOutcome; +import com.microsoft.prompty.model.ModelToolRequest; +import com.microsoft.prompty.model.ModelToolResult; +import com.microsoft.prompty.model.Role; +import com.microsoft.prompty.model.TurnCommit; +import com.microsoft.prompty.model.TurnEngineResult; +import com.microsoft.prompty.model.TypraJson; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * Grades the Java turn engine against the shared cross-runtime turn vectors. + * + *

The vectors pin more than the final answer. They pin the exact ordered list of journal events, + * which is what actually proves two runtimes agree: two engines can reach the same output through + * completely different orderings of persistence and effects, and only one of those orderings is + * resumable. + */ +@DisplayName("engine turn vectors") +final class EngineTurnVectorsTest { + + @Test + @DisplayName("every canonical turn vector reproduces Rust's committed turn and event journal") + void everyCanonicalTurnVectorReproducesTheReferenceRun() { + Map file = asMap(SpecVectors.read("engine/turn_vectors.json")); + assertEquals("1", file.get("version"), "vector file version"); + List cases = (List) file.get("cases"); + assertTrue(cases != null && !cases.isEmpty(), "vector file must define cases"); + + for (Object entry : cases) { + runVector(asMap(entry)); + } + } + + private void runVector(Map vector) { + String name = (String) vector.get("name"); + Map expected = asMap(vector.get("expected")); + + ScriptedModel model = new ScriptedModel(responses(vector.get("model"))); + RecordingTools tools = new RecordingTools(stringMap(vector.get("toolOutputs"))); + RecordingDurability durability = new RecordingDurability(); + RecordingPostCommit postCommit = new RecordingPostCommit(); + + TurnEngineEffects effects = + TurnEngineEffects.of(model) + .withPermission(new VectorPermissions(stringSet(vector.get("denyTools")))) + .withTools(tools) + .withDurability(durability) + .withPostCommit(postCommit) + .withClock(() -> "1970-01-01T00:00:00Z") + .withIds(new DefaultPorts.SequentialIds()); + + TurnEngine engine = TurnEngine.of(effects); + + CancellationToken cancellation = CancellationToken.create(); + if (Boolean.TRUE.equals(vector.get("cancelBeforeRun"))) { + cancellation.cancel(); + } + + TurnEngineResult result = + engine.run( + TurnEngineRequest.of( + "session-" + name, "turn-" + name, messages(vector.get("messages"))), + cancellation); + + TurnCommit commit = result.commit; + assertEquals( + EngineTurnStatus.fromValue((String) expected.get("status")), + commit.status, + name + " status"); + SpecVectors.assertEquivalent(name + " output", expected.get("output"), commit.output); + assertEquals(intOf(expected.get("iterations")), commit.iterations, name + " iterations"); + assertEquals( + intOf(expected.get("snapshots")), result.snapshots.size(), name + " snapshot count"); + assertEquals( + intOf(expected.get("toolResults")), result.toolResults.size(), name + " tool result count"); + + List toolOrder = new ArrayList<>(); + for (ModelToolResult toolResult : result.toolResults) { + toolOrder.add(toolResult.requestId); + } + assertEquals(stringList(expected.get("toolResultOrder")), toolOrder, name + " tool order"); + + List expectedPortability = stringList(expected.get("snapshotPortability")); + if (!expectedPortability.isEmpty()) { + List actual = new ArrayList<>(); + for (var snapshot : result.snapshots) { + actual.add(snapshot.contextState.portability.value); + } + assertEquals(expectedPortability, actual, name + " snapshot portability"); + } + + List expectedPrefixes = intList(expected.get("snapshotStablePrefixes")); + if (!expectedPrefixes.isEmpty()) { + List actual = new ArrayList<>(); + for (var snapshot : result.snapshots) { + actual.add(snapshot.stablePrefixMessages); + } + assertEquals(expectedPrefixes, actual, name + " snapshot stable prefixes"); + } + + if (expected.get("commitPortability") != null) { + assertEquals( + expected.get("commitPortability"), + commit.contextState.portability.value, + name + " commit portability"); + } + if (expected.get("delegatedState") != null) { + assertEquals( + intOf(expected.get("delegatedState")), + commit.contextState.delegatedState.size(), + name + " delegated state count"); + } + + // Gapless sequences are the invariant a replay depends on: a hole means an effect was + // journalled that a resumed run would never see. + for (int i = 1; i < durability.events.size(); i++) { + assertEquals( + durability.events.get(i - 1).sequence + 1, + durability.events.get(i).sequence, + name + " event sequence continuity at index " + i); + } + + List expectedKinds = stringList(expected.get("eventKinds")); + if (!expectedKinds.isEmpty()) { + List actualKinds = new ArrayList<>(); + for (EngineEvent event : durability.events) { + actualKinds.add(event.kind.value); + } + assertEquals(expectedKinds, actualKinds, name + " event kinds"); + } + + assertEquals( + commit.status == EngineTurnStatus.SUCCESS ? 1 : 0, + postCommit.effectIds.size(), + name + " post-commit invocations"); + assertEquals( + intOf(expected.get("snapshots")), model.requests.size(), name + " model invocations"); + } + + // ---------------------------------------------------------------- vector decoding + + private static List messages(Object value) { + List messages = new ArrayList<>(); + if (value == null) { + return messages; + } + for (Object entry : (List) value) { + Map map = asMap(entry); + messages.add( + Messages.withText(roleOf((String) map.get("role")), (String) map.get("content"))); + } + return messages; + } + + private static Role roleOf(String role) { + return switch (role == null ? "" : role) { + case "system" -> Role.SYSTEM; + case "assistant" -> Role.ASSISTANT; + case "tool" -> Role.TOOL; + default -> Role.USER; + }; + } + + private static Deque responses(Object value) { + Deque responses = new ArrayDeque<>(); + if (value == null) { + return responses; + } + for (Object entry : (List) value) { + responses.add(response(asMap(entry))); + } + return responses; + } + + private static ModelInvocationResponse response(Map vector) { + ModelInvocationResponse response = new ModelInvocationResponse(); + response.output = vector.get("output"); + + response.assistantMessages = new ArrayList<>(); + if (vector.get("assistant") instanceof String text) { + response.assistantMessages.add(Messages.assistant(text)); + } + + response.toolRequests = new ArrayList<>(); + if (vector.get("tools") instanceof List tools) { + for (Object entry : tools) { + Map map = asMap(entry); + ModelToolRequest request = new ModelToolRequest(); + request.id = (String) map.get("id"); + request.name = (String) map.get("name"); + request.arguments = map.get("arguments"); + response.toolRequests.add(request); + } + } + + Object portability = vector.get("nextPortability"); + Object delegated = vector.get("delegatedState"); + if (portability != null || delegated != null) { + InvocationContextState state = new InvocationContextState(); + state.portability = + portability == null + ? InvocationContextPortability.PORTABLE + : InvocationContextPortability.fromValue((String) portability); + state.delegatedState = new ArrayList<>(); + if (delegated instanceof List references) { + for (Object entry : references) { + Map map = asMap(entry); + DelegatedStateReference reference = new DelegatedStateReference(); + reference.provider = (String) map.get("provider"); + reference.kind = (String) map.get("kind"); + reference.id = (String) map.get("id"); + state.delegatedState.add(reference); + } + } + response.nextContextState = state; + } + return response; + } + + // ---------------------------------------------------------------- vector ports + + /** Replays canned responses in order and records the requests it was given. */ + private static final class ScriptedModel implements Ports.ModelPort { + private final Deque responses; + private final List requests = new ArrayList<>(); + + ScriptedModel(Deque responses) { + this.responses = responses; + } + + @Override + public ModelInvocationResponse invoke( + ModelInvocationRequest request, + CancellationToken cancellation, + Ports.ModelStreamPort stream) { + requests.add(request); + ModelInvocationResponse response = responses.poll(); + if (response == null) { + throw PortException.of("scripted model response exhausted"); + } + return response; + } + } + + /** Denies tools by name, mirroring the vectors' {@code denyTools} list. */ + private static final class VectorPermissions implements Ports.PermissionPort { + private final Set denied; + + VectorPermissions(Set denied) { + this.denied = denied; + } + + @Override + public EnginePermissionDecision authorize( + ModelToolRequest request, CancellationToken cancellation) { + boolean approved = !denied.contains(request.name); + EnginePermissionDecision decision = new EnginePermissionDecision(); + decision.approved = approved; + decision.reason = approved ? null : "denied by vector"; + return decision; + } + } + + /** Returns the vector's canned output for a request id, falling back to its arguments. */ + private static final class RecordingTools implements Ports.ToolPort { + private final Map outputs; + private final List calls = new ArrayList<>(); + + RecordingTools(Map outputs) { + this.outputs = outputs; + } + + @Override + public ModelToolResult execute(ModelToolRequest request, CancellationToken cancellation) { + calls.add(request.id); + ModelToolResult result = new ModelToolResult(); + result.requestId = request.id; + result.name = request.name; + result.outcome = ModelToolOutcome.SUCCESS; + result.output = + outputs.containsKey(request.id) + ? outputs.get(request.id) + : TypraJson.stringify(request.arguments); + return result; + } + } + + /** Captures the journal so the test can assert the exact event ordering. */ + private static final class RecordingDurability implements Ports.DurabilityPort { + private final List events = new ArrayList<>(); + private final List checkpoints = new ArrayList<>(); + + @Override + public void append(EngineEvent event) { + events.add(event); + } + + @Override + public void appendWithCheckpoint(List batch, EngineCheckpoint checkpoint) { + events.addAll(batch); + checkpoints.add(checkpoint); + } + } + + private static final class RecordingPostCommit implements Ports.PostCommitPort { + private final List effectIds = new ArrayList<>(); + + @Override + public void afterCommit(String effectId, TurnCommit commit, CancellationToken cancellation) { + effectIds.add(effectId); + } + } + + // ---------------------------------------------------------------- coercion helpers + + @SuppressWarnings("unchecked") + private static Map asMap(Object value) { + return value == null ? new LinkedHashMap<>() : (Map) value; + } + + private static int intOf(Object value) { + return value == null ? 0 : ((Number) value).intValue(); + } + + private static List stringList(Object value) { + List list = new ArrayList<>(); + if (value instanceof List entries) { + for (Object entry : entries) { + list.add((String) entry); + } + } + return list; + } + + private static List intList(Object value) { + List list = new ArrayList<>(); + if (value instanceof List entries) { + for (Object entry : entries) { + list.add(((Number) entry).intValue()); + } + } + return list; + } + + private static Map stringMap(Object value) { + Map map = new HashMap<>(); + if (value instanceof Map entries) { + for (Map.Entry entry : entries.entrySet()) { + map.put((String) entry.getKey(), (String) entry.getValue()); + } + } + return map; + } + + private static Set stringSet(Object value) { + return new HashSet<>(stringList(value)); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/engine/TurnEngineTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/engine/TurnEngineTest.java new file mode 100644 index 000000000..de943b857 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/engine/TurnEngineTest.java @@ -0,0 +1,1066 @@ +package com.microsoft.prompty.engine; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.microsoft.prompty.CancellationToken; +import com.microsoft.prompty.Messages; +import com.microsoft.prompty.model.EngineCheckpoint; +import com.microsoft.prompty.model.EngineEvent; +import com.microsoft.prompty.model.EngineEventKind; +import com.microsoft.prompty.model.EnginePermissionDecision; +import com.microsoft.prompty.model.EngineTurnStatus; +import com.microsoft.prompty.model.FinalOutputPolicyRequest; +import com.microsoft.prompty.model.FinalOutputPolicyResult; +import com.microsoft.prompty.model.HostPolicyRequest; +import com.microsoft.prompty.model.HostPolicyResult; +import com.microsoft.prompty.model.InvocationContextPortability; +import com.microsoft.prompty.model.InvocationContextState; +import com.microsoft.prompty.model.Message; +import com.microsoft.prompty.model.ModelInvocationContextSnapshot; +import com.microsoft.prompty.model.ModelInvocationRequest; +import com.microsoft.prompty.model.ModelInvocationResponse; +import com.microsoft.prompty.model.ModelToolOutcome; +import com.microsoft.prompty.model.ModelToolRequest; +import com.microsoft.prompty.model.ModelToolResult; +import com.microsoft.prompty.model.TurnCommit; +import com.microsoft.prompty.model.TurnEngineResult; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +/** + * Turn engine behaviour the shared vectors do not reach. + * + *

The vectors grade five happy-ish paths end to end. What they cannot express is what the engine + * does when an effect's outcome is genuinely unknown, when a port itself fails, or when a run is + * resumed from a checkpoint — the paths that decide whether a durable turn is actually safe to + * retry. Those are covered here, mirroring the dedicated tests in the Rust reference. + */ +@DisplayName("turn engine") +final class TurnEngineTest { + + @Nested + @DisplayName("indeterminate effects") + final class IndeterminateEffects { + + @Test + @DisplayName("an unknown tool outcome halts the turn for reconciliation instead of guessing") + void anUnknownToolOutcomeHaltsTheTurnForReconciliation() { + ScriptedModel model = new ScriptedModel(responses(toolCall("call-unknown", "external-write"))); + Recorder recorder = new Recorder(); + TurnEngine engine = + TurnEngine.of( + baseEffects(model, recorder).withTools(new IndeterminateTools())); + + TurnEngineResult result = + engine.run( + TurnEngineRequest.of( + "session-indeterminate", + "turn-indeterminate", + List.of(Messages.user("write externally"))), + CancellationToken.none()); + + assertEquals(EngineTurnStatus.RECONCILIATION_REQUIRED, result.commit.status); + assertEquals(ModelToolOutcome.INDETERMINATE, result.toolResults.get(0).outcome); + assertEquals(1, model.requests.size(), "the model must not be re-invoked"); + assertTrue(recorder.postCommitIds.isEmpty(), "an unreconciled turn must not run post-commit"); + assertEquals("effect_outcome_unknown", errorKind(result.commit)); + + EngineCheckpoint checkpoint = recorder.lastCheckpoint(); + assertTrue(checkpoint.reconciliationRequired, "checkpoint must record the blocked effect"); + assertEquals( + ModelToolOutcome.INDETERMINATE, checkpoint.completedToolResults.get(0).outcome); + } + + @Test + @DisplayName("resuming without resolving the effect stops again without calling the model") + void resumingWithoutResolvingTheEffectStopsAgain() { + EngineCheckpoint checkpoint = blockedCheckpoint(); + + ScriptedModel model = new ScriptedModel(new ArrayDeque<>()); + Recorder recorder = new Recorder(); + TurnEngineResult resumed = + TurnEngine.of(baseEffects(model, recorder).withTools(new IndeterminateTools())) + .run( + TurnEngineRequest.resumeFrom(checkpoint, 3, 20), + CancellationToken.none()); + + assertEquals(EngineTurnStatus.RECONCILIATION_REQUIRED, resumed.commit.status); + assertTrue(model.requests.isEmpty(), "an unresolved effect must not reach the model"); + } + + @Test + @DisplayName("a resolved effect resumes into the next iteration and is visible to the model") + void aResolvedEffectResumesIntoTheNextIteration() { + EngineCheckpoint checkpoint = blockedCheckpoint(); + + ModelToolResult resolved = new ModelToolResult(); + resolved.requestId = "call-unknown"; + resolved.name = "external-write"; + resolved.outcome = ModelToolOutcome.SUCCESS; + resolved.output = "confirmed complete"; + + ScriptedModel model = new ScriptedModel(responses(finalOutput("reconciled"))); + Recorder recorder = new Recorder(); + TurnEngineResult result = + TurnEngine.of(baseEffects(model, recorder).withTools(new IndeterminateTools())) + .run( + TurnEngineRequest.resumeAfterReconciliation(checkpoint, 2, 20, resolved), + CancellationToken.none()); + + assertEquals(EngineTurnStatus.SUCCESS, result.commit.status); + assertEquals( + ModelToolOutcome.SUCCESS, + recorder.checkpoints.get(0).completedToolResults.get(0).outcome, + "the resolved result must replace the indeterminate one"); + assertTrue( + recorder.kinds().contains(EngineEventKind.TOOL_RESULT_RECONCILED.value), + "resolution must be journalled"); + + ModelInvocationContextSnapshot context = model.requests.get(0).context; + assertTrue( + context.messages.stream() + .anyMatch(message -> "confirmed complete".equals(Messages.text(message))), + "the model must read the resolved output, not the 'outcome unknown' placeholder"); + + // The heart of the resume contract: the checkpoint's only outstanding item was the effect + // just resolved, so its iteration is finished. Resuming in place would re-run a tool round + // the host has already paid for. + assertEquals(1, context.iteration, "a resolved effect advances the iteration"); + } + + @Test + @DisplayName("a tool-blocked checkpoint rejects a model-reconciliation resolution") + void aToolBlockedCheckpointRejectsAModelResolution() { + EngineCheckpoint checkpoint = blockedCheckpoint(); + ModelInvocationResponse wrongShape = finalOutput("wrong resolution type"); + + assertThrows( + TurnEngineException.InvalidRequest.class, + () -> + TurnEngineRequest.resumeAfterModelReconciliation( + checkpoint, 3, checkpoint.lastSequence, wrongShape)); + } + + /** Runs a turn to its blocked checkpoint so resume paths have a real one to start from. */ + private EngineCheckpoint blockedCheckpoint() { + Recorder recorder = new Recorder(); + TurnEngine.of( + baseEffects( + new ScriptedModel(responses(toolCall("call-unknown", "external-write"))), + recorder) + .withTools(new IndeterminateTools())) + .run( + TurnEngineRequest.of( + "session-indeterminate", + "turn-indeterminate", + List.of(Messages.user("write externally"))), + CancellationToken.none()); + return recorder.lastCheckpoint(); + } + } + + @Nested + @DisplayName("port failures") + final class PortFailures { + + @Test + @DisplayName("a failing permission port commits a failed turn rather than proceeding") + void aFailingPermissionPortCommitsAFailedTurn() { + Recorder recorder = new Recorder(); + TurnEngineResult result = + TurnEngine.of( + baseEffects( + new ScriptedModel(responses(toolCall("call-permission", "restricted"))), + recorder) + .withPermission( + (request, cancellation) -> { + throw PortException.of("permission port unavailable"); + })) + .run( + TurnEngineRequest.of( + "session-permission-error", + "turn-permission-error", + List.of(Messages.user("authorize"))), + CancellationToken.none()); + + assertEquals(EngineTurnStatus.FAILED, result.commit.status); + assertEquals("permission_error", errorKind(result.commit)); + assertEquals( + EngineEventKind.TURN_FAILED.value, + recorder.kinds().get(recorder.kinds().size() - 1), + "the journal must end on the failure"); + } + + @Test + @DisplayName("an unknown tool is a terminal configuration failure, not a retryable one") + void anUnknownToolIsATerminalConfigurationFailure() { + Recorder recorder = new Recorder(); + TurnEngineResult result = + TurnEngine.of( + baseEffects( + new ScriptedModel(responses(toolCall("call-missing", "missing"))), + recorder) + .withTools( + (request, cancellation) -> { + throw PortException.configuration("unknown tool '" + request.name + "'"); + })) + .run( + TurnEngineRequest.of( + "session-unknown-tool", + "turn-unknown-tool", + List.of(Messages.user("call missing"))), + CancellationToken.none()); + + assertEquals(EngineTurnStatus.FAILED, result.commit.status); + assertEquals("tool_configuration_error", errorKind(result.commit)); + assertTrue(result.toolResults.isEmpty(), "a misconfigured tool produces no result"); + } + } + + @Nested + @DisplayName("cancellation") + final class Cancellation { + + @Test + @DisplayName("cancelling during authorization prevents the tool from running") + void cancellingDuringAuthorizationPreventsTheToolFromRunning() { + Recorder recorder = new Recorder(); + RecordingTools tools = new RecordingTools(); + CancellationToken cancellation = CancellationToken.create(); + + TurnEngineResult result = + TurnEngine.of( + baseEffects( + new ScriptedModel(responses(toolCall("call-cancelled", "write"))), + recorder) + .withTools(tools) + .withPermission( + (request, token) -> { + cancellation.cancel(); + EnginePermissionDecision decision = new EnginePermissionDecision(); + decision.approved = true; + return decision; + })) + .run( + TurnEngineRequest.of( + "session-cancel-permission", + "turn-cancel-permission", + List.of(Messages.user("write"))), + cancellation); + + assertEquals(EngineTurnStatus.CANCELLED, result.commit.status); + assertTrue(tools.calls.isEmpty(), "an approved but cancelled tool must not execute"); + } + } + + @Nested + @DisplayName("host policy") + final class HostPolicy { + + @Test + @DisplayName("a policy rewrite is checkpointed so a resumed run does not apply it twice") + void aPolicyRewriteIsCheckpointed() { + Recorder recorder = new Recorder(); + TurnEngineResult result = + TurnEngine.of( + baseEffects(new ScriptedModel(responses(finalOutput("done"))), recorder) + .withPolicy( + new Ports.HostPolicyPort() { + @Override + public HostPolicyResult beforeModel( + HostPolicyRequest request, CancellationToken cancellation) { + HostPolicyResult policyResult = new HostPolicyResult(); + policyResult.messages = + new ArrayList<>( + List.of( + Messages.system("redacted"), Messages.user("sanitised"))); + policyResult.stablePrefixMessages = 1; + return policyResult; + } + + @Override + public FinalOutputPolicyResult beforeCommit( + FinalOutputPolicyRequest request, CancellationToken cancellation) { + FinalOutputPolicyResult policyResult = new FinalOutputPolicyResult(); + policyResult.output = request.output; + return policyResult; + } + })) + .run( + TurnEngineRequest.of( + "session-policy", "turn-policy", List.of(Messages.user("raw"))), + CancellationToken.none()); + + assertEquals(EngineTurnStatus.SUCCESS, result.commit.status); + assertTrue( + recorder.kinds().contains(EngineEventKind.POLICY_APPLIED.value), + "a rewrite must be journalled"); + + EngineCheckpoint policyCheckpoint = recorder.checkpoints.get(0); + assertTrue( + policyCheckpoint.policyAppliedForIteration, + "the checkpoint must record that the policy already ran"); + assertTrue( + policyCheckpoint.resumeSameIteration, + "the rewrite belongs to the iteration that has not yet invoked the model"); + + // The rewrite must reach the model, not just the journal. + assertEquals( + List.of("redacted", "sanitised"), + result.snapshots.get(0).messages.stream().map(Messages::text).toList()); + recorder.assertGaplessSequences(); + } + + @Test + @DisplayName("a policy that leaves the conversation alone writes no checkpoint") + void aPolicyThatChangesNothingWritesNoCheckpoint() { + Recorder recorder = new Recorder(); + TurnEngine.of(baseEffects(new ScriptedModel(responses(finalOutput("done"))), recorder)) + .run( + TurnEngineRequest.of( + "session-noop-policy", "turn-noop-policy", List.of(Messages.user("raw"))), + CancellationToken.none()); + + assertFalse( + recorder.kinds().contains(EngineEventKind.POLICY_APPLIED.value), + "an unchanged conversation must not cost a checkpoint"); + } + + @Test + @DisplayName("a policy claiming a longer prefix than it returned fails the turn") + void aPolicyClaimingAnOverlongPrefixFailsTheTurn() { + Recorder recorder = new Recorder(); + TurnEngineResult result = + TurnEngine.of( + baseEffects(new ScriptedModel(responses(finalOutput("done"))), recorder) + .withPolicy( + new Ports.HostPolicyPort() { + @Override + public HostPolicyResult beforeModel( + HostPolicyRequest request, CancellationToken cancellation) { + HostPolicyResult policyResult = new HostPolicyResult(); + policyResult.messages = new ArrayList<>(List.of(Messages.user("a"))); + policyResult.stablePrefixMessages = 9; + return policyResult; + } + + @Override + public FinalOutputPolicyResult beforeCommit( + FinalOutputPolicyRequest request, CancellationToken cancellation) { + return new FinalOutputPolicyResult(); + } + })) + .run( + TurnEngineRequest.of( + "session-bad-policy", "turn-bad-policy", List.of(Messages.user("raw"))), + CancellationToken.none()); + + assertEquals(EngineTurnStatus.FAILED, result.commit.status); + assertEquals("policy_error", errorKind(result.commit)); + } + } + + @Nested + @DisplayName("model failures") + final class ModelFailures { + + @Test + @DisplayName("a retryable failure is journalled and the next attempt succeeds") + void aRetryableFailureIsJournalledAndRetried() { + Recorder recorder = new Recorder(); + TurnEngineResult result = + TurnEngine.of( + baseEffects(new FlakyModel(1, finalOutput("recovered")), recorder)) + .run( + TurnEngineRequest.of( + "session-retry", "turn-retry", List.of(Messages.user("hi"))), + CancellationToken.none()); + + assertEquals(EngineTurnStatus.SUCCESS, result.commit.status); + assertEquals("recovered", result.commit.output); + assertEquals( + 2, + recorder.countOf(EngineEventKind.MODEL_INVOCATION_STARTED), + "each attempt must be journalled"); + assertEquals(1, recorder.countOf(EngineEventKind.MODEL_INVOCATION_FAILED)); + assertEquals( + 1, + result.snapshots.size(), + "a retry reuses the prepared context rather than re-preparing it"); + recorder.assertGaplessSequences(); + } + + @Test + @DisplayName("exhausting the attempt budget fails the turn") + void exhaustingTheAttemptBudgetFailsTheTurn() { + Recorder recorder = new Recorder(); + TurnEngineRequest request = + TurnEngineRequest.of("session-exhaust", "turn-exhaust", List.of(Messages.user("hi"))); + request.maxModelAttempts = 2; + + TurnEngineResult result = + TurnEngine.of(baseEffects(new FlakyModel(5, finalOutput("never")), recorder)) + .run(request, CancellationToken.none()); + + assertEquals(EngineTurnStatus.FAILED, result.commit.status); + assertEquals("model_error", errorKind(result.commit)); + assertEquals(2, recorder.countOf(EngineEventKind.MODEL_INVOCATION_STARTED)); + recorder.assertGaplessSequences(); + } + + @Test + @DisplayName("an unknown invocation outcome halts for reconciliation without retrying") + void anUnknownInvocationOutcomeHaltsForReconciliation() { + Recorder recorder = new Recorder(); + TurnEngineResult result = + TurnEngine.of( + baseEffects( + (request, cancellation, stream) -> { + throw PortException.indeterminate( + "request sent but no response read", new LinkedHashMap<>()); + }, + recorder)) + .run( + TurnEngineRequest.of( + "session-model-unknown", + "turn-model-unknown", + List.of(Messages.user("hi"))), + CancellationToken.none()); + + assertEquals(EngineTurnStatus.RECONCILIATION_REQUIRED, result.commit.status); + assertEquals("model_outcome_unknown", errorKind(result.commit)); + assertEquals( + 1, + recorder.countOf(EngineEventKind.MODEL_INVOCATION_STARTED), + "an unknown outcome must not be retried"); + assertTrue( + recorder.kinds().contains(EngineEventKind.MODEL_RECONCILIATION_REQUIRED.value)); + assertTrue(recorder.postCommitIds.isEmpty()); + + EngineCheckpoint checkpoint = recorder.lastCheckpoint(); + assertTrue(checkpoint.reconciliationRequired); + assertNotNull(checkpoint.modelReconciliation, "the host needs the invocation identity"); + recorder.assertGaplessSequences(); + } + + @Test + @DisplayName("resolving an unknown invocation resumes without re-invoking the model") + void resolvingAnUnknownInvocationResumesWithoutReinvoking() { + Recorder blocked = new Recorder(); + TurnEngine.of( + baseEffects( + (request, cancellation, stream) -> { + throw PortException.indeterminate( + "request sent but no response read", new LinkedHashMap<>()); + }, + blocked)) + .run( + TurnEngineRequest.of( + "session-model-unknown", "turn-model-unknown", List.of(Messages.user("hi"))), + CancellationToken.none()); + EngineCheckpoint checkpoint = blocked.lastCheckpoint(); + + ScriptedModel unused = new ScriptedModel(new ArrayDeque<>()); + Recorder recorder = new Recorder(); + TurnEngineResult result = + TurnEngine.of(baseEffects(unused, recorder)) + .run( + TurnEngineRequest.fromResumeAfterModelReconciliation( + resumeContext(checkpoint), finalOutput("resolved")), + CancellationToken.none()); + + assertEquals(EngineTurnStatus.SUCCESS, result.commit.status); + assertEquals("resolved", result.commit.output); + assertTrue( + unused.requests.isEmpty(), + "the host supplied the outcome, so the model must not be called again"); + assertTrue( + recorder.kinds().contains(EngineEventKind.MODEL_INVOCATION_RECONCILED.value), + "the resolution must be journalled"); + recorder.assertGaplessSequences(); + } + + private com.microsoft.prompty.model.ResumeContext resumeContext(EngineCheckpoint checkpoint) { + com.microsoft.prompty.model.ResumeContext resume = + new com.microsoft.prompty.model.ResumeContext(); + resume.checkpoint = checkpoint; + resume.maxIterations = 10; + resume.maxModelAttempts = 3; + resume.lastJournalSequence = checkpoint.lastSequence; + return resume; + } + } + + @Nested + @DisplayName("commit") + final class Commit { + + @Test + @DisplayName("a failing post-commit effect is reported without un-committing the turn") + void aFailingPostCommitEffectIsReportedWithoutUncommittingTheTurn() { + Recorder recorder = new Recorder(); + TurnEngineResult result = + TurnEngine.of( + baseEffects(new ScriptedModel(responses(finalOutput("done"))), recorder) + .withPostCommit( + (effectId, commit, cancellation) -> { + throw PortException.of("notification webhook is down"); + })) + .run( + TurnEngineRequest.of( + "session-post-commit", "turn-post-commit", List.of(Messages.user("hi"))), + CancellationToken.none()); + + assertEquals( + EngineTurnStatus.SUCCESS, + result.commit.status, + "the turn is already durable; a post-commit effect cannot undo it"); + assertNotNull(result.postCommitError, "the failure must still be surfaced to the caller"); + assertTrue(recorder.kinds().contains(EngineEventKind.POST_COMMIT_FAILED.value)); + } + + @Test + @DisplayName("running out of iterations fails the turn") + void runningOutOfIterationsFailsTheTurn() { + Recorder recorder = new Recorder(); + TurnEngineRequest request = + TurnEngineRequest.of("session-budget", "turn-budget", List.of(Messages.user("hi"))); + request.maxIterations = 1; + + TurnEngineResult result = + TurnEngine.of( + baseEffects( + new ScriptedModel( + responses( + toolCall("call-1", "echo"), toolCall("call-2", "echo"))), + recorder) + .withTools(new RecordingTools())) + .run(request, CancellationToken.none()); + + assertEquals(EngineTurnStatus.FAILED, result.commit.status); + assertEquals("max_iterations", errorKind(result.commit)); + } + + @Test + @DisplayName("an engine-assigned run id is not written back onto the caller's request") + void anEngineAssignedRunIdIsNotWrittenBackOntoTheRequest() { + // Rust consumes the request by value, so this can never happen there. Java passes by + // reference, and a request whose run id had been filled in would make a second run + // silently inherit the first run's identity. + TurnEngineRequest request = + TurnEngineRequest.of("session-ids", "turn-ids", List.of(Messages.user("hi"))); + + Recorder recorder = new Recorder(); + TurnEngine.of(baseEffects(new ScriptedModel(responses(finalOutput("done"))), recorder)) + .run(request, CancellationToken.none()); + + assertEquals("", request.runId, "the caller's request must be left untouched"); + assertFalse( + recorder.events.get(0).runId.isEmpty(), "the run itself still gets an identity"); + } + } + + @Nested + @DisplayName("provider state") + final class ProviderState { + + @Test + @DisplayName("a portable response cannot carry delegated provider state") + void aPortableResponseCannotCarryDelegatedState() { + ModelInvocationResponse response = finalOutput("done"); + response.nextContextState = new InvocationContextState(); + response.nextContextState.portability = InvocationContextPortability.PORTABLE; + response.nextContextState.delegatedState = + new ArrayList<>(List.of(new com.microsoft.prompty.model.DelegatedStateReference())); + + TurnEngineResult result = + TurnEngine.of( + baseEffects(new ScriptedModel(responses(response)), new Recorder())) + .run( + TurnEngineRequest.of( + "session-provider", "turn-provider", List.of(Messages.user("hi"))), + CancellationToken.none()); + + assertEquals(EngineTurnStatus.FAILED, result.commit.status); + assertEquals("provider_state_error", errorKind(result.commit)); + } + + @Test + @DisplayName("a delegated response must name the state it delegates to") + void aDelegatedResponseMustNameItsState() { + ModelInvocationResponse response = finalOutput("done"); + response.nextContextState = new InvocationContextState(); + response.nextContextState.portability = InvocationContextPortability.DELEGATED; + response.nextContextState.delegatedState = new ArrayList<>(); + + TurnEngineResult result = + TurnEngine.of( + baseEffects(new ScriptedModel(responses(response)), new Recorder())) + .run( + TurnEngineRequest.of( + "session-provider", "turn-provider", List.of(Messages.user("hi"))), + CancellationToken.none()); + + assertEquals(EngineTurnStatus.FAILED, result.commit.status); + assertEquals("provider_state_error", errorKind(result.commit)); + } + } + + @Nested + @DisplayName("durability failures") + final class DurabilityFailures { + + @Test + @DisplayName("a failed checkpoint write surfaces the state the host must recover") + void aFailedCheckpointWriteSurfacesRecoveryState() { + TurnEngineException.RecoveryRequired failure = + assertThrows( + TurnEngineException.RecoveryRequired.class, + () -> + TurnEngine.of( + TurnEngineEffects.of(new ScriptedModel(responses(finalOutput("done")))) + .withClock(() -> "1970-01-01T00:00:00Z") + .withDurability( + new Ports.DurabilityPort() { + @Override + public void append(EngineEvent event) {} + + @Override + public void appendWithCheckpoint( + List events, EngineCheckpoint checkpoint) { + throw PortException.of("journal is unavailable"); + } + })) + .run( + TurnEngineRequest.of( + "session-durability", + "turn-durability", + List.of(Messages.user("hi"))), + CancellationToken.none())); + + assertNotNull( + failure.checkpoint(), "the host cannot resume without the checkpoint that failed"); + assertNotNull(failure.requestId()); + } + } + + @Nested + @DisplayName("resume arithmetic") + final class ResumeArithmetic { + + @Test + @DisplayName("a checkpoint with nothing outstanding advances to the next iteration") + void aCheckpointWithNothingOutstandingAdvances() { + EngineCheckpoint checkpoint = checkpoint(2); + assertEquals(3, TurnEngineRequest.resumeFrom(checkpoint, 10, 0).startIteration); + } + + @Test + @DisplayName("a checkpoint with outstanding tools resumes in place") + void aCheckpointWithOutstandingToolsResumesInPlace() { + EngineCheckpoint checkpoint = checkpoint(2); + checkpoint.pendingToolRequests = List.of(request("call-1", "echo")); + assertEquals(2, TurnEngineRequest.resumeFrom(checkpoint, 10, 0).startIteration); + } + + @Test + @DisplayName("an unresolved reconciliation keeps the run in its recorded iteration") + void anUnresolvedReconciliationKeepsTheRunInPlace() { + EngineCheckpoint checkpoint = checkpoint(2); + checkpoint.reconciliationRequired = true; + assertEquals(2, TurnEngineRequest.resumeFrom(checkpoint, 10, 0).startIteration); + } + + @Test + @DisplayName("an explicit resume-in-place request overrides the advance") + void anExplicitResumeInPlaceRequestOverridesTheAdvance() { + EngineCheckpoint checkpoint = checkpoint(2); + checkpoint.resumeSameIteration = true; + assertEquals(2, TurnEngineRequest.resumeFrom(checkpoint, 10, 0).startIteration); + } + + @Test + @DisplayName("resolving the last outstanding effect finishes its iteration") + void resolvingTheLastOutstandingEffectFinishesItsIteration() { + // The checkpoint shape written before the conversation batch became explicit state: the + // tool message is already in the conversation, the queue is drained, and the only thing + // holding the turn open is the unresolved effect. Once that effect is resolved the + // iteration is genuinely finished, so the run must advance rather than repeat a tool + // round the host has already paid for. + EngineCheckpoint checkpoint = new EngineCheckpoint(); + checkpoint.sessionId = "session-legacy"; + checkpoint.turnId = "turn-legacy"; + checkpoint.iteration = 2; + checkpoint.lastSequence = 9L; + checkpoint.reconciliationRequired = true; + checkpoint.pendingToolRequests = new ArrayList<>(); + checkpoint.pendingModelResponse = null; + checkpoint.finalOutputReady = false; + checkpoint.messages = + new ArrayList<>( + List.of(Messages.user("write"), Messages.toolResult("call-1", "outcome unknown"))); + checkpoint.stablePrefixMessages = 1; + + ModelToolResult indeterminate = new ModelToolResult(); + indeterminate.requestId = "call-1"; + indeterminate.name = "external-write"; + indeterminate.outcome = ModelToolOutcome.INDETERMINATE; + checkpoint.completedToolResults = new ArrayList<>(List.of(indeterminate)); + + ModelToolResult resolved = new ModelToolResult(); + resolved.requestId = "call-1"; + resolved.name = "external-write"; + resolved.outcome = ModelToolOutcome.SUCCESS; + resolved.output = "confirmed"; + + TurnEngineRequest request = + TurnEngineRequest.resumeAfterReconciliation(checkpoint, 10, 9, resolved); + + assertEquals(3, request.startIteration, "a resolved effect finishes its iteration"); + assertFalse(request.reconciliationRequired); + assertEquals( + "confirmed", + Messages.text(request.messages.get(1)), + "the model must not keep reading the 'outcome unknown' placeholder"); + } + + @Test + @DisplayName("an effect still outstanding alongside it keeps the run in place") + void anEffectStillOutstandingKeepsTheRunInPlace() { + EngineCheckpoint checkpoint = checkpoint(2); + checkpoint.reconciliationRequired = true; + checkpoint.pendingToolRequests = new ArrayList<>(List.of(request("call-2", "echo"))); + checkpoint.messages = + new ArrayList<>( + List.of(Messages.user("write"), Messages.toolResult("call-1", "outcome unknown"))); + + ModelToolResult indeterminate = new ModelToolResult(); + indeterminate.requestId = "call-1"; + indeterminate.name = "external-write"; + indeterminate.outcome = ModelToolOutcome.INDETERMINATE; + checkpoint.completedToolResults = new ArrayList<>(List.of(indeterminate)); + + ModelToolResult resolved = new ModelToolResult(); + resolved.requestId = "call-1"; + resolved.name = "external-write"; + resolved.outcome = ModelToolOutcome.SUCCESS; + resolved.output = "confirmed"; + + assertEquals( + 2, + TurnEngineRequest.resumeAfterReconciliation(checkpoint, 10, 0, resolved).startIteration, + "a queued tool still has to run inside the recorded iteration"); + } + + @Test + @DisplayName("model reconciliation always resumes the recorded iteration") + void modelReconciliationAlwaysResumesTheRecordedIteration() { EngineCheckpoint checkpoint = checkpoint(2); + checkpoint.reconciliationRequired = true; + checkpoint.activeInvocationId = "invocation-1"; + checkpoint.modelReconciliation = new com.microsoft.prompty.model.ModelReconciliationState(); + checkpoint.modelReconciliation.invocationId = "invocation-1"; + + TurnEngineRequest request = + TurnEngineRequest.resumeAfterModelReconciliation( + checkpoint, 10, 0, finalOutput("resolved")); + + assertEquals(2, request.startIteration, "the interrupted invocation is re-run in place"); + assertFalse(request.reconciliationRequired); + } + + @Test + @DisplayName("resuming continues from the journal tail when it extends past the checkpoint") + void resumingContinuesFromTheJournalTail() { + EngineCheckpoint checkpoint = checkpoint(1); + checkpoint.lastSequence = 7L; + assertEquals(12, TurnEngineRequest.resumeFrom(checkpoint, 10, 12).initialSequence); + assertEquals(7, TurnEngineRequest.resumeFrom(checkpoint, 10, 3).initialSequence); + } + + private EngineCheckpoint checkpoint(int iteration) { + EngineCheckpoint checkpoint = new EngineCheckpoint(); + checkpoint.sessionId = "session-resume"; + checkpoint.turnId = "turn-resume"; + checkpoint.iteration = iteration; + checkpoint.lastSequence = 5L; + checkpoint.messages = List.of(Messages.user("hello")); + checkpoint.stablePrefixMessages = 1; + return checkpoint; + } + } + + @Nested + @DisplayName("request validation") + final class RequestValidation { + + @Test + @DisplayName("a portable turn cannot begin holding delegated provider state") + void aPortableTurnCannotBeginHoldingDelegatedState() { + TurnEngineRequest request = + TurnEngineRequest.of("session", "turn", List.of(Messages.user("hi"))); + request.delegatedState = List.of(new com.microsoft.prompty.model.DelegatedStateReference()); + + assertThrows( + TurnEngineException.InvalidRequest.class, + () -> TurnEngine.of(TurnEngineEffects.of(new ScriptedModel(new ArrayDeque<>()))) + .run(request, CancellationToken.none())); + } + + @Test + @DisplayName("a stable prefix cannot claim more messages than the conversation holds") + void aStablePrefixCannotExceedTheConversation() { + TurnEngineRequest request = + TurnEngineRequest.of("session", "turn", List.of(Messages.user("hi"))); + request.stablePrefixMessages = 5; + + assertThrows( + TurnEngineException.InvalidRequest.class, + () -> TurnEngine.of(TurnEngineEffects.of(new ScriptedModel(new ArrayDeque<>()))) + .run(request, CancellationToken.none())); + } + + @Test + @DisplayName("a turn must be allowed at least one model attempt") + void aTurnMustBeAllowedAtLeastOneModelAttempt() { + TurnEngineRequest request = + TurnEngineRequest.of("session", "turn", List.of(Messages.user("hi"))); + request.maxModelAttempts = 0; + + assertThrows( + TurnEngineException.InvalidRequest.class, + () -> TurnEngine.of(TurnEngineEffects.of(new ScriptedModel(new ArrayDeque<>()))) + .run(request, CancellationToken.none())); + } + } + + @Nested + @DisplayName("snapshot validation") + final class SnapshotValidation { + + @Test + @DisplayName("a snapshot cannot claim a stable prefix longer than its own messages") + void aSnapshotCannotClaimAnOverlongStablePrefix() { + ModelInvocationContextSnapshot snapshot = snapshot(); + snapshot.stablePrefixMessages = 4; + assertThrows(ContextException.class, () -> Snapshots.validate(snapshot)); + } + + @Test + @DisplayName("a portable snapshot cannot carry delegated provider state") + void aPortableSnapshotCannotCarryDelegatedState() { + ModelInvocationContextSnapshot snapshot = snapshot(); + snapshot.contextState.delegatedState = + List.of(new com.microsoft.prompty.model.DelegatedStateReference()); + assertThrows(ContextException.class, () -> Snapshots.validate(snapshot)); + } + + @Test + @DisplayName("a delegated snapshot must name the state it delegates to") + void aDelegatedSnapshotMustNameItsState() { + ModelInvocationContextSnapshot snapshot = snapshot(); + snapshot.contextState.portability = InvocationContextPortability.DELEGATED; + assertThrows(ContextException.class, () -> Snapshots.validate(snapshot)); + } + + @Test + @DisplayName("a well-formed snapshot validates") + void aWellFormedSnapshotValidates() { + Snapshots.validate(snapshot()); + } + + private ModelInvocationContextSnapshot snapshot() { + ModelInvocationContextSnapshot snapshot = new ModelInvocationContextSnapshot(); + snapshot.id = "context:invocation-1"; + snapshot.messages = new ArrayList<>(List.of(Messages.user("hello"))); + snapshot.stablePrefixMessages = 1; + snapshot.iteration = 0; + snapshot.contextState = new InvocationContextState(); + snapshot.contextState.portability = InvocationContextPortability.PORTABLE; + snapshot.contextState.delegatedState = new ArrayList<>(); + return snapshot; + } + } + + // ---------------------------------------------------------------- shared fixtures + + private static TurnEngineEffects baseEffects(Ports.ModelPort model, Recorder recorder) { + return TurnEngineEffects.of(model) + .withDurability(recorder) + .withPostCommit(recorder) + .withClock(() -> "1970-01-01T00:00:00Z") + .withTools(new RecordingTools()); + } + + private static Deque responses(ModelInvocationResponse... responses) { + return new ArrayDeque<>(List.of(responses)); + } + + private static ModelInvocationResponse toolCall(String id, String name) { + ModelInvocationResponse response = new ModelInvocationResponse(); + response.assistantMessages = new ArrayList<>(); + response.toolRequests = new ArrayList<>(List.of(request(id, name))); + return response; + } + + private static ModelToolRequest request(String id, String name) { + ModelToolRequest request = new ModelToolRequest(); + request.id = id; + request.name = name; + return request; + } + + private static ModelInvocationResponse finalOutput(String output) { + ModelInvocationResponse response = new ModelInvocationResponse(); + response.output = output; + response.assistantMessages = new ArrayList<>(); + response.toolRequests = new ArrayList<>(); + return response; + } + + @SuppressWarnings("unchecked") + private static String errorKind(TurnCommit commit) { + assertNotNull(commit.output, "a non-success commit must explain itself"); + return (String) ((Map) commit.output).get("errorKind"); + } + + /** A model whose responses are scripted up front, recording what it was asked. */ + private static final class ScriptedModel implements Ports.ModelPort { + private final Deque responses; + private final List requests = new ArrayList<>(); + + ScriptedModel(Deque responses) { + this.responses = responses; + } + + @Override + public ModelInvocationResponse invoke( + ModelInvocationRequest request, + CancellationToken cancellation, + Ports.ModelStreamPort stream) { + requests.add(request); + ModelInvocationResponse response = responses.poll(); + if (response == null) { + throw PortException.of("scripted model response exhausted"); + } + return response; + } + } + + /** A tool port whose effect always lands in an unknown state. */ + private static final class IndeterminateTools implements Ports.ToolPort { + @Override + public ModelToolResult execute(ModelToolRequest request, CancellationToken cancellation) { + throw PortException.indeterminate( + "external write acknowledged but not confirmed", new LinkedHashMap<>()); + } + } + + /** A tool port that echoes its arguments and records the order it was called in. */ + private static final class RecordingTools implements Ports.ToolPort { + private final List calls = new ArrayList<>(); + + @Override + public ModelToolResult execute(ModelToolRequest request, CancellationToken cancellation) { + calls.add(request.id); + ModelToolResult result = new ModelToolResult(); + result.requestId = request.id; + result.name = request.name; + result.outcome = ModelToolOutcome.SUCCESS; + result.output = "ok"; + return result; + } + } + + /** A model that fails a fixed number of times before returning its scripted response. */ + private static final class FlakyModel implements Ports.ModelPort { + private final ModelInvocationResponse response; + private int failuresRemaining; + + FlakyModel(int failuresRemaining, ModelInvocationResponse response) { + this.failuresRemaining = failuresRemaining; + this.response = response; + } + + @Override + public ModelInvocationResponse invoke( + ModelInvocationRequest request, + CancellationToken cancellation, + Ports.ModelStreamPort stream) { + if (failuresRemaining > 0) { + failuresRemaining--; + throw PortException.of("transient upstream failure"); + } + return response; + } + } + + /** Captures the journal, checkpoints, and post-commit effects of a run. */ + private static final class Recorder implements Ports.DurabilityPort, Ports.PostCommitPort { + private final List events = new ArrayList<>(); + private final List checkpoints = new ArrayList<>(); + private final List postCommitIds = new ArrayList<>(); + + @Override + public void append(EngineEvent event) { + events.add(event); + } + + @Override + public void appendWithCheckpoint(List batch, EngineCheckpoint checkpoint) { + events.addAll(batch); + checkpoints.add(checkpoint); + } + + @Override + public void afterCommit(String effectId, TurnCommit commit, CancellationToken cancellation) { + postCommitIds.add(effectId); + } + + List kinds() { + List kinds = new ArrayList<>(); + for (EngineEvent event : events) { + kinds.add(event.kind.value); + } + return kinds; + } + + EngineCheckpoint lastCheckpoint() { + assertFalse(checkpoints.isEmpty(), "the run wrote no checkpoint"); + return checkpoints.get(checkpoints.size() - 1); + } + + int countOf(EngineEventKind kind) { + int count = 0; + for (EngineEvent event : events) { + if (event.kind == kind) { + count++; + } + } + return count; + } + + /** + * A hole in the journal means an effect was recorded that a resumed run would never replay, + * so gaplessness is the invariant every durable path has to hold. + */ + void assertGaplessSequences() { + for (int i = 1; i < events.size(); i++) { + assertEquals( + events.get(i - 1).sequence + 1, + events.get(i).sequence, + "event sequence continuity at index " + i); + } + } + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/harness/ReplayVectorsTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/harness/ReplayVectorsTest.java new file mode 100644 index 000000000..1ef02456f --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/harness/ReplayVectorsTest.java @@ -0,0 +1,518 @@ +package com.microsoft.prompty.harness; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.microsoft.prompty.SpecVectors; +import com.microsoft.prompty.model.Checkpoint; +import com.microsoft.prompty.model.EngineCheckpoint; +import com.microsoft.prompty.model.EngineEvent; +import com.microsoft.prompty.model.EngineEventKind; +import com.microsoft.prompty.model.HostToolRequest; +import com.microsoft.prompty.model.RunTurnRequest; +import com.microsoft.prompty.model.RunTurnResult; +import com.microsoft.prompty.model.RunTurnStatus; +import com.microsoft.prompty.model.TurnModelRequest; +import com.microsoft.prompty.model.TurnModelResponse; +import com.microsoft.prompty.model.TurnOptions; +import com.microsoft.prompty.model.TypraJson; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; +import org.junit.jupiter.api.io.TempDir; + +/** + * Grades the reference turn runner against the shared replay vectors. + * + *

The vectors are normalized journal strings rather than raw records, so they assert the shape a + * host observes — which events fire, in what order, carrying which identifying detail — without + * pinning timestamps or ids that differ legitimately between runtimes. + */ +class ReplayVectorsTest { + + private static final String VECTORS = "harness/replay_vectors.json"; + + @TempDir Path tempDir; + + @TestFactory + List replayVectors() throws IOException { + String clock = string(suite(), "clock"); + String sessionId = string(suite(), "sessionId"); + String turnId = string(suite(), "turnId"); + + List tests = new ArrayList<>(); + for (Object raw : (List) suite().get("scenarios")) { + @SuppressWarnings("unchecked") + Map scenario = (Map) raw; + String name = string(scenario, "name"); + tests.add( + DynamicTest.dynamicTest( + name, () -> runScenario(scenario, name, clock, sessionId, turnId))); + } + assertFalse(tests.isEmpty(), "replay vectors must not be empty"); + return tests; + } + + private void runScenario( + Map scenario, String name, String clock, String sessionId, String turnId) + throws IOException { + Path journalPath = tempDir.resolve(name + ".jsonl"); + Outcome outcome = run(scenario, name, clock, sessionId, turnId, journalPath); + + @SuppressWarnings("unchecked") + List expected = (List) scenario.get("expected"); + assertEquals(expected, normalizeJournal(journalPath), "normalized journal for " + name); + } + + /** Runs one scenario end to end and returns everything a caller can observe. */ + private Outcome run( + Map scenario, + String name, + String clock, + String sessionId, + String turnId, + Path journalPath) { + CollectingEventSink sink = new CollectingEventSink(); + JsonlEventJournalWriter journal = new JsonlEventJournalWriter(journalPath); + InMemoryCheckpointStore checkpoints = new InMemoryCheckpointStore(); + + boolean denied = "permission_denied".equals(name); + var resolver = + denied + ? new DenyAllPermissionResolver() + : (com.microsoft.prompty.model.PermissionResolver) new AllowAllPermissionResolver(); + + FunctionHostToolExecutor tools = + new FunctionHostToolExecutor() + .with( + "add", + (arguments, request) -> { + double a = number(arguments.get("a")); + double b = number(arguments.get("b")); + return new LinkedHashMap(Map.of("sum", a + b)); + }) + .with( + "fail", + (arguments, request) -> { + throw new IllegalStateException("tool exploded"); + }); + + ReferenceTurnRunner runner = + new ReferenceTurnRunner( + sink, + journal, + checkpoints, + resolver, + tools, + request -> modelFor(name, request), + ReferenceTurnRunner.fixedClock(clock), + ReferenceTurnRunner.sequentialIds()); + + RunTurnRequest request = new RunTurnRequest(); + request.sessionId = sessionId; + request.turnId = turnId; + @SuppressWarnings("unchecked") + Map inputs = (Map) scenario.get("inputs"); + request.inputs = inputs == null ? Map.of() : inputs; + + TurnOptions options = new TurnOptions(); + Object maxIterations = scenario.get("maxIterations"); + options.maxIterations = maxIterations == null ? 3 : (int) number(maxIterations); + request.options = options; + + return new Outcome(runner.run(request), sink, checkpoints); + } + + private record Outcome( + RunTurnResult result, CollectingEventSink sink, InMemoryCheckpointStore checkpoints) {} + + /** + * The scripted model each scenario runs against. + * + *

Deliberately trivial: the vectors grade the runner's projection and durability ordering, so + * anything the model decides for itself would be noise in the comparison. + */ + private static TurnModelResponse modelFor(String scenario, TurnModelRequest request) { + TurnModelResponse response = new TurnModelResponse(); + + if ("no_tool".equals(scenario)) { + Object name = request.inputs == null ? null : request.inputs.get("name"); + response.output = new LinkedHashMap(Map.of("text", "hello " + name)); + response.checkpointState = new LinkedHashMap<>(Map.of("stable", true)); + return response; + } + + if (request.iteration == 0) { + HostToolRequest tool = new HostToolRequest(); + tool.requestId = "exec-1"; + tool.toolCallId = "call-1"; + tool.toolName = "tool_failure".equals(scenario) ? "fail" : "add"; + tool.arguments = new LinkedHashMap<>(Map.of("a", 2, "b", 3)); + response.toolRequests = List.of(tool); + response.checkpointState = new LinkedHashMap<>(Map.of("stable", true)); + return response; + } + + // Later iterations echo what the tool round produced, so a mis-threaded result shows up as a + // wrong answer rather than being silently discarded. + Map output = new LinkedHashMap<>(); + List results = + request.toolResults == null ? List.of() : request.toolResults; + if (!results.isEmpty()) { + var first = results.get(0); + output.put("toolResult", first.result); + output.put("errorKind", first.errorKind); + } + response.output = output; + response.checkpointState = new LinkedHashMap<>(Map.of("stable", true)); + return response; + } + + /** + * Collapses a journal file into the comparable strings the vectors declare. + * + *

Timestamps and generated ids are dropped on purpose: they are correct-by-construction here + * and differ between runtimes for reasons that say nothing about behaviour. + */ + private static List normalizeJournal(Path path) throws IOException { + List normalized = new ArrayList<>(); + for (String line : Files.readAllLines(path, StandardCharsets.UTF_8)) { + if (line.isBlank()) { + continue; + } + Map record = asMap(TypraJson.parse(line)); + String kind = String.valueOf(record.get("kind")); + switch (kind) { + case "summary" -> { + Map summary = asMap(record.get("summary")); + normalized.add( + "summary:" + + summary.get("sessionId") + + ":" + + summary.get("status") + + ":turns=" + + intOf(summary.get("turns")) + + ":checkpoints=" + + intOf(summary.get("checkpoints"))); + } + case "session" -> { + Map event = asMap(record.get("event")); + Map payload = asMap(event.get("payload")); + String type = String.valueOf(event.get("type")); + StringBuilder text = + new StringBuilder("session:") + .append(type) + .append(':') + .append(event.get("sessionId")) + .append(':') + .append(event.get("turnId")); + if ("session_end".equals(type)) { + text.append(':').append(payload.get("status")); + } + normalized.add(text.toString()); + } + case "turn" -> { + Map event = asMap(record.get("event")); + Map payload = asMap(event.get("payload")); + String type = String.valueOf(event.get("type")); + StringBuilder text = + new StringBuilder("turn:") + .append(type) + .append(':') + .append(intOf(event.get("iteration"))); + switch (type) { + case "permission_requested" -> text.append(':').append(payload.get("requestId")); + case "permission_completed" -> text.append(':').append(payload.get("approved")); + case "tool_execution_start" -> text.append(':').append(payload.get("toolName")); + case "tool_execution_complete", "tool_result" -> { + text.append(':').append(payload.get("toolName")); + text.append(':').append(payload.get("success")); + if (payload.get("errorKind") != null) { + text.append(':').append(payload.get("errorKind")); + } + } + case "error" -> text.append(':').append(payload.get("errorKind")); + case "turn_end" -> text.append(':').append(payload.get("status")); + default -> { + // Every other turn event is identified by type and iteration alone. + } + } + normalized.add(text.toString()); + } + default -> throw new IllegalStateException("unknown journal record kind: " + kind); + } + } + return normalized; + } + + @Test + void runnerEmitsJournalsAndCheckpoints() throws IOException { + Path journalPath = tempDir.resolve("direct.jsonl"); + Outcome outcome = + run( + Map.of("name", "tool_success"), + "tool_success", + string(suite(), "clock"), + "session-1", + "turn-1", + journalPath); + + assertEquals(RunTurnStatus.SUCCESS, outcome.result().status); + assertEquals(2, outcome.result().iterations, "one tool round plus the answering iteration"); + + List saved = outcome.checkpoints().listCheckpoints("session-1"); + assertEquals(2, saved.size(), "a checkpoint per completed model invocation"); + assertEquals("turn-1-checkpoint-0", saved.get(0).id); + assertEquals("turn-1-checkpoint-1", saved.get(1).id); + assertEquals(1, saved.get(0).checkpointNumber); + assertEquals( + Boolean.TRUE, + saved.get(0).state.get("stable"), + "the model's own checkpoint state is merged into the saved state"); + + assertEquals( + List.of("session_start", "checkpoint_created", "checkpoint_created", "session_end"), + outcome.sink().sessionEvents().stream().map(event -> event.type.value).toList()); + + assertTrue( + outcome.sink().turnEvents().stream().anyMatch(event -> event.type.value.equals("tool_result")), + "the tool result reaches the host"); + } + + @Test + void toolResultsReachTheCaller() throws IOException { + Outcome outcome = + run( + Map.of("name", "tool_success"), + "tool_success", + string(suite(), "clock"), + "session-1", + "turn-1", + tempDir.resolve("results.jsonl")); + + assertEquals(1, outcome.result().toolResults.size()); + var result = outcome.result().toolResults.get(0); + assertEquals("add", result.toolName); + assertEquals(Boolean.TRUE, result.success); + assertEquals(5.0, number(asMap(result.result).get("sum")), 1e-9); + } + + @Test + void deniedToolNeverReachesTheExecutor() throws IOException { + Outcome outcome = + run( + Map.of("name", "permission_denied"), + "permission_denied", + string(suite(), "clock"), + "session-1", + "turn-1", + tempDir.resolve("denied.jsonl")); + + assertEquals(1, outcome.result().toolResults.size()); + var result = outcome.result().toolResults.get(0); + assertEquals(Boolean.FALSE, result.success); + assertEquals("permission_denied", result.errorKind); + assertTrue( + outcome.sink().turnEvents().stream() + .noneMatch(event -> event.type.value.equals("tool_execution_start")), + "a denied tool must not be started"); + } + + @Test + void maxIterationsReportsAReadableMessage() throws IOException { + Path journalPath = tempDir.resolve("max.jsonl"); + Outcome outcome = + run( + Map.of("name", "max_iterations", "maxIterations", 1), + "max_iterations", + string(suite(), "clock"), + "session-1", + "turn-1", + journalPath); + + assertEquals(RunTurnStatus.ERROR, outcome.result().status); + assertEquals("Maximum turn iterations reached", asMap(outcome.result().output).get("message")); + + // A host reading the journal and a host reading the return value must be told the same thing; + // the normalized vectors compare only the status, so the message itself is checked here. + Map turnEnd = payloadOf(journalPath, "turn", "turn_end"); + assertEquals( + "Maximum turn iterations reached", + asMap(turnEnd.get("response")).get("message"), + "the journal carries the same message the caller receives"); + + Map error = payloadOf(journalPath, "turn", "error"); + assertEquals("max_iterations", error.get("errorKind")); + assertEquals("Maximum turn iterations reached", error.get("message")); + } + + /** The payload of the first journal record of the given kind and type. */ + private static Map payloadOf(Path path, String kind, String type) + throws IOException { + for (String line : Files.readAllLines(path, StandardCharsets.UTF_8)) { + if (line.isBlank()) { + continue; + } + Map record = asMap(TypraJson.parse(line)); + if (!kind.equals(record.get("kind"))) { + continue; + } + Map event = asMap(record.get("event")); + if (type.equals(event.get("type"))) { + return asMap(event.get("payload")); + } + } + throw new AssertionError("no " + kind + " record of type " + type + " in the journal"); + } + + @Test + void messagesUpdatedSurvivesAQuietConversationPort() { + // The engine currently pairs every tool commit with a conversation update, so this rule is + // exercised directly rather than through a turn that cannot separate the two. + EngineCheckpoint settled = new EngineCheckpoint(); + assertTrue( + ReferenceTurnRunner.shouldRecordMessagesUpdated( + List.of(event(EngineEventKind.TOOL_RESULT_COMMITTED)), settled), + "a finished tool round notifies the host even without a conversation update"); + assertTrue( + ReferenceTurnRunner.shouldRecordMessagesUpdated( + List.of(event(EngineEventKind.CONVERSATION_UPDATED)), settled), + "a conversation update notifies the host on its own"); + + EngineCheckpoint awaitingTool = new EngineCheckpoint(); + awaitingTool.pendingToolRequests = List.of(new com.microsoft.prompty.model.ModelToolRequest()); + assertFalse( + ReferenceTurnRunner.shouldRecordMessagesUpdated( + List.of(event(EngineEventKind.TOOL_RESULT_COMMITTED)), awaitingTool), + "a round with a tool still outstanding is not finished"); + + EngineCheckpoint awaitingModel = new EngineCheckpoint(); + awaitingModel.pendingModelResponse = new com.microsoft.prompty.model.ModelInvocationResponse(); + assertFalse( + ReferenceTurnRunner.shouldRecordMessagesUpdated( + List.of(event(EngineEventKind.TOOL_RESULT_COMMITTED)), awaitingModel), + "a round with a model response still to apply is not finished"); + + assertFalse( + ReferenceTurnRunner.shouldRecordMessagesUpdated( + List.of(event(EngineEventKind.POLICY_APPLIED)), settled), + "unrelated events do not notify the host"); + } + + private static EngineEvent event(EngineEventKind kind) { + EngineEvent event = new EngineEvent(); + event.kind = kind; + return event; + } + + @Test + void journalCloseIsIdempotent() { + JsonlEventJournalWriter journal = new JsonlEventJournalWriter(tempDir.resolve("close.jsonl")); + com.microsoft.prompty.model.SessionSummary summary = + new com.microsoft.prompty.model.SessionSummary(); + summary.sessionId = "session-1"; + assertTrue(journal.close(summary), "the first close writes the summary"); + assertFalse(journal.close(summary), "a second close must not append a second summary"); + } + + @Test + void journalRecordsAreLfTerminatedOnEveryPlatform() throws Exception { + // Rust's writeln! always emits LF. Normalization reads lines and so cannot see the terminator, + // which is exactly why this has to be asserted on the raw bytes. + Path path = tempDir.resolve("terminators.jsonl"); + JsonlEventJournalWriter journal = new JsonlEventJournalWriter(path); + com.microsoft.prompty.model.SessionSummary summary = + new com.microsoft.prompty.model.SessionSummary(); + summary.sessionId = "session-1"; + journal.close(summary); + + String raw = Files.readString(path, java.nio.charset.StandardCharsets.UTF_8); + assertFalse(raw.contains("\r"), "journal lines must be LF-terminated, never CRLF"); + assertTrue(raw.endsWith("\n"), "every journal record ends with a newline"); + } + + @Test + void unknownToolIsReportedRatherThanThrown() { + HostToolRequest request = new HostToolRequest(); + request.requestId = "exec-1"; + request.toolName = "nope"; + var result = new FunctionHostToolExecutor().execute(request); + + assertEquals(Boolean.FALSE, result.success); + assertEquals("not_found", result.errorKind); + assertNotNull(result.result); + assertEquals("No host tool registered for 'nope'", asMap(result.result).get("message")); + } + + @Test + void throwingToolIsReportedAsAnException() { + HostToolRequest request = new HostToolRequest(); + request.requestId = "exec-1"; + request.toolName = "boom"; + var result = + new FunctionHostToolExecutor() + .with( + "boom", + (arguments, ignored) -> { + throw new IllegalStateException("tool exploded"); + }) + .execute(request); + + assertEquals(Boolean.FALSE, result.success); + assertEquals("exception", result.errorKind); + assertEquals("tool exploded", asMap(result.result).get("message")); + } + + @Test + void checkpointsListInIdOrder() { + InMemoryCheckpointStore store = new InMemoryCheckpointStore(); + for (String id : List.of("turn-1-checkpoint-2", "turn-1-checkpoint-0", "turn-1-checkpoint-1")) { + Checkpoint checkpoint = new Checkpoint(); + checkpoint.id = id; + checkpoint.sessionId = "session-1"; + store.save(checkpoint); + } + Checkpoint other = new Checkpoint(); + other.id = "turn-1-checkpoint-0"; + other.sessionId = "session-2"; + store.save(other); + + assertEquals( + List.of("turn-1-checkpoint-0", "turn-1-checkpoint-1", "turn-1-checkpoint-2"), + store.listCheckpoints("session-1").stream().map(checkpoint -> checkpoint.id).toList()); + assertEquals(1, store.listCheckpoints("session-2").size(), "sessions do not share checkpoints"); + assertTrue(store.listCheckpoints("absent").isEmpty()); + } + + @SuppressWarnings("unchecked") + private static Map suite() throws IOException { + return (Map) SpecVectors.read(VECTORS); + } + + private static String string(Map source, String key) { + return String.valueOf(source.get(key)); + } + + @SuppressWarnings("unchecked") + private static Map asMap(Object value) { + return value instanceof Map map ? (Map) map : Map.of(); + } + + private static double number(Object value) { + return value instanceof Number n ? n.doubleValue() : 0.0; + } + + private static int intOf(Object value) { + return value instanceof Number n ? n.intValue() : 0; + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AiResourceInfoGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AiResourceInfoGeneratedTest.java new file mode 100644 index 000000000..b11eaa069 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AiResourceInfoGeneratedTest.java @@ -0,0 +1,43 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class AiResourceInfoGeneratedTest { + private AiResourceInfoGeneratedTest() { } + + static void run() { + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnonymousConnectionGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnonymousConnectionGeneratedTest.java new file mode 100644 index 000000000..2d840f738 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnonymousConnectionGeneratedTest.java @@ -0,0 +1,65 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class AnonymousConnectionGeneratedTest { + private AnonymousConnectionGeneratedTest() { } + + static void run() { + + // AnonymousConnection example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "kind": "anonymous", + "endpoint": "https://{your-custom-endpoint}.openai.azure.com/" + } + """; + AnonymousConnection instance1 = AnonymousConnection.fromJson(jsonData1); + assertEquals("anonymous", instance1.kind, "Expected kind"); + assertEquals("https://{your-custom-endpoint}.openai.azure.com/", instance1.endpoint, "Expected endpoint"); + String yamlRoundtrip1 = instance1.toYaml(); + AnonymousConnection fromYaml1 = AnonymousConnection.fromYaml(yamlRoundtrip1); + assertEquals("anonymous", fromYaml1.kind, "Expected kind"); + assertEquals("https://{your-custom-endpoint}.openai.azure.com/", fromYaml1.endpoint, "Expected endpoint"); + AnonymousConnection reloaded1 = AnonymousConnection.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("anonymous", reloaded1.kind, "Expected kind"); + assertEquals("https://{your-custom-endpoint}.openai.azure.com/", reloaded1.endpoint, "Expected endpoint"); + + assertThrows(() -> AnonymousConnection.fromJson("{"), "AnonymousConnection.fromJson should reject malformed JSON"); + + assertThrows(() -> AnonymousConnection.fromYaml(":\n broken"), "AnonymousConnection.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnthropicImageBlockGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnthropicImageBlockGeneratedTest.java new file mode 100644 index 000000000..71255280e --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnthropicImageBlockGeneratedTest.java @@ -0,0 +1,43 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class AnthropicImageBlockGeneratedTest { + private AnthropicImageBlockGeneratedTest() { } + + static void run() { + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnthropicImageSourceGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnthropicImageSourceGeneratedTest.java new file mode 100644 index 000000000..f6901bfb3 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnthropicImageSourceGeneratedTest.java @@ -0,0 +1,65 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class AnthropicImageSourceGeneratedTest { + private AnthropicImageSourceGeneratedTest() { } + + static void run() { + + // AnthropicImageSource example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "media_type": "image/png", + "data": "iVBORw0KGgo..." + } + """; + AnthropicImageSource instance1 = AnthropicImageSource.fromJson(jsonData1); + assertEquals("image/png", instance1.media_type, "Expected media_type"); + assertEquals("iVBORw0KGgo...", instance1.data, "Expected data"); + String yamlRoundtrip1 = instance1.toYaml(); + AnthropicImageSource fromYaml1 = AnthropicImageSource.fromYaml(yamlRoundtrip1); + assertEquals("image/png", fromYaml1.media_type, "Expected media_type"); + assertEquals("iVBORw0KGgo...", fromYaml1.data, "Expected data"); + AnthropicImageSource reloaded1 = AnthropicImageSource.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("image/png", reloaded1.media_type, "Expected media_type"); + assertEquals("iVBORw0KGgo...", reloaded1.data, "Expected data"); + + assertThrows(() -> AnthropicImageSource.fromJson("{"), "AnthropicImageSource.fromJson should reject malformed JSON"); + + assertThrows(() -> AnthropicImageSource.fromYaml(":\n broken"), "AnthropicImageSource.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnthropicMessagesRequestGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnthropicMessagesRequestGeneratedTest.java new file mode 100644 index 000000000..79e453f6b --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnthropicMessagesRequestGeneratedTest.java @@ -0,0 +1,90 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class AnthropicMessagesRequestGeneratedTest { + private AnthropicMessagesRequestGeneratedTest() { } + + static void run() { + + // AnthropicMessagesRequest example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "model": "claude-sonnet-4-20250514", + "max_tokens": 4096, + "system": "You are a helpful assistant.", + "temperature": 0.7, + "top_p": 0.9, + "top_k": 40, + "stop_sequences": [ + "\\n\\nHuman:" + ] + } + """; + AnthropicMessagesRequest instance1 = AnthropicMessagesRequest.fromJson(jsonData1); + assertEquals("claude-sonnet-4-20250514", instance1.model, "Expected model"); + assertEquals(4096, instance1.max_tokens, "Expected max_tokens"); + assertEquals("You are a helpful assistant.", instance1.system, "Expected system"); + assertEquals(0.7, instance1.temperature, "Expected temperature"); + assertEquals(0.9, instance1.top_p, "Expected top_p"); + assertEquals(40, instance1.top_k, "Expected top_k"); + assertEquals(1, instance1.stop_sequences.size(), "Expected stop_sequences size"); + assertEquals("\n\nHuman:", instance1.stop_sequences.get(0), "Expected stop_sequences[0]"); + String yamlRoundtrip1 = instance1.toYaml(); + AnthropicMessagesRequest fromYaml1 = AnthropicMessagesRequest.fromYaml(yamlRoundtrip1); + assertEquals("claude-sonnet-4-20250514", fromYaml1.model, "Expected model"); + assertEquals(4096, fromYaml1.max_tokens, "Expected max_tokens"); + assertEquals("You are a helpful assistant.", fromYaml1.system, "Expected system"); + assertEquals(0.7, fromYaml1.temperature, "Expected temperature"); + assertEquals(0.9, fromYaml1.top_p, "Expected top_p"); + assertEquals(40, fromYaml1.top_k, "Expected top_k"); + assertEquals(1, fromYaml1.stop_sequences.size(), "Expected stop_sequences size"); + assertEquals("\n\nHuman:", fromYaml1.stop_sequences.get(0), "Expected stop_sequences[0]"); + AnthropicMessagesRequest reloaded1 = AnthropicMessagesRequest.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("claude-sonnet-4-20250514", reloaded1.model, "Expected model"); + assertEquals(4096, reloaded1.max_tokens, "Expected max_tokens"); + assertEquals("You are a helpful assistant.", reloaded1.system, "Expected system"); + assertEquals(0.7, reloaded1.temperature, "Expected temperature"); + assertEquals(0.9, reloaded1.top_p, "Expected top_p"); + assertEquals(40, reloaded1.top_k, "Expected top_k"); + assertEquals(1, reloaded1.stop_sequences.size(), "Expected stop_sequences size"); + assertEquals("\n\nHuman:", reloaded1.stop_sequences.get(0), "Expected stop_sequences[0]"); + + assertThrows(() -> AnthropicMessagesRequest.fromJson("{"), "AnthropicMessagesRequest.fromJson should reject malformed JSON"); + + assertThrows(() -> AnthropicMessagesRequest.fromYaml(":\n broken"), "AnthropicMessagesRequest.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnthropicMessagesResponseGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnthropicMessagesResponseGeneratedTest.java new file mode 100644 index 000000000..9cbf2ff4e --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnthropicMessagesResponseGeneratedTest.java @@ -0,0 +1,69 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class AnthropicMessagesResponseGeneratedTest { + private AnthropicMessagesResponseGeneratedTest() { } + + static void run() { + + // AnthropicMessagesResponse example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "id": "msg_01XFDUDYJgAACzvnptvVoYEL", + "model": "claude-sonnet-4-20250514", + "stop_reason": "end_turn" + } + """; + AnthropicMessagesResponse instance1 = AnthropicMessagesResponse.fromJson(jsonData1); + assertEquals("msg_01XFDUDYJgAACzvnptvVoYEL", instance1.id, "Expected id"); + assertEquals("claude-sonnet-4-20250514", instance1.model, "Expected model"); + assertEquals("end_turn", instance1.stop_reason, "Expected stop_reason"); + String yamlRoundtrip1 = instance1.toYaml(); + AnthropicMessagesResponse fromYaml1 = AnthropicMessagesResponse.fromYaml(yamlRoundtrip1); + assertEquals("msg_01XFDUDYJgAACzvnptvVoYEL", fromYaml1.id, "Expected id"); + assertEquals("claude-sonnet-4-20250514", fromYaml1.model, "Expected model"); + assertEquals("end_turn", fromYaml1.stop_reason, "Expected stop_reason"); + AnthropicMessagesResponse reloaded1 = AnthropicMessagesResponse.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("msg_01XFDUDYJgAACzvnptvVoYEL", reloaded1.id, "Expected id"); + assertEquals("claude-sonnet-4-20250514", reloaded1.model, "Expected model"); + assertEquals("end_turn", reloaded1.stop_reason, "Expected stop_reason"); + + assertThrows(() -> AnthropicMessagesResponse.fromJson("{"), "AnthropicMessagesResponse.fromJson should reject malformed JSON"); + + assertThrows(() -> AnthropicMessagesResponse.fromYaml(":\n broken"), "AnthropicMessagesResponse.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnthropicTextBlockGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnthropicTextBlockGeneratedTest.java new file mode 100644 index 000000000..06d6ec1a9 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnthropicTextBlockGeneratedTest.java @@ -0,0 +1,61 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class AnthropicTextBlockGeneratedTest { + private AnthropicTextBlockGeneratedTest() { } + + static void run() { + + // AnthropicTextBlock example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "text": "Hello, how can I help?" + } + """; + AnthropicTextBlock instance1 = AnthropicTextBlock.fromJson(jsonData1); + assertEquals("Hello, how can I help?", instance1.text, "Expected text"); + String yamlRoundtrip1 = instance1.toYaml(); + AnthropicTextBlock fromYaml1 = AnthropicTextBlock.fromYaml(yamlRoundtrip1); + assertEquals("Hello, how can I help?", fromYaml1.text, "Expected text"); + AnthropicTextBlock reloaded1 = AnthropicTextBlock.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("Hello, how can I help?", reloaded1.text, "Expected text"); + + assertThrows(() -> AnthropicTextBlock.fromJson("{"), "AnthropicTextBlock.fromJson should reject malformed JSON"); + + assertThrows(() -> AnthropicTextBlock.fromYaml(":\n broken"), "AnthropicTextBlock.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnthropicToolDefinitionGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnthropicToolDefinitionGeneratedTest.java new file mode 100644 index 000000000..b530b123c --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnthropicToolDefinitionGeneratedTest.java @@ -0,0 +1,65 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class AnthropicToolDefinitionGeneratedTest { + private AnthropicToolDefinitionGeneratedTest() { } + + static void run() { + + // AnthropicToolDefinition example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "name": "get_weather", + "description": "Get the current weather for a city" + } + """; + AnthropicToolDefinition instance1 = AnthropicToolDefinition.fromJson(jsonData1); + assertEquals("get_weather", instance1.name, "Expected name"); + assertEquals("Get the current weather for a city", instance1.description, "Expected description"); + String yamlRoundtrip1 = instance1.toYaml(); + AnthropicToolDefinition fromYaml1 = AnthropicToolDefinition.fromYaml(yamlRoundtrip1); + assertEquals("get_weather", fromYaml1.name, "Expected name"); + assertEquals("Get the current weather for a city", fromYaml1.description, "Expected description"); + AnthropicToolDefinition reloaded1 = AnthropicToolDefinition.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("get_weather", reloaded1.name, "Expected name"); + assertEquals("Get the current weather for a city", reloaded1.description, "Expected description"); + + assertThrows(() -> AnthropicToolDefinition.fromJson("{"), "AnthropicToolDefinition.fromJson should reject malformed JSON"); + + assertThrows(() -> AnthropicToolDefinition.fromYaml(":\n broken"), "AnthropicToolDefinition.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnthropicToolResultBlockGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnthropicToolResultBlockGeneratedTest.java new file mode 100644 index 000000000..2ecc9ead6 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnthropicToolResultBlockGeneratedTest.java @@ -0,0 +1,65 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class AnthropicToolResultBlockGeneratedTest { + private AnthropicToolResultBlockGeneratedTest() { } + + static void run() { + + // AnthropicToolResultBlock example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "tool_use_id": "toolu_01A09q90qw90lq917835lq9", + "content": "72°F and sunny in Paris" + } + """; + AnthropicToolResultBlock instance1 = AnthropicToolResultBlock.fromJson(jsonData1); + assertEquals("toolu_01A09q90qw90lq917835lq9", instance1.tool_use_id, "Expected tool_use_id"); + assertEquals("72°F and sunny in Paris", instance1.content, "Expected content"); + String yamlRoundtrip1 = instance1.toYaml(); + AnthropicToolResultBlock fromYaml1 = AnthropicToolResultBlock.fromYaml(yamlRoundtrip1); + assertEquals("toolu_01A09q90qw90lq917835lq9", fromYaml1.tool_use_id, "Expected tool_use_id"); + assertEquals("72°F and sunny in Paris", fromYaml1.content, "Expected content"); + AnthropicToolResultBlock reloaded1 = AnthropicToolResultBlock.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("toolu_01A09q90qw90lq917835lq9", reloaded1.tool_use_id, "Expected tool_use_id"); + assertEquals("72°F and sunny in Paris", reloaded1.content, "Expected content"); + + assertThrows(() -> AnthropicToolResultBlock.fromJson("{"), "AnthropicToolResultBlock.fromJson should reject malformed JSON"); + + assertThrows(() -> AnthropicToolResultBlock.fromYaml(":\n broken"), "AnthropicToolResultBlock.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnthropicToolUseBlockGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnthropicToolUseBlockGeneratedTest.java new file mode 100644 index 000000000..5d34c8472 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnthropicToolUseBlockGeneratedTest.java @@ -0,0 +1,71 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class AnthropicToolUseBlockGeneratedTest { + private AnthropicToolUseBlockGeneratedTest() { } + + static void run() { + + // AnthropicToolUseBlock example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "id": "toolu_01A09q90qw90lq917835lq9", + "name": "get_weather", + "input": { + "city": "Paris" + } + } + """; + AnthropicToolUseBlock instance1 = AnthropicToolUseBlock.fromJson(jsonData1); + assertEquals("toolu_01A09q90qw90lq917835lq9", instance1.id, "Expected id"); + assertEquals("get_weather", instance1.name, "Expected name"); + assertEquals("Paris", instance1.input.get("city"), "Expected input.city"); + String yamlRoundtrip1 = instance1.toYaml(); + AnthropicToolUseBlock fromYaml1 = AnthropicToolUseBlock.fromYaml(yamlRoundtrip1); + assertEquals("toolu_01A09q90qw90lq917835lq9", fromYaml1.id, "Expected id"); + assertEquals("get_weather", fromYaml1.name, "Expected name"); + assertEquals("Paris", fromYaml1.input.get("city"), "Expected input.city"); + AnthropicToolUseBlock reloaded1 = AnthropicToolUseBlock.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("toolu_01A09q90qw90lq917835lq9", reloaded1.id, "Expected id"); + assertEquals("get_weather", reloaded1.name, "Expected name"); + assertEquals("Paris", reloaded1.input.get("city"), "Expected input.city"); + + assertThrows(() -> AnthropicToolUseBlock.fromJson("{"), "AnthropicToolUseBlock.fromJson should reject malformed JSON"); + + assertThrows(() -> AnthropicToolUseBlock.fromYaml(":\n broken"), "AnthropicToolUseBlock.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnthropicUsageGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnthropicUsageGeneratedTest.java new file mode 100644 index 000000000..cdcf47ff3 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnthropicUsageGeneratedTest.java @@ -0,0 +1,65 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class AnthropicUsageGeneratedTest { + private AnthropicUsageGeneratedTest() { } + + static void run() { + + // AnthropicUsage example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "input_tokens": 150, + "output_tokens": 42 + } + """; + AnthropicUsage instance1 = AnthropicUsage.fromJson(jsonData1); + assertEquals(150, instance1.input_tokens, "Expected input_tokens"); + assertEquals(42, instance1.output_tokens, "Expected output_tokens"); + String yamlRoundtrip1 = instance1.toYaml(); + AnthropicUsage fromYaml1 = AnthropicUsage.fromYaml(yamlRoundtrip1); + assertEquals(150, fromYaml1.input_tokens, "Expected input_tokens"); + assertEquals(42, fromYaml1.output_tokens, "Expected output_tokens"); + AnthropicUsage reloaded1 = AnthropicUsage.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals(150, reloaded1.input_tokens, "Expected input_tokens"); + assertEquals(42, reloaded1.output_tokens, "Expected output_tokens"); + + assertThrows(() -> AnthropicUsage.fromJson("{"), "AnthropicUsage.fromJson should reject malformed JSON"); + + assertThrows(() -> AnthropicUsage.fromYaml(":\n broken"), "AnthropicUsage.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnthropicWireMessageGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnthropicWireMessageGeneratedTest.java new file mode 100644 index 000000000..d190d6e05 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnthropicWireMessageGeneratedTest.java @@ -0,0 +1,61 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class AnthropicWireMessageGeneratedTest { + private AnthropicWireMessageGeneratedTest() { } + + static void run() { + + // AnthropicWireMessage example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "role": "user" + } + """; + AnthropicWireMessage instance1 = AnthropicWireMessage.fromJson(jsonData1); + assertEquals("user", instance1.role, "Expected role"); + String yamlRoundtrip1 = instance1.toYaml(); + AnthropicWireMessage fromYaml1 = AnthropicWireMessage.fromYaml(yamlRoundtrip1); + assertEquals("user", fromYaml1.role, "Expected role"); + AnthropicWireMessage reloaded1 = AnthropicWireMessage.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("user", reloaded1.role, "Expected role"); + + assertThrows(() -> AnthropicWireMessage.fromJson("{"), "AnthropicWireMessage.fromJson should reject malformed JSON"); + + assertThrows(() -> AnthropicWireMessage.fromYaml(":\n broken"), "AnthropicWireMessage.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ApiKeyConnectionGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ApiKeyConnectionGeneratedTest.java new file mode 100644 index 000000000..1625a2afa --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ApiKeyConnectionGeneratedTest.java @@ -0,0 +1,69 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class ApiKeyConnectionGeneratedTest { + private ApiKeyConnectionGeneratedTest() { } + + static void run() { + + // ApiKeyConnection example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "kind": "key", + "endpoint": "https://{your-custom-endpoint}.openai.azure.com/", + "apiKey": "your-api-key" + } + """; + ApiKeyConnection instance1 = ApiKeyConnection.fromJson(jsonData1); + assertEquals("key", instance1.kind, "Expected kind"); + assertEquals("https://{your-custom-endpoint}.openai.azure.com/", instance1.endpoint, "Expected endpoint"); + assertEquals("your-api-key", instance1.apiKey, "Expected apiKey"); + String yamlRoundtrip1 = instance1.toYaml(); + ApiKeyConnection fromYaml1 = ApiKeyConnection.fromYaml(yamlRoundtrip1); + assertEquals("key", fromYaml1.kind, "Expected kind"); + assertEquals("https://{your-custom-endpoint}.openai.azure.com/", fromYaml1.endpoint, "Expected endpoint"); + assertEquals("your-api-key", fromYaml1.apiKey, "Expected apiKey"); + ApiKeyConnection reloaded1 = ApiKeyConnection.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("key", reloaded1.kind, "Expected kind"); + assertEquals("https://{your-custom-endpoint}.openai.azure.com/", reloaded1.endpoint, "Expected endpoint"); + assertEquals("your-api-key", reloaded1.apiKey, "Expected apiKey"); + + assertThrows(() -> ApiKeyConnection.fromJson("{"), "ApiKeyConnection.fromJson should reject malformed JSON"); + + assertThrows(() -> ApiKeyConnection.fromYaml(":\n broken"), "ApiKeyConnection.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ArrayPropertyGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ArrayPropertyGeneratedTest.java new file mode 100644 index 000000000..d8945d169 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ArrayPropertyGeneratedTest.java @@ -0,0 +1,60 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class ArrayPropertyGeneratedTest { + private ArrayPropertyGeneratedTest() { } + + static void run() { + + // ArrayProperty example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "items": { + "kind": "string" + } + } + """; + ArrayProperty instance1 = ArrayProperty.fromJson(jsonData1); + String yamlRoundtrip1 = instance1.toYaml(); + ArrayProperty fromYaml1 = ArrayProperty.fromYaml(yamlRoundtrip1); + ArrayProperty reloaded1 = ArrayProperty.load(instance1.save(new SaveContext()), new LoadContext()); + + assertThrows(() -> ArrayProperty.fromJson("{"), "ArrayProperty.fromJson should reject malformed JSON"); + + assertThrows(() -> ArrayProperty.fromYaml(":\n broken"), "ArrayProperty.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AudioPartGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AudioPartGeneratedTest.java new file mode 100644 index 000000000..ec5ff8ece --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AudioPartGeneratedTest.java @@ -0,0 +1,65 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class AudioPartGeneratedTest { + private AudioPartGeneratedTest() { } + + static void run() { + + // AudioPart example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "source": "https://example.com/audio.wav", + "mediaType": "audio/wav" + } + """; + AudioPart instance1 = AudioPart.fromJson(jsonData1); + assertEquals("https://example.com/audio.wav", instance1.source, "Expected source"); + assertEquals("audio/wav", instance1.mediaType, "Expected mediaType"); + String yamlRoundtrip1 = instance1.toYaml(); + AudioPart fromYaml1 = AudioPart.fromYaml(yamlRoundtrip1); + assertEquals("https://example.com/audio.wav", fromYaml1.source, "Expected source"); + assertEquals("audio/wav", fromYaml1.mediaType, "Expected mediaType"); + AudioPart reloaded1 = AudioPart.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("https://example.com/audio.wav", reloaded1.source, "Expected source"); + assertEquals("audio/wav", reloaded1.mediaType, "Expected mediaType"); + + assertThrows(() -> AudioPart.fromJson("{"), "AudioPart.fromJson should reject malformed JSON"); + + assertThrows(() -> AudioPart.fromYaml(":\n broken"), "AudioPart.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AuthorizationCodeFlowGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AuthorizationCodeFlowGeneratedTest.java new file mode 100644 index 000000000..7f74aba88 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AuthorizationCodeFlowGeneratedTest.java @@ -0,0 +1,43 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class AuthorizationCodeFlowGeneratedTest { + private AuthorizationCodeFlowGeneratedTest() { } + + static void run() { + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/BindingGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/BindingGeneratedTest.java new file mode 100644 index 000000000..37c32429e --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/BindingGeneratedTest.java @@ -0,0 +1,65 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class BindingGeneratedTest { + private BindingGeneratedTest() { } + + static void run() { + + // Binding example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "name": "my-tool", + "input": "input-variable" + } + """; + Binding instance1 = Binding.fromJson(jsonData1); + assertEquals("my-tool", instance1.name, "Expected name"); + assertEquals("input-variable", instance1.input, "Expected input"); + String yamlRoundtrip1 = instance1.toYaml(); + Binding fromYaml1 = Binding.fromYaml(yamlRoundtrip1); + assertEquals("my-tool", fromYaml1.name, "Expected name"); + assertEquals("input-variable", fromYaml1.input, "Expected input"); + Binding reloaded1 = Binding.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("my-tool", reloaded1.name, "Expected name"); + assertEquals("input-variable", reloaded1.input, "Expected input"); + + assertThrows(() -> Binding.fromJson("{"), "Binding.fromJson should reject malformed JSON"); + + assertThrows(() -> Binding.fromYaml(":\n broken"), "Binding.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/CheckpointGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/CheckpointGeneratedTest.java new file mode 100644 index 000000000..18760ad24 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/CheckpointGeneratedTest.java @@ -0,0 +1,81 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class CheckpointGeneratedTest { + private CheckpointGeneratedTest() { } + + static void run() { + + // Checkpoint example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "id": "chk_abc123", + "sessionId": "sess_abc123", + "turnId": "turn_001", + "checkpointNumber": 3, + "title": "Added harness contracts", + "createdAt": "2026-06-09T20:00:00Z" + } + """; + Checkpoint instance1 = Checkpoint.fromJson(jsonData1); + assertEquals("chk_abc123", instance1.id, "Expected id"); + assertEquals("sess_abc123", instance1.sessionId, "Expected sessionId"); + assertEquals("turn_001", instance1.turnId, "Expected turnId"); + assertEquals(3, instance1.checkpointNumber, "Expected checkpointNumber"); + assertEquals("Added harness contracts", instance1.title, "Expected title"); + assertEquals("2026-06-09T20:00:00Z", instance1.createdAt, "Expected createdAt"); + String yamlRoundtrip1 = instance1.toYaml(); + Checkpoint fromYaml1 = Checkpoint.fromYaml(yamlRoundtrip1); + assertEquals("chk_abc123", fromYaml1.id, "Expected id"); + assertEquals("sess_abc123", fromYaml1.sessionId, "Expected sessionId"); + assertEquals("turn_001", fromYaml1.turnId, "Expected turnId"); + assertEquals(3, fromYaml1.checkpointNumber, "Expected checkpointNumber"); + assertEquals("Added harness contracts", fromYaml1.title, "Expected title"); + assertEquals("2026-06-09T20:00:00Z", fromYaml1.createdAt, "Expected createdAt"); + Checkpoint reloaded1 = Checkpoint.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("chk_abc123", reloaded1.id, "Expected id"); + assertEquals("sess_abc123", reloaded1.sessionId, "Expected sessionId"); + assertEquals("turn_001", reloaded1.turnId, "Expected turnId"); + assertEquals(3, reloaded1.checkpointNumber, "Expected checkpointNumber"); + assertEquals("Added harness contracts", reloaded1.title, "Expected title"); + assertEquals("2026-06-09T20:00:00Z", reloaded1.createdAt, "Expected createdAt"); + + assertThrows(() -> Checkpoint.fromJson("{"), "Checkpoint.fromJson should reject malformed JSON"); + + assertThrows(() -> Checkpoint.fromYaml(":\n broken"), "Checkpoint.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/CompactionCompletePayloadGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/CompactionCompletePayloadGeneratedTest.java new file mode 100644 index 000000000..b7087e8ef --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/CompactionCompletePayloadGeneratedTest.java @@ -0,0 +1,69 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class CompactionCompletePayloadGeneratedTest { + private CompactionCompletePayloadGeneratedTest() { } + + static void run() { + + // CompactionCompletePayload example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "removed": 5, + "remaining": 3, + "summaryLength": 1200 + } + """; + CompactionCompletePayload instance1 = CompactionCompletePayload.fromJson(jsonData1); + assertEquals(5, instance1.removed, "Expected removed"); + assertEquals(3, instance1.remaining, "Expected remaining"); + assertEquals(1200, instance1.summaryLength, "Expected summaryLength"); + String yamlRoundtrip1 = instance1.toYaml(); + CompactionCompletePayload fromYaml1 = CompactionCompletePayload.fromYaml(yamlRoundtrip1); + assertEquals(5, fromYaml1.removed, "Expected removed"); + assertEquals(3, fromYaml1.remaining, "Expected remaining"); + assertEquals(1200, fromYaml1.summaryLength, "Expected summaryLength"); + CompactionCompletePayload reloaded1 = CompactionCompletePayload.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals(5, reloaded1.removed, "Expected removed"); + assertEquals(3, reloaded1.remaining, "Expected remaining"); + assertEquals(1200, reloaded1.summaryLength, "Expected summaryLength"); + + assertThrows(() -> CompactionCompletePayload.fromJson("{"), "CompactionCompletePayload.fromJson should reject malformed JSON"); + + assertThrows(() -> CompactionCompletePayload.fromYaml(":\n broken"), "CompactionCompletePayload.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/CompactionConfigGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/CompactionConfigGeneratedTest.java new file mode 100644 index 000000000..1d0f6f622 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/CompactionConfigGeneratedTest.java @@ -0,0 +1,68 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class CompactionConfigGeneratedTest { + private CompactionConfigGeneratedTest() { } + + static void run() { + + // CompactionConfig example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "strategy": "summarize", + "budget": 50000, + "options": { + "preserveSystemMessages": true + } + } + """; + CompactionConfig instance1 = CompactionConfig.fromJson(jsonData1); + assertEquals("summarize", instance1.strategy, "Expected strategy"); + assertEquals(50000, instance1.budget, "Expected budget"); + String yamlRoundtrip1 = instance1.toYaml(); + CompactionConfig fromYaml1 = CompactionConfig.fromYaml(yamlRoundtrip1); + assertEquals("summarize", fromYaml1.strategy, "Expected strategy"); + assertEquals(50000, fromYaml1.budget, "Expected budget"); + CompactionConfig reloaded1 = CompactionConfig.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("summarize", reloaded1.strategy, "Expected strategy"); + assertEquals(50000, reloaded1.budget, "Expected budget"); + + assertThrows(() -> CompactionConfig.fromJson("{"), "CompactionConfig.fromJson should reject malformed JSON"); + + assertThrows(() -> CompactionConfig.fromYaml(":\n broken"), "CompactionConfig.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/CompactionFailedPayloadGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/CompactionFailedPayloadGeneratedTest.java new file mode 100644 index 000000000..dc91f0ae0 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/CompactionFailedPayloadGeneratedTest.java @@ -0,0 +1,61 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class CompactionFailedPayloadGeneratedTest { + private CompactionFailedPayloadGeneratedTest() { } + + static void run() { + + // CompactionFailedPayload example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "message": "Summarization prompt exceeded context window" + } + """; + CompactionFailedPayload instance1 = CompactionFailedPayload.fromJson(jsonData1); + assertEquals("Summarization prompt exceeded context window", instance1.message, "Expected message"); + String yamlRoundtrip1 = instance1.toYaml(); + CompactionFailedPayload fromYaml1 = CompactionFailedPayload.fromYaml(yamlRoundtrip1); + assertEquals("Summarization prompt exceeded context window", fromYaml1.message, "Expected message"); + CompactionFailedPayload reloaded1 = CompactionFailedPayload.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("Summarization prompt exceeded context window", reloaded1.message, "Expected message"); + + assertThrows(() -> CompactionFailedPayload.fromJson("{"), "CompactionFailedPayload.fromJson should reject malformed JSON"); + + assertThrows(() -> CompactionFailedPayload.fromYaml(":\n broken"), "CompactionFailedPayload.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/CompactionStartPayloadGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/CompactionStartPayloadGeneratedTest.java new file mode 100644 index 000000000..d865a4a35 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/CompactionStartPayloadGeneratedTest.java @@ -0,0 +1,61 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class CompactionStartPayloadGeneratedTest { + private CompactionStartPayloadGeneratedTest() { } + + static void run() { + + // CompactionStartPayload example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "droppedCount": 5 + } + """; + CompactionStartPayload instance1 = CompactionStartPayload.fromJson(jsonData1); + assertEquals(5, instance1.droppedCount, "Expected droppedCount"); + String yamlRoundtrip1 = instance1.toYaml(); + CompactionStartPayload fromYaml1 = CompactionStartPayload.fromYaml(yamlRoundtrip1); + assertEquals(5, fromYaml1.droppedCount, "Expected droppedCount"); + CompactionStartPayload reloaded1 = CompactionStartPayload.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals(5, reloaded1.droppedCount, "Expected droppedCount"); + + assertThrows(() -> CompactionStartPayload.fromJson("{"), "CompactionStartPayload.fromJson should reject malformed JSON"); + + assertThrows(() -> CompactionStartPayload.fromYaml(":\n broken"), "CompactionStartPayload.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ConnectionGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ConnectionGeneratedTest.java new file mode 100644 index 000000000..c19dcf988 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ConnectionGeneratedTest.java @@ -0,0 +1,69 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class ConnectionGeneratedTest { + private ConnectionGeneratedTest() { } + + static void run() { + + // Connection example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "kind": "reference", + "authenticationMode": "system", + "usageDescription": "This will allow the agent to respond to an email on your behalf" + } + """; + Connection instance1 = Connection.fromJson(jsonData1); + assertEquals("reference", instance1.kind, "Expected kind"); + assertEquals(AuthenticationMode.fromValue("system"), instance1.authenticationMode, "Expected authenticationMode"); + assertEquals("This will allow the agent to respond to an email on your behalf", instance1.usageDescription, "Expected usageDescription"); + String yamlRoundtrip1 = instance1.toYaml(); + Connection fromYaml1 = Connection.fromYaml(yamlRoundtrip1); + assertEquals("reference", fromYaml1.kind, "Expected kind"); + assertEquals(AuthenticationMode.fromValue("system"), fromYaml1.authenticationMode, "Expected authenticationMode"); + assertEquals("This will allow the agent to respond to an email on your behalf", fromYaml1.usageDescription, "Expected usageDescription"); + Connection reloaded1 = Connection.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("reference", reloaded1.kind, "Expected kind"); + assertEquals(AuthenticationMode.fromValue("system"), reloaded1.authenticationMode, "Expected authenticationMode"); + assertEquals("This will allow the agent to respond to an email on your behalf", reloaded1.usageDescription, "Expected usageDescription"); + + assertThrows(() -> Connection.fromJson("{"), "Connection.fromJson should reject malformed JSON"); + + assertThrows(() -> Connection.fromYaml(":\n broken"), "Connection.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ContentPartGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ContentPartGeneratedTest.java new file mode 100644 index 000000000..9c51dc534 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ContentPartGeneratedTest.java @@ -0,0 +1,43 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class ContentPartGeneratedTest { + private ContentPartGeneratedTest() { } + + static void run() { + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ContextCandidateGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ContextCandidateGeneratedTest.java new file mode 100644 index 000000000..3e57135f6 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ContextCandidateGeneratedTest.java @@ -0,0 +1,65 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class ContextCandidateGeneratedTest { + private ContextCandidateGeneratedTest() { } + + static void run() { + + // ContextCandidate example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "id": "memory:project-plan", + "source": "memory" + } + """; + ContextCandidate instance1 = ContextCandidate.fromJson(jsonData1); + assertEquals("memory:project-plan", instance1.id, "Expected id"); + assertEquals("memory", instance1.source, "Expected source"); + String yamlRoundtrip1 = instance1.toYaml(); + ContextCandidate fromYaml1 = ContextCandidate.fromYaml(yamlRoundtrip1); + assertEquals("memory:project-plan", fromYaml1.id, "Expected id"); + assertEquals("memory", fromYaml1.source, "Expected source"); + ContextCandidate reloaded1 = ContextCandidate.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("memory:project-plan", reloaded1.id, "Expected id"); + assertEquals("memory", reloaded1.source, "Expected source"); + + assertThrows(() -> ContextCandidate.fromJson("{"), "ContextCandidate.fromJson should reject malformed JSON"); + + assertThrows(() -> ContextCandidate.fromYaml(":\n broken"), "ContextCandidate.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ContextRequestGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ContextRequestGeneratedTest.java new file mode 100644 index 000000000..c34f56200 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ContextRequestGeneratedTest.java @@ -0,0 +1,43 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class ContextRequestGeneratedTest { + private ContextRequestGeneratedTest() { } + + static void run() { + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/CustomToolGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/CustomToolGeneratedTest.java new file mode 100644 index 000000000..caf43961e --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/CustomToolGeneratedTest.java @@ -0,0 +1,70 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class CustomToolGeneratedTest { + private CustomToolGeneratedTest() { } + + static void run() { + + // CustomTool example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "connection": { + "kind": "reference" + }, + "options": { + "timeout": 30, + "retries": 3 + } + } + """; + CustomTool instance1 = CustomTool.fromJson(jsonData1); + assertEquals(30, instance1.options.get("timeout"), "Expected options.timeout"); + assertEquals(3, instance1.options.get("retries"), "Expected options.retries"); + String yamlRoundtrip1 = instance1.toYaml(); + CustomTool fromYaml1 = CustomTool.fromYaml(yamlRoundtrip1); + assertEquals(30, fromYaml1.options.get("timeout"), "Expected options.timeout"); + assertEquals(3, fromYaml1.options.get("retries"), "Expected options.retries"); + CustomTool reloaded1 = CustomTool.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals(30, reloaded1.options.get("timeout"), "Expected options.timeout"); + assertEquals(3, reloaded1.options.get("retries"), "Expected options.retries"); + + assertThrows(() -> CustomTool.fromJson("{"), "CustomTool.fromJson should reject malformed JSON"); + + assertThrows(() -> CustomTool.fromYaml(":\n broken"), "CustomTool.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/DelegatedStateReferenceGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/DelegatedStateReferenceGeneratedTest.java new file mode 100644 index 000000000..815efa07e --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/DelegatedStateReferenceGeneratedTest.java @@ -0,0 +1,69 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class DelegatedStateReferenceGeneratedTest { + private DelegatedStateReferenceGeneratedTest() { } + + static void run() { + + // DelegatedStateReference example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "provider": "openai", + "kind": "response", + "id": "resp_abc123" + } + """; + DelegatedStateReference instance1 = DelegatedStateReference.fromJson(jsonData1); + assertEquals("openai", instance1.provider, "Expected provider"); + assertEquals("response", instance1.kind, "Expected kind"); + assertEquals("resp_abc123", instance1.id, "Expected id"); + String yamlRoundtrip1 = instance1.toYaml(); + DelegatedStateReference fromYaml1 = DelegatedStateReference.fromYaml(yamlRoundtrip1); + assertEquals("openai", fromYaml1.provider, "Expected provider"); + assertEquals("response", fromYaml1.kind, "Expected kind"); + assertEquals("resp_abc123", fromYaml1.id, "Expected id"); + DelegatedStateReference reloaded1 = DelegatedStateReference.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("openai", reloaded1.provider, "Expected provider"); + assertEquals("response", reloaded1.kind, "Expected kind"); + assertEquals("resp_abc123", reloaded1.id, "Expected id"); + + assertThrows(() -> DelegatedStateReference.fromJson("{"), "DelegatedStateReference.fromJson should reject malformed JSON"); + + assertThrows(() -> DelegatedStateReference.fromYaml(":\n broken"), "DelegatedStateReference.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/DeviceAuthorizationGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/DeviceAuthorizationGeneratedTest.java new file mode 100644 index 000000000..dc8b9bb47 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/DeviceAuthorizationGeneratedTest.java @@ -0,0 +1,43 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class DeviceAuthorizationGeneratedTest { + private DeviceAuthorizationGeneratedTest() { } + + static void run() { + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/DoneEventPayloadGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/DoneEventPayloadGeneratedTest.java new file mode 100644 index 000000000..0dbf7f49c --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/DoneEventPayloadGeneratedTest.java @@ -0,0 +1,43 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class DoneEventPayloadGeneratedTest { + private DoneEventPayloadGeneratedTest() { } + + static void run() { + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/EngineCheckpointGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/EngineCheckpointGeneratedTest.java new file mode 100644 index 000000000..45a703023 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/EngineCheckpointGeneratedTest.java @@ -0,0 +1,73 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class EngineCheckpointGeneratedTest { + private EngineCheckpointGeneratedTest() { } + + static void run() { + + // EngineCheckpoint example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "id": "ckpt_abc123", + "sessionId": "sess_abc123", + "turnId": "turn_abc123", + "runId": "run_abc123" + } + """; + EngineCheckpoint instance1 = EngineCheckpoint.fromJson(jsonData1); + assertEquals("ckpt_abc123", instance1.id, "Expected id"); + assertEquals("sess_abc123", instance1.sessionId, "Expected sessionId"); + assertEquals("turn_abc123", instance1.turnId, "Expected turnId"); + assertEquals("run_abc123", instance1.runId, "Expected runId"); + String yamlRoundtrip1 = instance1.toYaml(); + EngineCheckpoint fromYaml1 = EngineCheckpoint.fromYaml(yamlRoundtrip1); + assertEquals("ckpt_abc123", fromYaml1.id, "Expected id"); + assertEquals("sess_abc123", fromYaml1.sessionId, "Expected sessionId"); + assertEquals("turn_abc123", fromYaml1.turnId, "Expected turnId"); + assertEquals("run_abc123", fromYaml1.runId, "Expected runId"); + EngineCheckpoint reloaded1 = EngineCheckpoint.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("ckpt_abc123", reloaded1.id, "Expected id"); + assertEquals("sess_abc123", reloaded1.sessionId, "Expected sessionId"); + assertEquals("turn_abc123", reloaded1.turnId, "Expected turnId"); + assertEquals("run_abc123", reloaded1.runId, "Expected runId"); + + assertThrows(() -> EngineCheckpoint.fromJson("{"), "EngineCheckpoint.fromJson should reject malformed JSON"); + + assertThrows(() -> EngineCheckpoint.fromYaml(":\n broken"), "EngineCheckpoint.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/EngineEventGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/EngineEventGeneratedTest.java new file mode 100644 index 000000000..db2adea6c --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/EngineEventGeneratedTest.java @@ -0,0 +1,77 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class EngineEventGeneratedTest { + private EngineEventGeneratedTest() { } + + static void run() { + + // EngineEvent example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "id": "evt_abc123", + "timestamp": "2025-01-01T00:00:00Z", + "sessionId": "sess_abc123", + "turnId": "turn_abc123", + "runId": "run_abc123" + } + """; + EngineEvent instance1 = EngineEvent.fromJson(jsonData1); + assertEquals("evt_abc123", instance1.id, "Expected id"); + assertEquals("2025-01-01T00:00:00Z", instance1.timestamp, "Expected timestamp"); + assertEquals("sess_abc123", instance1.sessionId, "Expected sessionId"); + assertEquals("turn_abc123", instance1.turnId, "Expected turnId"); + assertEquals("run_abc123", instance1.runId, "Expected runId"); + String yamlRoundtrip1 = instance1.toYaml(); + EngineEvent fromYaml1 = EngineEvent.fromYaml(yamlRoundtrip1); + assertEquals("evt_abc123", fromYaml1.id, "Expected id"); + assertEquals("2025-01-01T00:00:00Z", fromYaml1.timestamp, "Expected timestamp"); + assertEquals("sess_abc123", fromYaml1.sessionId, "Expected sessionId"); + assertEquals("turn_abc123", fromYaml1.turnId, "Expected turnId"); + assertEquals("run_abc123", fromYaml1.runId, "Expected runId"); + EngineEvent reloaded1 = EngineEvent.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("evt_abc123", reloaded1.id, "Expected id"); + assertEquals("2025-01-01T00:00:00Z", reloaded1.timestamp, "Expected timestamp"); + assertEquals("sess_abc123", reloaded1.sessionId, "Expected sessionId"); + assertEquals("turn_abc123", reloaded1.turnId, "Expected turnId"); + assertEquals("run_abc123", reloaded1.runId, "Expected runId"); + + assertThrows(() -> EngineEvent.fromJson("{"), "EngineEvent.fromJson should reject malformed JSON"); + + assertThrows(() -> EngineEvent.fromYaml(":\n broken"), "EngineEvent.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/EnginePermissionDecisionGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/EnginePermissionDecisionGeneratedTest.java new file mode 100644 index 000000000..edd6f8acb --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/EnginePermissionDecisionGeneratedTest.java @@ -0,0 +1,43 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class EnginePermissionDecisionGeneratedTest { + private EnginePermissionDecisionGeneratedTest() { } + + static void run() { + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ErrorChunkGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ErrorChunkGeneratedTest.java new file mode 100644 index 000000000..1470b294d --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ErrorChunkGeneratedTest.java @@ -0,0 +1,61 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class ErrorChunkGeneratedTest { + private ErrorChunkGeneratedTest() { } + + static void run() { + + // ErrorChunk example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "message": "Rate limit exceeded" + } + """; + ErrorChunk instance1 = ErrorChunk.fromJson(jsonData1); + assertEquals("Rate limit exceeded", instance1.message, "Expected message"); + String yamlRoundtrip1 = instance1.toYaml(); + ErrorChunk fromYaml1 = ErrorChunk.fromYaml(yamlRoundtrip1); + assertEquals("Rate limit exceeded", fromYaml1.message, "Expected message"); + ErrorChunk reloaded1 = ErrorChunk.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("Rate limit exceeded", reloaded1.message, "Expected message"); + + assertThrows(() -> ErrorChunk.fromJson("{"), "ErrorChunk.fromJson should reject malformed JSON"); + + assertThrows(() -> ErrorChunk.fromYaml(":\n broken"), "ErrorChunk.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ErrorEventPayloadGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ErrorEventPayloadGeneratedTest.java new file mode 100644 index 000000000..1ded0ac2a --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ErrorEventPayloadGeneratedTest.java @@ -0,0 +1,69 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class ErrorEventPayloadGeneratedTest { + private ErrorEventPayloadGeneratedTest() { } + + static void run() { + + // ErrorEventPayload example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "message": "Rate limit exceeded", + "errorKind": "rate_limit", + "phase": "llm" + } + """; + ErrorEventPayload instance1 = ErrorEventPayload.fromJson(jsonData1); + assertEquals("Rate limit exceeded", instance1.message, "Expected message"); + assertEquals("rate_limit", instance1.errorKind, "Expected errorKind"); + assertEquals("llm", instance1.phase, "Expected phase"); + String yamlRoundtrip1 = instance1.toYaml(); + ErrorEventPayload fromYaml1 = ErrorEventPayload.fromYaml(yamlRoundtrip1); + assertEquals("Rate limit exceeded", fromYaml1.message, "Expected message"); + assertEquals("rate_limit", fromYaml1.errorKind, "Expected errorKind"); + assertEquals("llm", fromYaml1.phase, "Expected phase"); + ErrorEventPayload reloaded1 = ErrorEventPayload.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("Rate limit exceeded", reloaded1.message, "Expected message"); + assertEquals("rate_limit", reloaded1.errorKind, "Expected errorKind"); + assertEquals("llm", reloaded1.phase, "Expected phase"); + + assertThrows(() -> ErrorEventPayload.fromJson("{"), "ErrorEventPayload.fromJson should reject malformed JSON"); + + assertThrows(() -> ErrorEventPayload.fromYaml(":\n broken"), "ErrorEventPayload.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/FileNotFoundErrorGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/FileNotFoundErrorGeneratedTest.java new file mode 100644 index 000000000..c3a2ff3f5 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/FileNotFoundErrorGeneratedTest.java @@ -0,0 +1,65 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class FileNotFoundErrorGeneratedTest { + private FileNotFoundErrorGeneratedTest() { } + + static void run() { + + // FileNotFoundError example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "message": "Prompty file not found: ./chat.prompty", + "path": "./chat.prompty" + } + """; + FileNotFoundError instance1 = FileNotFoundError.fromJson(jsonData1); + assertEquals("Prompty file not found: ./chat.prompty", instance1.message, "Expected message"); + assertEquals("./chat.prompty", instance1.path, "Expected path"); + String yamlRoundtrip1 = instance1.toYaml(); + FileNotFoundError fromYaml1 = FileNotFoundError.fromYaml(yamlRoundtrip1); + assertEquals("Prompty file not found: ./chat.prompty", fromYaml1.message, "Expected message"); + assertEquals("./chat.prompty", fromYaml1.path, "Expected path"); + FileNotFoundError reloaded1 = FileNotFoundError.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("Prompty file not found: ./chat.prompty", reloaded1.message, "Expected message"); + assertEquals("./chat.prompty", reloaded1.path, "Expected path"); + + assertThrows(() -> FileNotFoundError.fromJson("{"), "FileNotFoundError.fromJson should reject malformed JSON"); + + assertThrows(() -> FileNotFoundError.fromYaml(":\n broken"), "FileNotFoundError.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/FilePartGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/FilePartGeneratedTest.java new file mode 100644 index 000000000..167150d3a --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/FilePartGeneratedTest.java @@ -0,0 +1,65 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class FilePartGeneratedTest { + private FilePartGeneratedTest() { } + + static void run() { + + // FilePart example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "source": "https://example.com/document.pdf", + "mediaType": "application/pdf" + } + """; + FilePart instance1 = FilePart.fromJson(jsonData1); + assertEquals("https://example.com/document.pdf", instance1.source, "Expected source"); + assertEquals("application/pdf", instance1.mediaType, "Expected mediaType"); + String yamlRoundtrip1 = instance1.toYaml(); + FilePart fromYaml1 = FilePart.fromYaml(yamlRoundtrip1); + assertEquals("https://example.com/document.pdf", fromYaml1.source, "Expected source"); + assertEquals("application/pdf", fromYaml1.mediaType, "Expected mediaType"); + FilePart reloaded1 = FilePart.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("https://example.com/document.pdf", reloaded1.source, "Expected source"); + assertEquals("application/pdf", reloaded1.mediaType, "Expected mediaType"); + + assertThrows(() -> FilePart.fromJson("{"), "FilePart.fromJson should reject malformed JSON"); + + assertThrows(() -> FilePart.fromYaml(":\n broken"), "FilePart.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/FinalOutputPolicyRequestGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/FinalOutputPolicyRequestGeneratedTest.java new file mode 100644 index 000000000..b129d5d4d --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/FinalOutputPolicyRequestGeneratedTest.java @@ -0,0 +1,43 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class FinalOutputPolicyRequestGeneratedTest { + private FinalOutputPolicyRequestGeneratedTest() { } + + static void run() { + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/FinalOutputPolicyResultGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/FinalOutputPolicyResultGeneratedTest.java new file mode 100644 index 000000000..ebbb9ea6b --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/FinalOutputPolicyResultGeneratedTest.java @@ -0,0 +1,43 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class FinalOutputPolicyResultGeneratedTest { + private FinalOutputPolicyResultGeneratedTest() { } + + static void run() { + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/FormatConfigGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/FormatConfigGeneratedTest.java new file mode 100644 index 000000000..b3d455942 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/FormatConfigGeneratedTest.java @@ -0,0 +1,71 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class FormatConfigGeneratedTest { + private FormatConfigGeneratedTest() { } + + static void run() { + + // FormatConfig example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "kind": "mustache", + "strict": true, + "options": { + "key": "value" + } + } + """; + FormatConfig instance1 = FormatConfig.fromJson(jsonData1); + assertEquals("mustache", instance1.kind, "Expected kind"); + assertEquals(true, instance1.strict, "Expected strict"); + assertEquals("value", instance1.options.get("key"), "Expected options.key"); + String yamlRoundtrip1 = instance1.toYaml(); + FormatConfig fromYaml1 = FormatConfig.fromYaml(yamlRoundtrip1); + assertEquals("mustache", fromYaml1.kind, "Expected kind"); + assertEquals(true, fromYaml1.strict, "Expected strict"); + assertEquals("value", fromYaml1.options.get("key"), "Expected options.key"); + FormatConfig reloaded1 = FormatConfig.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("mustache", reloaded1.kind, "Expected kind"); + assertEquals(true, reloaded1.strict, "Expected strict"); + assertEquals("value", reloaded1.options.get("key"), "Expected options.key"); + + assertThrows(() -> FormatConfig.fromJson("{"), "FormatConfig.fromJson should reject malformed JSON"); + + assertThrows(() -> FormatConfig.fromYaml(":\n broken"), "FormatConfig.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/FoundryConnectionGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/FoundryConnectionGeneratedTest.java new file mode 100644 index 000000000..324f77af3 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/FoundryConnectionGeneratedTest.java @@ -0,0 +1,73 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class FoundryConnectionGeneratedTest { + private FoundryConnectionGeneratedTest() { } + + static void run() { + + // FoundryConnection example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "kind": "foundry", + "endpoint": "https://myresource.services.ai.azure.com/api/projects/myproject", + "name": "my-openai-connection", + "connectionType": "model" + } + """; + FoundryConnection instance1 = FoundryConnection.fromJson(jsonData1); + assertEquals("foundry", instance1.kind, "Expected kind"); + assertEquals("https://myresource.services.ai.azure.com/api/projects/myproject", instance1.endpoint, "Expected endpoint"); + assertEquals("my-openai-connection", instance1.name, "Expected name"); + assertEquals("model", instance1.connectionType, "Expected connectionType"); + String yamlRoundtrip1 = instance1.toYaml(); + FoundryConnection fromYaml1 = FoundryConnection.fromYaml(yamlRoundtrip1); + assertEquals("foundry", fromYaml1.kind, "Expected kind"); + assertEquals("https://myresource.services.ai.azure.com/api/projects/myproject", fromYaml1.endpoint, "Expected endpoint"); + assertEquals("my-openai-connection", fromYaml1.name, "Expected name"); + assertEquals("model", fromYaml1.connectionType, "Expected connectionType"); + FoundryConnection reloaded1 = FoundryConnection.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("foundry", reloaded1.kind, "Expected kind"); + assertEquals("https://myresource.services.ai.azure.com/api/projects/myproject", reloaded1.endpoint, "Expected endpoint"); + assertEquals("my-openai-connection", reloaded1.name, "Expected name"); + assertEquals("model", reloaded1.connectionType, "Expected connectionType"); + + assertThrows(() -> FoundryConnection.fromJson("{"), "FoundryConnection.fromJson should reject malformed JSON"); + + assertThrows(() -> FoundryConnection.fromYaml(":\n broken"), "FoundryConnection.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/FunctionToolGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/FunctionToolGeneratedTest.java new file mode 100644 index 000000000..a28b24a4f --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/FunctionToolGeneratedTest.java @@ -0,0 +1,117 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class FunctionToolGeneratedTest { + private FunctionToolGeneratedTest() { } + + static void run() { + + // FunctionTool example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "kind": "function", + "parameters": { + "firstName": { + "kind": "string", + "default": "Jane" + }, + "lastName": { + "kind": "string", + "default": "Doe" + }, + "question": { + "kind": "string", + "default": "What is the meaning of life?" + } + }, + "strict": true + } + """; + FunctionTool instance1 = FunctionTool.fromJson(jsonData1); + assertEquals("function", instance1.kind, "Expected kind"); + assertEquals(true, instance1.strict, "Expected strict"); + String yamlRoundtrip1 = instance1.toYaml(); + FunctionTool fromYaml1 = FunctionTool.fromYaml(yamlRoundtrip1); + assertEquals("function", fromYaml1.kind, "Expected kind"); + assertEquals(true, fromYaml1.strict, "Expected strict"); + FunctionTool reloaded1 = FunctionTool.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("function", reloaded1.kind, "Expected kind"); + assertEquals(true, reloaded1.strict, "Expected strict"); + + // FunctionTool example 2: fromJson, fromYaml, save, and reload + String jsonData2 = """ + { + "kind": "function", + "parameters": [ + { + "name": "firstName", + "kind": "string", + "default": "Jane" + }, + { + "name": "lastName", + "kind": "string", + "default": "Doe" + }, + { + "name": "question", + "kind": "string", + "default": "What is the meaning of life?" + } + ], + "strict": true + } + """; + FunctionTool instance2 = FunctionTool.fromJson(jsonData2); + assertEquals("function", instance2.kind, "Expected kind"); + assertEquals(true, instance2.strict, "Expected strict"); + assertEquals(3, instance2.parameters.size(), "Expected parameters size"); + String yamlRoundtrip2 = instance2.toYaml(); + FunctionTool fromYaml2 = FunctionTool.fromYaml(yamlRoundtrip2); + assertEquals("function", fromYaml2.kind, "Expected kind"); + assertEquals(true, fromYaml2.strict, "Expected strict"); + assertEquals(3, fromYaml2.parameters.size(), "Expected parameters size"); + FunctionTool reloaded2 = FunctionTool.load(instance2.save(new SaveContext()), new LoadContext()); + assertEquals("function", reloaded2.kind, "Expected kind"); + assertEquals(true, reloaded2.strict, "Expected strict"); + assertEquals(3, reloaded2.parameters.size(), "Expected parameters size"); + + assertThrows(() -> FunctionTool.fromJson("{"), "FunctionTool.fromJson should reject malformed JSON"); + + assertThrows(() -> FunctionTool.fromYaml(":\n broken"), "FunctionTool.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/GuardrailResultGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/GuardrailResultGeneratedTest.java new file mode 100644 index 000000000..200cdf13a --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/GuardrailResultGeneratedTest.java @@ -0,0 +1,65 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class GuardrailResultGeneratedTest { + private GuardrailResultGeneratedTest() { } + + static void run() { + + // GuardrailResult example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "allowed": true, + "reason": "Content is safe" + } + """; + GuardrailResult instance1 = GuardrailResult.fromJson(jsonData1); + assertEquals(true, instance1.allowed, "Expected allowed"); + assertEquals("Content is safe", instance1.reason, "Expected reason"); + String yamlRoundtrip1 = instance1.toYaml(); + GuardrailResult fromYaml1 = GuardrailResult.fromYaml(yamlRoundtrip1); + assertEquals(true, fromYaml1.allowed, "Expected allowed"); + assertEquals("Content is safe", fromYaml1.reason, "Expected reason"); + GuardrailResult reloaded1 = GuardrailResult.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals(true, reloaded1.allowed, "Expected allowed"); + assertEquals("Content is safe", reloaded1.reason, "Expected reason"); + + assertThrows(() -> GuardrailResult.fromJson("{"), "GuardrailResult.fromJson should reject malformed JSON"); + + assertThrows(() -> GuardrailResult.fromYaml(":\n broken"), "GuardrailResult.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/HarnessContextGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/HarnessContextGeneratedTest.java new file mode 100644 index 000000000..c33f507a2 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/HarnessContextGeneratedTest.java @@ -0,0 +1,65 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class HarnessContextGeneratedTest { + private HarnessContextGeneratedTest() { } + + static void run() { + + // HarnessContext example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "cwd": "/workspace/project", + "gitRoot": "/workspace/project" + } + """; + HarnessContext instance1 = HarnessContext.fromJson(jsonData1); + assertEquals("/workspace/project", instance1.cwd, "Expected cwd"); + assertEquals("/workspace/project", instance1.gitRoot, "Expected gitRoot"); + String yamlRoundtrip1 = instance1.toYaml(); + HarnessContext fromYaml1 = HarnessContext.fromYaml(yamlRoundtrip1); + assertEquals("/workspace/project", fromYaml1.cwd, "Expected cwd"); + assertEquals("/workspace/project", fromYaml1.gitRoot, "Expected gitRoot"); + HarnessContext reloaded1 = HarnessContext.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("/workspace/project", reloaded1.cwd, "Expected cwd"); + assertEquals("/workspace/project", reloaded1.gitRoot, "Expected gitRoot"); + + assertThrows(() -> HarnessContext.fromJson("{"), "HarnessContext.fromJson should reject malformed JSON"); + + assertThrows(() -> HarnessContext.fromYaml(":\n broken"), "HarnessContext.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/HookEndPayloadGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/HookEndPayloadGeneratedTest.java new file mode 100644 index 000000000..6b86dfc84 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/HookEndPayloadGeneratedTest.java @@ -0,0 +1,77 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class HookEndPayloadGeneratedTest { + private HookEndPayloadGeneratedTest() { } + + static void run() { + + // HookEndPayload example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "hookInvocationId": "hook_abc123", + "hookType": "preToolUse", + "success": true, + "durationMs": 12, + "error": "hook failed" + } + """; + HookEndPayload instance1 = HookEndPayload.fromJson(jsonData1); + assertEquals("hook_abc123", instance1.hookInvocationId, "Expected hookInvocationId"); + assertEquals("preToolUse", instance1.hookType, "Expected hookType"); + assertEquals(true, instance1.success, "Expected success"); + assertEquals(12, instance1.durationMs, "Expected durationMs"); + assertEquals("hook failed", instance1.error, "Expected error"); + String yamlRoundtrip1 = instance1.toYaml(); + HookEndPayload fromYaml1 = HookEndPayload.fromYaml(yamlRoundtrip1); + assertEquals("hook_abc123", fromYaml1.hookInvocationId, "Expected hookInvocationId"); + assertEquals("preToolUse", fromYaml1.hookType, "Expected hookType"); + assertEquals(true, fromYaml1.success, "Expected success"); + assertEquals(12, fromYaml1.durationMs, "Expected durationMs"); + assertEquals("hook failed", fromYaml1.error, "Expected error"); + HookEndPayload reloaded1 = HookEndPayload.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("hook_abc123", reloaded1.hookInvocationId, "Expected hookInvocationId"); + assertEquals("preToolUse", reloaded1.hookType, "Expected hookType"); + assertEquals(true, reloaded1.success, "Expected success"); + assertEquals(12, reloaded1.durationMs, "Expected durationMs"); + assertEquals("hook failed", reloaded1.error, "Expected error"); + + assertThrows(() -> HookEndPayload.fromJson("{"), "HookEndPayload.fromJson should reject malformed JSON"); + + assertThrows(() -> HookEndPayload.fromYaml(":\n broken"), "HookEndPayload.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/HookStartPayloadGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/HookStartPayloadGeneratedTest.java new file mode 100644 index 000000000..c50eba866 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/HookStartPayloadGeneratedTest.java @@ -0,0 +1,65 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class HookStartPayloadGeneratedTest { + private HookStartPayloadGeneratedTest() { } + + static void run() { + + // HookStartPayload example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "hookInvocationId": "hook_abc123", + "hookType": "preToolUse" + } + """; + HookStartPayload instance1 = HookStartPayload.fromJson(jsonData1); + assertEquals("hook_abc123", instance1.hookInvocationId, "Expected hookInvocationId"); + assertEquals("preToolUse", instance1.hookType, "Expected hookType"); + String yamlRoundtrip1 = instance1.toYaml(); + HookStartPayload fromYaml1 = HookStartPayload.fromYaml(yamlRoundtrip1); + assertEquals("hook_abc123", fromYaml1.hookInvocationId, "Expected hookInvocationId"); + assertEquals("preToolUse", fromYaml1.hookType, "Expected hookType"); + HookStartPayload reloaded1 = HookStartPayload.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("hook_abc123", reloaded1.hookInvocationId, "Expected hookInvocationId"); + assertEquals("preToolUse", reloaded1.hookType, "Expected hookType"); + + assertThrows(() -> HookStartPayload.fromJson("{"), "HookStartPayload.fromJson should reject malformed JSON"); + + assertThrows(() -> HookStartPayload.fromYaml(":\n broken"), "HookStartPayload.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/HostPolicyRequestGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/HostPolicyRequestGeneratedTest.java new file mode 100644 index 000000000..d656f3a7d --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/HostPolicyRequestGeneratedTest.java @@ -0,0 +1,43 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class HostPolicyRequestGeneratedTest { + private HostPolicyRequestGeneratedTest() { } + + static void run() { + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/HostPolicyResultGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/HostPolicyResultGeneratedTest.java new file mode 100644 index 000000000..a7da8fa4b --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/HostPolicyResultGeneratedTest.java @@ -0,0 +1,43 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class HostPolicyResultGeneratedTest { + private HostPolicyResultGeneratedTest() { } + + static void run() { + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/HostToolRequestGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/HostToolRequestGeneratedTest.java new file mode 100644 index 000000000..bf0c26494 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/HostToolRequestGeneratedTest.java @@ -0,0 +1,73 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class HostToolRequestGeneratedTest { + private HostToolRequestGeneratedTest() { } + + static void run() { + + // HostToolRequest example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "requestId": "exec_abc123", + "toolCallId": "call_abc123", + "toolName": "powershell", + "workingDirectory": "/workspace/project" + } + """; + HostToolRequest instance1 = HostToolRequest.fromJson(jsonData1); + assertEquals("exec_abc123", instance1.requestId, "Expected requestId"); + assertEquals("call_abc123", instance1.toolCallId, "Expected toolCallId"); + assertEquals("powershell", instance1.toolName, "Expected toolName"); + assertEquals("/workspace/project", instance1.workingDirectory, "Expected workingDirectory"); + String yamlRoundtrip1 = instance1.toYaml(); + HostToolRequest fromYaml1 = HostToolRequest.fromYaml(yamlRoundtrip1); + assertEquals("exec_abc123", fromYaml1.requestId, "Expected requestId"); + assertEquals("call_abc123", fromYaml1.toolCallId, "Expected toolCallId"); + assertEquals("powershell", fromYaml1.toolName, "Expected toolName"); + assertEquals("/workspace/project", fromYaml1.workingDirectory, "Expected workingDirectory"); + HostToolRequest reloaded1 = HostToolRequest.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("exec_abc123", reloaded1.requestId, "Expected requestId"); + assertEquals("call_abc123", reloaded1.toolCallId, "Expected toolCallId"); + assertEquals("powershell", reloaded1.toolName, "Expected toolName"); + assertEquals("/workspace/project", reloaded1.workingDirectory, "Expected workingDirectory"); + + assertThrows(() -> HostToolRequest.fromJson("{"), "HostToolRequest.fromJson should reject malformed JSON"); + + assertThrows(() -> HostToolRequest.fromYaml(":\n broken"), "HostToolRequest.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/HostToolResultGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/HostToolResultGeneratedTest.java new file mode 100644 index 000000000..2fa602278 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/HostToolResultGeneratedTest.java @@ -0,0 +1,85 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class HostToolResultGeneratedTest { + private HostToolResultGeneratedTest() { } + + static void run() { + + // HostToolResult example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "requestId": "exec_abc123", + "toolCallId": "call_abc123", + "toolName": "powershell", + "success": true, + "exitCode": 0, + "durationMs": 250, + "errorKind": "timeout" + } + """; + HostToolResult instance1 = HostToolResult.fromJson(jsonData1); + assertEquals("exec_abc123", instance1.requestId, "Expected requestId"); + assertEquals("call_abc123", instance1.toolCallId, "Expected toolCallId"); + assertEquals("powershell", instance1.toolName, "Expected toolName"); + assertEquals(true, instance1.success, "Expected success"); + assertEquals(0, instance1.exitCode, "Expected exitCode"); + assertEquals(250, instance1.durationMs, "Expected durationMs"); + assertEquals("timeout", instance1.errorKind, "Expected errorKind"); + String yamlRoundtrip1 = instance1.toYaml(); + HostToolResult fromYaml1 = HostToolResult.fromYaml(yamlRoundtrip1); + assertEquals("exec_abc123", fromYaml1.requestId, "Expected requestId"); + assertEquals("call_abc123", fromYaml1.toolCallId, "Expected toolCallId"); + assertEquals("powershell", fromYaml1.toolName, "Expected toolName"); + assertEquals(true, fromYaml1.success, "Expected success"); + assertEquals(0, fromYaml1.exitCode, "Expected exitCode"); + assertEquals(250, fromYaml1.durationMs, "Expected durationMs"); + assertEquals("timeout", fromYaml1.errorKind, "Expected errorKind"); + HostToolResult reloaded1 = HostToolResult.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("exec_abc123", reloaded1.requestId, "Expected requestId"); + assertEquals("call_abc123", reloaded1.toolCallId, "Expected toolCallId"); + assertEquals("powershell", reloaded1.toolName, "Expected toolName"); + assertEquals(true, reloaded1.success, "Expected success"); + assertEquals(0, reloaded1.exitCode, "Expected exitCode"); + assertEquals(250, reloaded1.durationMs, "Expected durationMs"); + assertEquals("timeout", reloaded1.errorKind, "Expected errorKind"); + + assertThrows(() -> HostToolResult.fromJson("{"), "HostToolResult.fromJson should reject malformed JSON"); + + assertThrows(() -> HostToolResult.fromYaml(":\n broken"), "HostToolResult.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ImagePartGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ImagePartGeneratedTest.java new file mode 100644 index 000000000..fb546b580 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ImagePartGeneratedTest.java @@ -0,0 +1,69 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class ImagePartGeneratedTest { + private ImagePartGeneratedTest() { } + + static void run() { + + // ImagePart example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "source": "https://example.com/image.png", + "detail": "auto", + "mediaType": "image/png" + } + """; + ImagePart instance1 = ImagePart.fromJson(jsonData1); + assertEquals("https://example.com/image.png", instance1.source, "Expected source"); + assertEquals("auto", instance1.detail, "Expected detail"); + assertEquals("image/png", instance1.mediaType, "Expected mediaType"); + String yamlRoundtrip1 = instance1.toYaml(); + ImagePart fromYaml1 = ImagePart.fromYaml(yamlRoundtrip1); + assertEquals("https://example.com/image.png", fromYaml1.source, "Expected source"); + assertEquals("auto", fromYaml1.detail, "Expected detail"); + assertEquals("image/png", fromYaml1.mediaType, "Expected mediaType"); + ImagePart reloaded1 = ImagePart.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("https://example.com/image.png", reloaded1.source, "Expected source"); + assertEquals("auto", reloaded1.detail, "Expected detail"); + assertEquals("image/png", reloaded1.mediaType, "Expected mediaType"); + + assertThrows(() -> ImagePart.fromJson("{"), "ImagePart.fromJson should reject malformed JSON"); + + assertThrows(() -> ImagePart.fromYaml(":\n broken"), "ImagePart.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/InvocationContextDecisionGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/InvocationContextDecisionGeneratedTest.java new file mode 100644 index 000000000..b86576e94 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/InvocationContextDecisionGeneratedTest.java @@ -0,0 +1,65 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class InvocationContextDecisionGeneratedTest { + private InvocationContextDecisionGeneratedTest() { } + + static void run() { + + // InvocationContextDecision example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "candidateId": "memory:project-plan", + "reason": "included by relevance ranking" + } + """; + InvocationContextDecision instance1 = InvocationContextDecision.fromJson(jsonData1); + assertEquals("memory:project-plan", instance1.candidateId, "Expected candidateId"); + assertEquals("included by relevance ranking", instance1.reason, "Expected reason"); + String yamlRoundtrip1 = instance1.toYaml(); + InvocationContextDecision fromYaml1 = InvocationContextDecision.fromYaml(yamlRoundtrip1); + assertEquals("memory:project-plan", fromYaml1.candidateId, "Expected candidateId"); + assertEquals("included by relevance ranking", fromYaml1.reason, "Expected reason"); + InvocationContextDecision reloaded1 = InvocationContextDecision.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("memory:project-plan", reloaded1.candidateId, "Expected candidateId"); + assertEquals("included by relevance ranking", reloaded1.reason, "Expected reason"); + + assertThrows(() -> InvocationContextDecision.fromJson("{"), "InvocationContextDecision.fromJson should reject malformed JSON"); + + assertThrows(() -> InvocationContextDecision.fromYaml(":\n broken"), "InvocationContextDecision.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/InvocationContextStateGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/InvocationContextStateGeneratedTest.java new file mode 100644 index 000000000..96eff0e70 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/InvocationContextStateGeneratedTest.java @@ -0,0 +1,43 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class InvocationContextStateGeneratedTest { + private InvocationContextStateGeneratedTest() { } + + static void run() { + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/InvocationUsageGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/InvocationUsageGeneratedTest.java new file mode 100644 index 000000000..da41646f6 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/InvocationUsageGeneratedTest.java @@ -0,0 +1,83 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class InvocationUsageGeneratedTest { + private InvocationUsageGeneratedTest() { } + + static void run() { + + // InvocationUsage example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "inputTokens": 150, + "outputTokens": 42, + "totalTokens": 192 + } + """; + InvocationUsage instance1 = InvocationUsage.fromJson(jsonData1); + assertEquals(150, instance1.inputTokens, "Expected inputTokens"); + assertEquals(42, instance1.outputTokens, "Expected outputTokens"); + assertEquals(192, instance1.totalTokens, "Expected totalTokens"); + String yamlRoundtrip1 = instance1.toYaml(); + InvocationUsage fromYaml1 = InvocationUsage.fromYaml(yamlRoundtrip1); + assertEquals(150, fromYaml1.inputTokens, "Expected inputTokens"); + assertEquals(42, fromYaml1.outputTokens, "Expected outputTokens"); + assertEquals(192, fromYaml1.totalTokens, "Expected totalTokens"); + InvocationUsage reloaded1 = InvocationUsage.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals(150, reloaded1.inputTokens, "Expected inputTokens"); + assertEquals(42, reloaded1.outputTokens, "Expected outputTokens"); + assertEquals(192, reloaded1.totalTokens, "Expected totalTokens"); + + assertThrows(() -> InvocationUsage.fromJson("{"), "InvocationUsage.fromJson should reject malformed JSON"); + + assertThrows(() -> InvocationUsage.fromYaml(":\n broken"), "InvocationUsage.fromYaml should reject malformed YAML"); + + InvocationUsage wireInstance = InvocationUsage.fromJson("{\n \"inputTokens\": 150,\n \"outputTokens\": 42,\n \"totalTokens\": 192\n}"); + java.util.Map openaiWire = wireInstance.toWire("openai"); + assertTrue(openaiWire.containsKey("prompt_tokens"), "Expected openai wire output to include prompt_tokens"); + assertTrue(!openaiWire.containsKey("inputTokens"), "Expected openai wire output to omit inputTokens"); + assertTrue(openaiWire.containsKey("completion_tokens"), "Expected openai wire output to include completion_tokens"); + assertTrue(!openaiWire.containsKey("outputTokens"), "Expected openai wire output to omit outputTokens"); + assertTrue(openaiWire.containsKey("total_tokens"), "Expected openai wire output to include total_tokens"); + assertTrue(!openaiWire.containsKey("totalTokens"), "Expected openai wire output to omit totalTokens"); + java.util.Map anthropicWire = wireInstance.toWire("anthropic"); + assertTrue(anthropicWire.containsKey("input_tokens"), "Expected anthropic wire output to include input_tokens"); + assertTrue(!anthropicWire.containsKey("inputTokens"), "Expected anthropic wire output to omit inputTokens"); + assertTrue(anthropicWire.containsKey("output_tokens"), "Expected anthropic wire output to include output_tokens"); + assertTrue(!anthropicWire.containsKey("outputTokens"), "Expected anthropic wire output to omit outputTokens"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/InvokerErrorGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/InvokerErrorGeneratedTest.java new file mode 100644 index 000000000..81111acb3 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/InvokerErrorGeneratedTest.java @@ -0,0 +1,69 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class InvokerErrorGeneratedTest { + private InvokerErrorGeneratedTest() { } + + static void run() { + + // InvokerError example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "message": "No renderer registered for key: jinja2", + "component": "renderer", + "key": "jinja2" + } + """; + InvokerError instance1 = InvokerError.fromJson(jsonData1); + assertEquals("No renderer registered for key: jinja2", instance1.message, "Expected message"); + assertEquals("renderer", instance1.component, "Expected component"); + assertEquals("jinja2", instance1.key, "Expected key"); + String yamlRoundtrip1 = instance1.toYaml(); + InvokerError fromYaml1 = InvokerError.fromYaml(yamlRoundtrip1); + assertEquals("No renderer registered for key: jinja2", fromYaml1.message, "Expected message"); + assertEquals("renderer", fromYaml1.component, "Expected component"); + assertEquals("jinja2", fromYaml1.key, "Expected key"); + InvokerError reloaded1 = InvokerError.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("No renderer registered for key: jinja2", reloaded1.message, "Expected message"); + assertEquals("renderer", reloaded1.component, "Expected component"); + assertEquals("jinja2", reloaded1.key, "Expected key"); + + assertThrows(() -> InvokerError.fromJson("{"), "InvokerError.fromJson should reject malformed JSON"); + + assertThrows(() -> InvokerError.fromYaml(":\n broken"), "InvokerError.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/LlmCompletePayloadGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/LlmCompletePayloadGeneratedTest.java new file mode 100644 index 000000000..3f1abcaea --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/LlmCompletePayloadGeneratedTest.java @@ -0,0 +1,69 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class LlmCompletePayloadGeneratedTest { + private LlmCompletePayloadGeneratedTest() { } + + static void run() { + + // LlmCompletePayload example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "requestId": "req_abc123", + "serviceRequestId": "srv_abc123", + "durationMs": 820 + } + """; + LlmCompletePayload instance1 = LlmCompletePayload.fromJson(jsonData1); + assertEquals("req_abc123", instance1.requestId, "Expected requestId"); + assertEquals("srv_abc123", instance1.serviceRequestId, "Expected serviceRequestId"); + assertEquals(820, instance1.durationMs, "Expected durationMs"); + String yamlRoundtrip1 = instance1.toYaml(); + LlmCompletePayload fromYaml1 = LlmCompletePayload.fromYaml(yamlRoundtrip1); + assertEquals("req_abc123", fromYaml1.requestId, "Expected requestId"); + assertEquals("srv_abc123", fromYaml1.serviceRequestId, "Expected serviceRequestId"); + assertEquals(820, fromYaml1.durationMs, "Expected durationMs"); + LlmCompletePayload reloaded1 = LlmCompletePayload.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("req_abc123", reloaded1.requestId, "Expected requestId"); + assertEquals("srv_abc123", reloaded1.serviceRequestId, "Expected serviceRequestId"); + assertEquals(820, reloaded1.durationMs, "Expected durationMs"); + + assertThrows(() -> LlmCompletePayload.fromJson("{"), "LlmCompletePayload.fromJson should reject malformed JSON"); + + assertThrows(() -> LlmCompletePayload.fromYaml(":\n broken"), "LlmCompletePayload.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/LlmStartPayloadGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/LlmStartPayloadGeneratedTest.java new file mode 100644 index 000000000..38334cb16 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/LlmStartPayloadGeneratedTest.java @@ -0,0 +1,73 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class LlmStartPayloadGeneratedTest { + private LlmStartPayloadGeneratedTest() { } + + static void run() { + + // LlmStartPayload example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "provider": "openai", + "modelId": "gpt-4o-mini", + "messageCount": 4, + "attempt": 0 + } + """; + LlmStartPayload instance1 = LlmStartPayload.fromJson(jsonData1); + assertEquals("openai", instance1.provider, "Expected provider"); + assertEquals("gpt-4o-mini", instance1.modelId, "Expected modelId"); + assertEquals(4, instance1.messageCount, "Expected messageCount"); + assertEquals(0, instance1.attempt, "Expected attempt"); + String yamlRoundtrip1 = instance1.toYaml(); + LlmStartPayload fromYaml1 = LlmStartPayload.fromYaml(yamlRoundtrip1); + assertEquals("openai", fromYaml1.provider, "Expected provider"); + assertEquals("gpt-4o-mini", fromYaml1.modelId, "Expected modelId"); + assertEquals(4, fromYaml1.messageCount, "Expected messageCount"); + assertEquals(0, fromYaml1.attempt, "Expected attempt"); + LlmStartPayload reloaded1 = LlmStartPayload.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("openai", reloaded1.provider, "Expected provider"); + assertEquals("gpt-4o-mini", reloaded1.modelId, "Expected modelId"); + assertEquals(4, reloaded1.messageCount, "Expected messageCount"); + assertEquals(0, reloaded1.attempt, "Expected attempt"); + + assertThrows(() -> LlmStartPayload.fromJson("{"), "LlmStartPayload.fromJson should reject malformed JSON"); + + assertThrows(() -> LlmStartPayload.fromYaml(":\n broken"), "LlmStartPayload.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/McpApprovalModeGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/McpApprovalModeGeneratedTest.java new file mode 100644 index 000000000..63355d884 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/McpApprovalModeGeneratedTest.java @@ -0,0 +1,79 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class McpApprovalModeGeneratedTest { + private McpApprovalModeGeneratedTest() { } + + static void run() { + + // McpApprovalMode example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "kind": "never", + "alwaysRequireApprovalTools": [ + "operation1" + ], + "neverRequireApprovalTools": [ + "operation2" + ] + } + """; + McpApprovalMode instance1 = McpApprovalMode.fromJson(jsonData1); + assertEquals(McpApprovalModeKind.fromValue("never"), instance1.kind, "Expected kind"); + assertEquals(1, instance1.alwaysRequireApprovalTools.size(), "Expected alwaysRequireApprovalTools size"); + assertEquals("operation1", instance1.alwaysRequireApprovalTools.get(0), "Expected alwaysRequireApprovalTools[0]"); + assertEquals(1, instance1.neverRequireApprovalTools.size(), "Expected neverRequireApprovalTools size"); + assertEquals("operation2", instance1.neverRequireApprovalTools.get(0), "Expected neverRequireApprovalTools[0]"); + String yamlRoundtrip1 = instance1.toYaml(); + McpApprovalMode fromYaml1 = McpApprovalMode.fromYaml(yamlRoundtrip1); + assertEquals(McpApprovalModeKind.fromValue("never"), fromYaml1.kind, "Expected kind"); + assertEquals(1, fromYaml1.alwaysRequireApprovalTools.size(), "Expected alwaysRequireApprovalTools size"); + assertEquals("operation1", fromYaml1.alwaysRequireApprovalTools.get(0), "Expected alwaysRequireApprovalTools[0]"); + assertEquals(1, fromYaml1.neverRequireApprovalTools.size(), "Expected neverRequireApprovalTools size"); + assertEquals("operation2", fromYaml1.neverRequireApprovalTools.get(0), "Expected neverRequireApprovalTools[0]"); + McpApprovalMode reloaded1 = McpApprovalMode.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals(McpApprovalModeKind.fromValue("never"), reloaded1.kind, "Expected kind"); + assertEquals(1, reloaded1.alwaysRequireApprovalTools.size(), "Expected alwaysRequireApprovalTools size"); + assertEquals("operation1", reloaded1.alwaysRequireApprovalTools.get(0), "Expected alwaysRequireApprovalTools[0]"); + assertEquals(1, reloaded1.neverRequireApprovalTools.size(), "Expected neverRequireApprovalTools size"); + assertEquals("operation2", reloaded1.neverRequireApprovalTools.get(0), "Expected neverRequireApprovalTools[0]"); + + assertThrows(() -> McpApprovalMode.fromJson("{"), "McpApprovalMode.fromJson should reject malformed JSON"); + + assertThrows(() -> McpApprovalMode.fromYaml(":\n broken"), "McpApprovalMode.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/McpToolGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/McpToolGeneratedTest.java new file mode 100644 index 000000000..935bb5f30 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/McpToolGeneratedTest.java @@ -0,0 +1,91 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class McpToolGeneratedTest { + private McpToolGeneratedTest() { } + + static void run() { + + // McpTool example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "kind": "mcp", + "connection": { + "kind": "reference" + }, + "serverName": "My MCP Server", + "serverDescription": "This tool allows access to MCP services.", + "approvalMode": { + "kind": "always" + }, + "allowedTools": [ + "operation1", + "operation2" + ] + } + """; + McpTool instance1 = McpTool.fromJson(jsonData1); + assertEquals("mcp", instance1.kind, "Expected kind"); + assertEquals("My MCP Server", instance1.serverName, "Expected serverName"); + assertEquals("This tool allows access to MCP services.", instance1.serverDescription, "Expected serverDescription"); + assertEquals("always", instance1.approvalMode.kind, "Expected instance1.approvalMode.kind"); + assertEquals(2, instance1.allowedTools.size(), "Expected allowedTools size"); + assertEquals("operation1", instance1.allowedTools.get(0), "Expected allowedTools[0]"); + assertEquals("operation2", instance1.allowedTools.get(1), "Expected allowedTools[1]"); + String yamlRoundtrip1 = instance1.toYaml(); + McpTool fromYaml1 = McpTool.fromYaml(yamlRoundtrip1); + assertEquals("mcp", fromYaml1.kind, "Expected kind"); + assertEquals("My MCP Server", fromYaml1.serverName, "Expected serverName"); + assertEquals("This tool allows access to MCP services.", fromYaml1.serverDescription, "Expected serverDescription"); + assertEquals("always", fromYaml1.approvalMode.kind, "Expected fromYaml1.approvalMode.kind"); + assertEquals(2, fromYaml1.allowedTools.size(), "Expected allowedTools size"); + assertEquals("operation1", fromYaml1.allowedTools.get(0), "Expected allowedTools[0]"); + assertEquals("operation2", fromYaml1.allowedTools.get(1), "Expected allowedTools[1]"); + McpTool reloaded1 = McpTool.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("mcp", reloaded1.kind, "Expected kind"); + assertEquals("My MCP Server", reloaded1.serverName, "Expected serverName"); + assertEquals("This tool allows access to MCP services.", reloaded1.serverDescription, "Expected serverDescription"); + assertEquals("always", reloaded1.approvalMode.kind, "Expected reloaded1.approvalMode.kind"); + assertEquals(2, reloaded1.allowedTools.size(), "Expected allowedTools size"); + assertEquals("operation1", reloaded1.allowedTools.get(0), "Expected allowedTools[0]"); + assertEquals("operation2", reloaded1.allowedTools.get(1), "Expected allowedTools[1]"); + + assertThrows(() -> McpTool.fromJson("{"), "McpTool.fromJson should reject malformed JSON"); + + assertThrows(() -> McpTool.fromYaml(":\n broken"), "McpTool.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/MemoryEntryGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/MemoryEntryGeneratedTest.java new file mode 100644 index 000000000..5747008b8 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/MemoryEntryGeneratedTest.java @@ -0,0 +1,82 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class MemoryEntryGeneratedTest { + private MemoryEntryGeneratedTest() { } + + static void run() { + + // MemoryEntry example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "content": "The user prefers concise answers.", + "category": "core", + "createdAt": "2026-06-09T20:00:00Z", + "tags": [ + "preference", + "tone" + ] + } + """; + MemoryEntry instance1 = MemoryEntry.fromJson(jsonData1); + assertEquals("The user prefers concise answers.", instance1.content, "Expected content"); + assertEquals(MemoryCategory.fromValue("core"), instance1.category, "Expected category"); + assertEquals("2026-06-09T20:00:00Z", instance1.createdAt, "Expected createdAt"); + assertEquals(2, instance1.tags.size(), "Expected tags size"); + assertEquals("preference", instance1.tags.get(0), "Expected tags[0]"); + assertEquals("tone", instance1.tags.get(1), "Expected tags[1]"); + String yamlRoundtrip1 = instance1.toYaml(); + MemoryEntry fromYaml1 = MemoryEntry.fromYaml(yamlRoundtrip1); + assertEquals("The user prefers concise answers.", fromYaml1.content, "Expected content"); + assertEquals(MemoryCategory.fromValue("core"), fromYaml1.category, "Expected category"); + assertEquals("2026-06-09T20:00:00Z", fromYaml1.createdAt, "Expected createdAt"); + assertEquals(2, fromYaml1.tags.size(), "Expected tags size"); + assertEquals("preference", fromYaml1.tags.get(0), "Expected tags[0]"); + assertEquals("tone", fromYaml1.tags.get(1), "Expected tags[1]"); + MemoryEntry reloaded1 = MemoryEntry.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("The user prefers concise answers.", reloaded1.content, "Expected content"); + assertEquals(MemoryCategory.fromValue("core"), reloaded1.category, "Expected category"); + assertEquals("2026-06-09T20:00:00Z", reloaded1.createdAt, "Expected createdAt"); + assertEquals(2, reloaded1.tags.size(), "Expected tags size"); + assertEquals("preference", reloaded1.tags.get(0), "Expected tags[0]"); + assertEquals("tone", reloaded1.tags.get(1), "Expected tags[1]"); + + assertThrows(() -> MemoryEntry.fromJson("{"), "MemoryEntry.fromJson should reject malformed JSON"); + + assertThrows(() -> MemoryEntry.fromYaml(":\n broken"), "MemoryEntry.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/MemoryStoreGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/MemoryStoreGeneratedTest.java new file mode 100644 index 000000000..a2dc8e60c --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/MemoryStoreGeneratedTest.java @@ -0,0 +1,61 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class MemoryStoreGeneratedTest { + private MemoryStoreGeneratedTest() { } + + static void run() { + + // MemoryStore example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "entries": [] + } + """; + MemoryStore instance1 = MemoryStore.fromJson(jsonData1); + assertEquals(0, instance1.entries.size(), "Expected entries size"); + String yamlRoundtrip1 = instance1.toYaml(); + MemoryStore fromYaml1 = MemoryStore.fromYaml(yamlRoundtrip1); + assertEquals(0, fromYaml1.entries.size(), "Expected entries size"); + MemoryStore reloaded1 = MemoryStore.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals(0, reloaded1.entries.size(), "Expected entries size"); + + assertThrows(() -> MemoryStore.fromJson("{"), "MemoryStore.fromJson should reject malformed JSON"); + + assertThrows(() -> MemoryStore.fromYaml(":\n broken"), "MemoryStore.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/MessageGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/MessageGeneratedTest.java new file mode 100644 index 000000000..3573ea776 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/MessageGeneratedTest.java @@ -0,0 +1,88 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class MessageGeneratedTest { + private MessageGeneratedTest() { } + + static void run() { + + // Message example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "role": "user", + "parts": [ + { + "kind": "text", + "value": "Hello!" + } + ], + "metadata": { + "source": "user-input" + } + } + """; + Message instance1 = Message.fromJson(jsonData1); + assertEquals(Role.fromValue("user"), instance1.role, "Expected role"); + assertEquals(1, instance1.parts.size(), "Expected parts size"); + assertTrue(instance1.parts.get(0) instanceof TextPart, "Expected parts[0] to be TextPart"); + TextPart instance1Parts0Value = (TextPart) instance1.parts.get(0); + assertEquals("text", instance1Parts0Value.kind, "Expected kind"); + assertEquals("Hello!", instance1Parts0Value.value, "Expected value"); + assertEquals("user-input", instance1.metadata.get("source"), "Expected metadata.source"); + String yamlRoundtrip1 = instance1.toYaml(); + Message fromYaml1 = Message.fromYaml(yamlRoundtrip1); + assertEquals(Role.fromValue("user"), fromYaml1.role, "Expected role"); + assertEquals(1, fromYaml1.parts.size(), "Expected parts size"); + assertTrue(fromYaml1.parts.get(0) instanceof TextPart, "Expected parts[0] to be TextPart"); + TextPart fromYaml1Parts0Value = (TextPart) fromYaml1.parts.get(0); + assertEquals("text", fromYaml1Parts0Value.kind, "Expected kind"); + assertEquals("Hello!", fromYaml1Parts0Value.value, "Expected value"); + assertEquals("user-input", fromYaml1.metadata.get("source"), "Expected metadata.source"); + Message reloaded1 = Message.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals(Role.fromValue("user"), reloaded1.role, "Expected role"); + assertEquals(1, reloaded1.parts.size(), "Expected parts size"); + assertTrue(reloaded1.parts.get(0) instanceof TextPart, "Expected parts[0] to be TextPart"); + TextPart reloaded1Parts0Value = (TextPart) reloaded1.parts.get(0); + assertEquals("text", reloaded1Parts0Value.kind, "Expected kind"); + assertEquals("Hello!", reloaded1Parts0Value.value, "Expected value"); + assertEquals("user-input", reloaded1.metadata.get("source"), "Expected metadata.source"); + + assertThrows(() -> Message.fromJson("{"), "Message.fromJson should reject malformed JSON"); + + assertThrows(() -> Message.fromYaml(":\n broken"), "Message.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/MessagesUpdatedPayloadGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/MessagesUpdatedPayloadGeneratedTest.java new file mode 100644 index 000000000..0cf122d44 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/MessagesUpdatedPayloadGeneratedTest.java @@ -0,0 +1,65 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class MessagesUpdatedPayloadGeneratedTest { + private MessagesUpdatedPayloadGeneratedTest() { } + + static void run() { + + // MessagesUpdatedPayload example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "reason": "tool_results", + "removed": 2 + } + """; + MessagesUpdatedPayload instance1 = MessagesUpdatedPayload.fromJson(jsonData1); + assertEquals("tool_results", instance1.reason, "Expected reason"); + assertEquals(2, instance1.removed, "Expected removed"); + String yamlRoundtrip1 = instance1.toYaml(); + MessagesUpdatedPayload fromYaml1 = MessagesUpdatedPayload.fromYaml(yamlRoundtrip1); + assertEquals("tool_results", fromYaml1.reason, "Expected reason"); + assertEquals(2, fromYaml1.removed, "Expected removed"); + MessagesUpdatedPayload reloaded1 = MessagesUpdatedPayload.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("tool_results", reloaded1.reason, "Expected reason"); + assertEquals(2, reloaded1.removed, "Expected removed"); + + assertThrows(() -> MessagesUpdatedPayload.fromJson("{"), "MessagesUpdatedPayload.fromJson should reject malformed JSON"); + + assertThrows(() -> MessagesUpdatedPayload.fromYaml(":\n broken"), "MessagesUpdatedPayload.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ModelGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ModelGeneratedTest.java new file mode 100644 index 000000000..1b2ed3c4f --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ModelGeneratedTest.java @@ -0,0 +1,97 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class ModelGeneratedTest { + private ModelGeneratedTest() { } + + static void run() { + + // Model example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "id": "gpt-35-turbo", + "provider": "foundry", + "apiType": "chat", + "connection": { + "kind": "key", + "endpoint": "https://{your-custom-endpoint}.openai.azure.com/", + "key": "{your-api-key}" + }, + "options": { + "type": "chat", + "temperature": 0.7, + "maxOutputTokens": 1000 + } + } + """; + Model instance1 = Model.fromJson(jsonData1); + assertEquals("gpt-35-turbo", instance1.id, "Expected id"); + assertEquals("foundry", instance1.provider, "Expected provider"); + assertEquals("chat", instance1.apiType, "Expected apiType"); + assertTrue(instance1.connection instanceof ApiKeyConnection, "Expected connection to be ApiKeyConnection"); + ApiKeyConnection instance1ConnectionValue = (ApiKeyConnection) instance1.connection; + assertEquals("key", instance1ConnectionValue.kind, "Expected kind"); + assertEquals("https://{your-custom-endpoint}.openai.azure.com/", instance1ConnectionValue.endpoint, "Expected endpoint"); + assertEquals(1000, instance1.options.maxOutputTokens, "Expected instance1.options.maxOutputTokens"); + assertEquals(0.7, instance1.options.temperature, "Expected instance1.options.temperature"); + String yamlRoundtrip1 = instance1.toYaml(); + Model fromYaml1 = Model.fromYaml(yamlRoundtrip1); + assertEquals("gpt-35-turbo", fromYaml1.id, "Expected id"); + assertEquals("foundry", fromYaml1.provider, "Expected provider"); + assertEquals("chat", fromYaml1.apiType, "Expected apiType"); + assertTrue(fromYaml1.connection instanceof ApiKeyConnection, "Expected connection to be ApiKeyConnection"); + ApiKeyConnection fromYaml1ConnectionValue = (ApiKeyConnection) fromYaml1.connection; + assertEquals("key", fromYaml1ConnectionValue.kind, "Expected kind"); + assertEquals("https://{your-custom-endpoint}.openai.azure.com/", fromYaml1ConnectionValue.endpoint, "Expected endpoint"); + assertEquals(1000, fromYaml1.options.maxOutputTokens, "Expected fromYaml1.options.maxOutputTokens"); + assertEquals(0.7, fromYaml1.options.temperature, "Expected fromYaml1.options.temperature"); + Model reloaded1 = Model.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("gpt-35-turbo", reloaded1.id, "Expected id"); + assertEquals("foundry", reloaded1.provider, "Expected provider"); + assertEquals("chat", reloaded1.apiType, "Expected apiType"); + assertTrue(reloaded1.connection instanceof ApiKeyConnection, "Expected connection to be ApiKeyConnection"); + ApiKeyConnection reloaded1ConnectionValue = (ApiKeyConnection) reloaded1.connection; + assertEquals("key", reloaded1ConnectionValue.kind, "Expected kind"); + assertEquals("https://{your-custom-endpoint}.openai.azure.com/", reloaded1ConnectionValue.endpoint, "Expected endpoint"); + assertEquals(1000, reloaded1.options.maxOutputTokens, "Expected reloaded1.options.maxOutputTokens"); + assertEquals(0.7, reloaded1.options.temperature, "Expected reloaded1.options.temperature"); + + assertThrows(() -> Model.fromJson("{"), "Model.fromJson should reject malformed JSON"); + + assertThrows(() -> Model.fromYaml(":\n broken"), "Model.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ModelInfoGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ModelInfoGeneratedTest.java new file mode 100644 index 000000000..9a44a4127 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ModelInfoGeneratedTest.java @@ -0,0 +1,114 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class ModelInfoGeneratedTest { + private ModelInfoGeneratedTest() { } + + static void run() { + + // ModelInfo example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "id": "gpt-4o", + "displayName": "GPT-4o", + "ownedBy": "openai", + "contextWindow": 128000, + "inputModalities": [ + "text", + "image" + ], + "outputModalities": [ + "text" + ], + "additionalProperties": { + "supportsStreaming": true + } + } + """; + ModelInfo instance1 = ModelInfo.fromJson(jsonData1); + assertEquals("gpt-4o", instance1.id, "Expected id"); + assertEquals("GPT-4o", instance1.displayName, "Expected displayName"); + assertEquals("openai", instance1.ownedBy, "Expected ownedBy"); + assertEquals(128000, instance1.contextWindow, "Expected contextWindow"); + assertEquals(2, instance1.inputModalities.size(), "Expected inputModalities size"); + assertEquals("text", instance1.inputModalities.get(0), "Expected inputModalities[0]"); + assertEquals("image", instance1.inputModalities.get(1), "Expected inputModalities[1]"); + assertEquals(1, instance1.outputModalities.size(), "Expected outputModalities size"); + assertEquals("text", instance1.outputModalities.get(0), "Expected outputModalities[0]"); + String yamlRoundtrip1 = instance1.toYaml(); + ModelInfo fromYaml1 = ModelInfo.fromYaml(yamlRoundtrip1); + assertEquals("gpt-4o", fromYaml1.id, "Expected id"); + assertEquals("GPT-4o", fromYaml1.displayName, "Expected displayName"); + assertEquals("openai", fromYaml1.ownedBy, "Expected ownedBy"); + assertEquals(128000, fromYaml1.contextWindow, "Expected contextWindow"); + assertEquals(2, fromYaml1.inputModalities.size(), "Expected inputModalities size"); + assertEquals("text", fromYaml1.inputModalities.get(0), "Expected inputModalities[0]"); + assertEquals("image", fromYaml1.inputModalities.get(1), "Expected inputModalities[1]"); + assertEquals(1, fromYaml1.outputModalities.size(), "Expected outputModalities size"); + assertEquals("text", fromYaml1.outputModalities.get(0), "Expected outputModalities[0]"); + ModelInfo reloaded1 = ModelInfo.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("gpt-4o", reloaded1.id, "Expected id"); + assertEquals("GPT-4o", reloaded1.displayName, "Expected displayName"); + assertEquals("openai", reloaded1.ownedBy, "Expected ownedBy"); + assertEquals(128000, reloaded1.contextWindow, "Expected contextWindow"); + assertEquals(2, reloaded1.inputModalities.size(), "Expected inputModalities size"); + assertEquals("text", reloaded1.inputModalities.get(0), "Expected inputModalities[0]"); + assertEquals("image", reloaded1.inputModalities.get(1), "Expected inputModalities[1]"); + assertEquals(1, reloaded1.outputModalities.size(), "Expected outputModalities size"); + assertEquals("text", reloaded1.outputModalities.get(0), "Expected outputModalities[0]"); + + assertThrows(() -> ModelInfo.fromJson("{"), "ModelInfo.fromJson should reject malformed JSON"); + + assertThrows(() -> ModelInfo.fromYaml(":\n broken"), "ModelInfo.fromYaml should reject malformed YAML"); + + ModelInfo wireInstance = ModelInfo.fromJson("{\n \"id\": \"gpt-4o\",\n \"displayName\": \"GPT-4o\",\n \"ownedBy\": \"openai\",\n \"contextWindow\": 128000,\n \"inputModalities\": [\n \"text\",\n \"image\"\n ],\n \"outputModalities\": [\n \"text\"\n ],\n \"additionalProperties\": {\n \"supportsStreaming\": true\n }\n}"); + java.util.Map openaiWire = wireInstance.toWire("openai"); + assertTrue(openaiWire.containsKey("id"), "Expected openai wire output to include id"); + assertTrue(openaiWire.containsKey("owned_by"), "Expected openai wire output to include owned_by"); + assertTrue(!openaiWire.containsKey("ownedBy"), "Expected openai wire output to omit ownedBy"); + java.util.Map anthropicWire = wireInstance.toWire("anthropic"); + assertTrue(anthropicWire.containsKey("id"), "Expected anthropic wire output to include id"); + assertTrue(anthropicWire.containsKey("display_name"), "Expected anthropic wire output to include display_name"); + assertTrue(!anthropicWire.containsKey("displayName"), "Expected anthropic wire output to omit displayName"); + assertTrue(anthropicWire.containsKey("context_length"), "Expected anthropic wire output to include context_length"); + assertTrue(!anthropicWire.containsKey("contextWindow"), "Expected anthropic wire output to omit contextWindow"); + assertTrue(anthropicWire.containsKey("input_modalities"), "Expected anthropic wire output to include input_modalities"); + assertTrue(!anthropicWire.containsKey("inputModalities"), "Expected anthropic wire output to omit inputModalities"); + assertTrue(anthropicWire.containsKey("output_modalities"), "Expected anthropic wire output to include output_modalities"); + assertTrue(!anthropicWire.containsKey("outputModalities"), "Expected anthropic wire output to omit outputModalities"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ModelInvocationContextSnapshotGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ModelInvocationContextSnapshotGeneratedTest.java new file mode 100644 index 000000000..72a27a031 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ModelInvocationContextSnapshotGeneratedTest.java @@ -0,0 +1,73 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class ModelInvocationContextSnapshotGeneratedTest { + private ModelInvocationContextSnapshotGeneratedTest() { } + + static void run() { + + // ModelInvocationContextSnapshot example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "id": "context:inv_abc123", + "sessionId": "sess_abc123", + "turnId": "turn_abc123", + "invocationId": "inv_abc123" + } + """; + ModelInvocationContextSnapshot instance1 = ModelInvocationContextSnapshot.fromJson(jsonData1); + assertEquals("context:inv_abc123", instance1.id, "Expected id"); + assertEquals("sess_abc123", instance1.sessionId, "Expected sessionId"); + assertEquals("turn_abc123", instance1.turnId, "Expected turnId"); + assertEquals("inv_abc123", instance1.invocationId, "Expected invocationId"); + String yamlRoundtrip1 = instance1.toYaml(); + ModelInvocationContextSnapshot fromYaml1 = ModelInvocationContextSnapshot.fromYaml(yamlRoundtrip1); + assertEquals("context:inv_abc123", fromYaml1.id, "Expected id"); + assertEquals("sess_abc123", fromYaml1.sessionId, "Expected sessionId"); + assertEquals("turn_abc123", fromYaml1.turnId, "Expected turnId"); + assertEquals("inv_abc123", fromYaml1.invocationId, "Expected invocationId"); + ModelInvocationContextSnapshot reloaded1 = ModelInvocationContextSnapshot.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("context:inv_abc123", reloaded1.id, "Expected id"); + assertEquals("sess_abc123", reloaded1.sessionId, "Expected sessionId"); + assertEquals("turn_abc123", reloaded1.turnId, "Expected turnId"); + assertEquals("inv_abc123", reloaded1.invocationId, "Expected invocationId"); + + assertThrows(() -> ModelInvocationContextSnapshot.fromJson("{"), "ModelInvocationContextSnapshot.fromJson should reject malformed JSON"); + + assertThrows(() -> ModelInvocationContextSnapshot.fromYaml(":\n broken"), "ModelInvocationContextSnapshot.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ModelInvocationRequestGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ModelInvocationRequestGeneratedTest.java new file mode 100644 index 000000000..dbd28181f --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ModelInvocationRequestGeneratedTest.java @@ -0,0 +1,43 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class ModelInvocationRequestGeneratedTest { + private ModelInvocationRequestGeneratedTest() { } + + static void run() { + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ModelInvocationResponseGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ModelInvocationResponseGeneratedTest.java new file mode 100644 index 000000000..934ee5a64 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ModelInvocationResponseGeneratedTest.java @@ -0,0 +1,43 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class ModelInvocationResponseGeneratedTest { + private ModelInvocationResponseGeneratedTest() { } + + static void run() { + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ModelOptionsGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ModelOptionsGeneratedTest.java new file mode 100644 index 000000000..d08c8a90c --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ModelOptionsGeneratedTest.java @@ -0,0 +1,147 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class ModelOptionsGeneratedTest { + private ModelOptionsGeneratedTest() { } + + static void run() { + + // ModelOptions example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "frequencyPenalty": 0.5, + "maxOutputTokens": 2048, + "presencePenalty": 0.3, + "seed": 42, + "temperature": 0.7, + "topK": 40, + "topP": 0.9, + "stopSequences": [ + "\\n", + "###" + ], + "allowMultipleToolCalls": true, + "additionalProperties": { + "customProperty": "value", + "anotherProperty": "anotherValue" + } + } + """; + ModelOptions instance1 = ModelOptions.fromJson(jsonData1); + assertEquals(0.5, instance1.frequencyPenalty, "Expected frequencyPenalty"); + assertEquals(2048, instance1.maxOutputTokens, "Expected maxOutputTokens"); + assertEquals(0.3, instance1.presencePenalty, "Expected presencePenalty"); + assertEquals(42, instance1.seed, "Expected seed"); + assertEquals(0.7, instance1.temperature, "Expected temperature"); + assertEquals(40, instance1.topK, "Expected topK"); + assertEquals(0.9, instance1.topP, "Expected topP"); + assertEquals(true, instance1.allowMultipleToolCalls, "Expected allowMultipleToolCalls"); + assertEquals(2, instance1.stopSequences.size(), "Expected stopSequences size"); + assertEquals("\n", instance1.stopSequences.get(0), "Expected stopSequences[0]"); + assertEquals("###", instance1.stopSequences.get(1), "Expected stopSequences[1]"); + assertEquals("value", instance1.additionalProperties.get("customProperty"), "Expected additionalProperties.customProperty"); + assertEquals("anotherValue", instance1.additionalProperties.get("anotherProperty"), "Expected additionalProperties.anotherProperty"); + String yamlRoundtrip1 = instance1.toYaml(); + ModelOptions fromYaml1 = ModelOptions.fromYaml(yamlRoundtrip1); + assertEquals(0.5, fromYaml1.frequencyPenalty, "Expected frequencyPenalty"); + assertEquals(2048, fromYaml1.maxOutputTokens, "Expected maxOutputTokens"); + assertEquals(0.3, fromYaml1.presencePenalty, "Expected presencePenalty"); + assertEquals(42, fromYaml1.seed, "Expected seed"); + assertEquals(0.7, fromYaml1.temperature, "Expected temperature"); + assertEquals(40, fromYaml1.topK, "Expected topK"); + assertEquals(0.9, fromYaml1.topP, "Expected topP"); + assertEquals(true, fromYaml1.allowMultipleToolCalls, "Expected allowMultipleToolCalls"); + assertEquals(2, fromYaml1.stopSequences.size(), "Expected stopSequences size"); + assertEquals("\n", fromYaml1.stopSequences.get(0), "Expected stopSequences[0]"); + assertEquals("###", fromYaml1.stopSequences.get(1), "Expected stopSequences[1]"); + assertEquals("value", fromYaml1.additionalProperties.get("customProperty"), "Expected additionalProperties.customProperty"); + assertEquals("anotherValue", fromYaml1.additionalProperties.get("anotherProperty"), "Expected additionalProperties.anotherProperty"); + ModelOptions reloaded1 = ModelOptions.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals(0.5, reloaded1.frequencyPenalty, "Expected frequencyPenalty"); + assertEquals(2048, reloaded1.maxOutputTokens, "Expected maxOutputTokens"); + assertEquals(0.3, reloaded1.presencePenalty, "Expected presencePenalty"); + assertEquals(42, reloaded1.seed, "Expected seed"); + assertEquals(0.7, reloaded1.temperature, "Expected temperature"); + assertEquals(40, reloaded1.topK, "Expected topK"); + assertEquals(0.9, reloaded1.topP, "Expected topP"); + assertEquals(true, reloaded1.allowMultipleToolCalls, "Expected allowMultipleToolCalls"); + assertEquals(2, reloaded1.stopSequences.size(), "Expected stopSequences size"); + assertEquals("\n", reloaded1.stopSequences.get(0), "Expected stopSequences[0]"); + assertEquals("###", reloaded1.stopSequences.get(1), "Expected stopSequences[1]"); + assertEquals("value", reloaded1.additionalProperties.get("customProperty"), "Expected additionalProperties.customProperty"); + assertEquals("anotherValue", reloaded1.additionalProperties.get("anotherProperty"), "Expected additionalProperties.anotherProperty"); + + assertThrows(() -> ModelOptions.fromJson("{"), "ModelOptions.fromJson should reject malformed JSON"); + + assertThrows(() -> ModelOptions.fromYaml(":\n broken"), "ModelOptions.fromYaml should reject malformed YAML"); + + ModelOptions wireInstance = ModelOptions.fromJson("{\n \"frequencyPenalty\": 0.5,\n \"maxOutputTokens\": 2048,\n \"presencePenalty\": 0.3,\n \"seed\": 42,\n \"temperature\": 0.7,\n \"topK\": 40,\n \"topP\": 0.9,\n \"stopSequences\": [\n \"\\n\",\n \"###\"\n ],\n \"allowMultipleToolCalls\": true,\n \"additionalProperties\": {\n \"customProperty\": \"value\",\n \"anotherProperty\": \"anotherValue\"\n }\n}"); + java.util.Map openaiWire = wireInstance.toWire("openai"); + assertTrue(openaiWire.containsKey("frequency_penalty"), "Expected openai wire output to include frequency_penalty"); + assertTrue(!openaiWire.containsKey("frequencyPenalty"), "Expected openai wire output to omit frequencyPenalty"); + assertTrue(openaiWire.containsKey("max_completion_tokens"), "Expected openai wire output to include max_completion_tokens"); + assertTrue(!openaiWire.containsKey("maxOutputTokens"), "Expected openai wire output to omit maxOutputTokens"); + assertTrue(openaiWire.containsKey("presence_penalty"), "Expected openai wire output to include presence_penalty"); + assertTrue(!openaiWire.containsKey("presencePenalty"), "Expected openai wire output to omit presencePenalty"); + assertTrue(openaiWire.containsKey("seed"), "Expected openai wire output to include seed"); + assertTrue(openaiWire.containsKey("temperature"), "Expected openai wire output to include temperature"); + assertTrue(openaiWire.containsKey("top_k"), "Expected openai wire output to include top_k"); + assertTrue(!openaiWire.containsKey("topK"), "Expected openai wire output to omit topK"); + assertTrue(openaiWire.containsKey("top_p"), "Expected openai wire output to include top_p"); + assertTrue(!openaiWire.containsKey("topP"), "Expected openai wire output to omit topP"); + assertTrue(openaiWire.containsKey("stop"), "Expected openai wire output to include stop"); + assertTrue(!openaiWire.containsKey("stopSequences"), "Expected openai wire output to omit stopSequences"); + assertTrue(openaiWire.containsKey("parallel_tool_calls"), "Expected openai wire output to include parallel_tool_calls"); + assertTrue(!openaiWire.containsKey("allowMultipleToolCalls"), "Expected openai wire output to omit allowMultipleToolCalls"); + java.util.Map responsesWire = wireInstance.toWire("responses"); + assertTrue(responsesWire.containsKey("max_output_tokens"), "Expected responses wire output to include max_output_tokens"); + assertTrue(!responsesWire.containsKey("maxOutputTokens"), "Expected responses wire output to omit maxOutputTokens"); + assertTrue(responsesWire.containsKey("temperature"), "Expected responses wire output to include temperature"); + assertTrue(responsesWire.containsKey("top_p"), "Expected responses wire output to include top_p"); + assertTrue(!responsesWire.containsKey("topP"), "Expected responses wire output to omit topP"); + java.util.Map anthropicWire = wireInstance.toWire("anthropic"); + assertTrue(anthropicWire.containsKey("max_tokens"), "Expected anthropic wire output to include max_tokens"); + assertTrue(!anthropicWire.containsKey("maxOutputTokens"), "Expected anthropic wire output to omit maxOutputTokens"); + assertTrue(anthropicWire.containsKey("temperature"), "Expected anthropic wire output to include temperature"); + assertTrue(anthropicWire.containsKey("top_k"), "Expected anthropic wire output to include top_k"); + assertTrue(!anthropicWire.containsKey("topK"), "Expected anthropic wire output to omit topK"); + assertTrue(anthropicWire.containsKey("top_p"), "Expected anthropic wire output to include top_p"); + assertTrue(!anthropicWire.containsKey("topP"), "Expected anthropic wire output to omit topP"); + assertTrue(anthropicWire.containsKey("stop_sequences"), "Expected anthropic wire output to include stop_sequences"); + assertTrue(!anthropicWire.containsKey("stopSequences"), "Expected anthropic wire output to omit stopSequences"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ModelReconciliationStateGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ModelReconciliationStateGeneratedTest.java new file mode 100644 index 000000000..843c38ff8 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ModelReconciliationStateGeneratedTest.java @@ -0,0 +1,65 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class ModelReconciliationStateGeneratedTest { + private ModelReconciliationStateGeneratedTest() { } + + static void run() { + + // ModelReconciliationState example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "invocationId": "inv_abc123", + "message": "provider connection dropped after request was sent" + } + """; + ModelReconciliationState instance1 = ModelReconciliationState.fromJson(jsonData1); + assertEquals("inv_abc123", instance1.invocationId, "Expected invocationId"); + assertEquals("provider connection dropped after request was sent", instance1.message, "Expected message"); + String yamlRoundtrip1 = instance1.toYaml(); + ModelReconciliationState fromYaml1 = ModelReconciliationState.fromYaml(yamlRoundtrip1); + assertEquals("inv_abc123", fromYaml1.invocationId, "Expected invocationId"); + assertEquals("provider connection dropped after request was sent", fromYaml1.message, "Expected message"); + ModelReconciliationState reloaded1 = ModelReconciliationState.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("inv_abc123", reloaded1.invocationId, "Expected invocationId"); + assertEquals("provider connection dropped after request was sent", reloaded1.message, "Expected message"); + + assertThrows(() -> ModelReconciliationState.fromJson("{"), "ModelReconciliationState.fromJson should reject malformed JSON"); + + assertThrows(() -> ModelReconciliationState.fromYaml(":\n broken"), "ModelReconciliationState.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ModelToolRequestGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ModelToolRequestGeneratedTest.java new file mode 100644 index 000000000..7f3301925 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ModelToolRequestGeneratedTest.java @@ -0,0 +1,65 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class ModelToolRequestGeneratedTest { + private ModelToolRequestGeneratedTest() { } + + static void run() { + + // ModelToolRequest example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "id": "call_abc123", + "name": "get_weather" + } + """; + ModelToolRequest instance1 = ModelToolRequest.fromJson(jsonData1); + assertEquals("call_abc123", instance1.id, "Expected id"); + assertEquals("get_weather", instance1.name, "Expected name"); + String yamlRoundtrip1 = instance1.toYaml(); + ModelToolRequest fromYaml1 = ModelToolRequest.fromYaml(yamlRoundtrip1); + assertEquals("call_abc123", fromYaml1.id, "Expected id"); + assertEquals("get_weather", fromYaml1.name, "Expected name"); + ModelToolRequest reloaded1 = ModelToolRequest.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("call_abc123", reloaded1.id, "Expected id"); + assertEquals("get_weather", reloaded1.name, "Expected name"); + + assertThrows(() -> ModelToolRequest.fromJson("{"), "ModelToolRequest.fromJson should reject malformed JSON"); + + assertThrows(() -> ModelToolRequest.fromYaml(":\n broken"), "ModelToolRequest.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ModelToolResultGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ModelToolResultGeneratedTest.java new file mode 100644 index 000000000..8759968a6 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ModelToolResultGeneratedTest.java @@ -0,0 +1,65 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class ModelToolResultGeneratedTest { + private ModelToolResultGeneratedTest() { } + + static void run() { + + // ModelToolResult example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "requestId": "call_abc123", + "name": "get_weather" + } + """; + ModelToolResult instance1 = ModelToolResult.fromJson(jsonData1); + assertEquals("call_abc123", instance1.requestId, "Expected requestId"); + assertEquals("get_weather", instance1.name, "Expected name"); + String yamlRoundtrip1 = instance1.toYaml(); + ModelToolResult fromYaml1 = ModelToolResult.fromYaml(yamlRoundtrip1); + assertEquals("call_abc123", fromYaml1.requestId, "Expected requestId"); + assertEquals("get_weather", fromYaml1.name, "Expected name"); + ModelToolResult reloaded1 = ModelToolResult.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("call_abc123", reloaded1.requestId, "Expected requestId"); + assertEquals("get_weather", reloaded1.name, "Expected name"); + + assertThrows(() -> ModelToolResult.fromJson("{"), "ModelToolResult.fromJson should reject malformed JSON"); + + assertThrows(() -> ModelToolResult.fromYaml(":\n broken"), "ModelToolResult.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/OAuthConnectionGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/OAuthConnectionGeneratedTest.java new file mode 100644 index 000000000..f5bef2c88 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/OAuthConnectionGeneratedTest.java @@ -0,0 +1,86 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class OAuthConnectionGeneratedTest { + private OAuthConnectionGeneratedTest() { } + + static void run() { + + // OAuthConnection example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "kind": "oauth", + "endpoint": "https://api.example.com", + "clientId": "your-client-id", + "clientSecret": "your-client-secret", + "tokenUrl": "https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token", + "scopes": [ + "https://cognitiveservices.azure.com/.default" + ] + } + """; + OAuthConnection instance1 = OAuthConnection.fromJson(jsonData1); + assertEquals("oauth", instance1.kind, "Expected kind"); + assertEquals("https://api.example.com", instance1.endpoint, "Expected endpoint"); + assertEquals("your-client-id", instance1.clientId, "Expected clientId"); + assertEquals("your-client-secret", instance1.clientSecret, "Expected clientSecret"); + assertEquals("https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token", instance1.tokenUrl, "Expected tokenUrl"); + assertEquals(1, instance1.scopes.size(), "Expected scopes size"); + assertEquals("https://cognitiveservices.azure.com/.default", instance1.scopes.get(0), "Expected scopes[0]"); + String yamlRoundtrip1 = instance1.toYaml(); + OAuthConnection fromYaml1 = OAuthConnection.fromYaml(yamlRoundtrip1); + assertEquals("oauth", fromYaml1.kind, "Expected kind"); + assertEquals("https://api.example.com", fromYaml1.endpoint, "Expected endpoint"); + assertEquals("your-client-id", fromYaml1.clientId, "Expected clientId"); + assertEquals("your-client-secret", fromYaml1.clientSecret, "Expected clientSecret"); + assertEquals("https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token", fromYaml1.tokenUrl, "Expected tokenUrl"); + assertEquals(1, fromYaml1.scopes.size(), "Expected scopes size"); + assertEquals("https://cognitiveservices.azure.com/.default", fromYaml1.scopes.get(0), "Expected scopes[0]"); + OAuthConnection reloaded1 = OAuthConnection.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("oauth", reloaded1.kind, "Expected kind"); + assertEquals("https://api.example.com", reloaded1.endpoint, "Expected endpoint"); + assertEquals("your-client-id", reloaded1.clientId, "Expected clientId"); + assertEquals("your-client-secret", reloaded1.clientSecret, "Expected clientSecret"); + assertEquals("https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token", reloaded1.tokenUrl, "Expected tokenUrl"); + assertEquals(1, reloaded1.scopes.size(), "Expected scopes size"); + assertEquals("https://cognitiveservices.azure.com/.default", reloaded1.scopes.get(0), "Expected scopes[0]"); + + assertThrows(() -> OAuthConnection.fromJson("{"), "OAuthConnection.fromJson should reject malformed JSON"); + + assertThrows(() -> OAuthConnection.fromYaml(":\n broken"), "OAuthConnection.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/OAuthTokenGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/OAuthTokenGeneratedTest.java new file mode 100644 index 000000000..fd6616f81 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/OAuthTokenGeneratedTest.java @@ -0,0 +1,43 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class OAuthTokenGeneratedTest { + private OAuthTokenGeneratedTest() { } + + static void run() { + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ObjectPropertyGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ObjectPropertyGeneratedTest.java new file mode 100644 index 000000000..f2a749875 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ObjectPropertyGeneratedTest.java @@ -0,0 +1,65 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class ObjectPropertyGeneratedTest { + private ObjectPropertyGeneratedTest() { } + + static void run() { + + // ObjectProperty example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "properties": { + "property1": { + "kind": "string" + }, + "property2": { + "kind": "number" + } + } + } + """; + ObjectProperty instance1 = ObjectProperty.fromJson(jsonData1); + String yamlRoundtrip1 = instance1.toYaml(); + ObjectProperty fromYaml1 = ObjectProperty.fromYaml(yamlRoundtrip1); + ObjectProperty reloaded1 = ObjectProperty.load(instance1.save(new SaveContext()), new LoadContext()); + + assertThrows(() -> ObjectProperty.fromJson("{"), "ObjectProperty.fromJson should reject malformed JSON"); + + assertThrows(() -> ObjectProperty.fromYaml(":\n broken"), "ObjectProperty.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/OpenApiToolGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/OpenApiToolGeneratedTest.java new file mode 100644 index 000000000..7b66581d7 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/OpenApiToolGeneratedTest.java @@ -0,0 +1,68 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class OpenApiToolGeneratedTest { + private OpenApiToolGeneratedTest() { } + + static void run() { + + // OpenApiTool example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "kind": "openapi", + "connection": { + "kind": "reference" + }, + "specification": "./openapi.json" + } + """; + OpenApiTool instance1 = OpenApiTool.fromJson(jsonData1); + assertEquals("openapi", instance1.kind, "Expected kind"); + assertEquals("./openapi.json", instance1.specification, "Expected specification"); + String yamlRoundtrip1 = instance1.toYaml(); + OpenApiTool fromYaml1 = OpenApiTool.fromYaml(yamlRoundtrip1); + assertEquals("openapi", fromYaml1.kind, "Expected kind"); + assertEquals("./openapi.json", fromYaml1.specification, "Expected specification"); + OpenApiTool reloaded1 = OpenApiTool.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("openapi", reloaded1.kind, "Expected kind"); + assertEquals("./openapi.json", reloaded1.specification, "Expected specification"); + + assertThrows(() -> OpenApiTool.fromJson("{"), "OpenApiTool.fromJson should reject malformed JSON"); + + assertThrows(() -> OpenApiTool.fromYaml(":\n broken"), "OpenApiTool.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ParserConfigGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ParserConfigGeneratedTest.java new file mode 100644 index 000000000..6b15d5228 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ParserConfigGeneratedTest.java @@ -0,0 +1,67 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class ParserConfigGeneratedTest { + private ParserConfigGeneratedTest() { } + + static void run() { + + // ParserConfig example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "kind": "prompty", + "options": { + "key": "value" + } + } + """; + ParserConfig instance1 = ParserConfig.fromJson(jsonData1); + assertEquals("prompty", instance1.kind, "Expected kind"); + assertEquals("value", instance1.options.get("key"), "Expected options.key"); + String yamlRoundtrip1 = instance1.toYaml(); + ParserConfig fromYaml1 = ParserConfig.fromYaml(yamlRoundtrip1); + assertEquals("prompty", fromYaml1.kind, "Expected kind"); + assertEquals("value", fromYaml1.options.get("key"), "Expected options.key"); + ParserConfig reloaded1 = ParserConfig.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("prompty", reloaded1.kind, "Expected kind"); + assertEquals("value", reloaded1.options.get("key"), "Expected options.key"); + + assertThrows(() -> ParserConfig.fromJson("{"), "ParserConfig.fromJson should reject malformed JSON"); + + assertThrows(() -> ParserConfig.fromYaml(":\n broken"), "ParserConfig.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/PermissionCompletedPayloadGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/PermissionCompletedPayloadGeneratedTest.java new file mode 100644 index 000000000..c6e029d84 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/PermissionCompletedPayloadGeneratedTest.java @@ -0,0 +1,77 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class PermissionCompletedPayloadGeneratedTest { + private PermissionCompletedPayloadGeneratedTest() { } + + static void run() { + + // PermissionCompletedPayload example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "requestId": "perm_abc123", + "toolCallId": "call_abc123", + "permission": "tool.execute", + "approved": true, + "reason": "user_approved" + } + """; + PermissionCompletedPayload instance1 = PermissionCompletedPayload.fromJson(jsonData1); + assertEquals("perm_abc123", instance1.requestId, "Expected requestId"); + assertEquals("call_abc123", instance1.toolCallId, "Expected toolCallId"); + assertEquals("tool.execute", instance1.permission, "Expected permission"); + assertEquals(true, instance1.approved, "Expected approved"); + assertEquals("user_approved", instance1.reason, "Expected reason"); + String yamlRoundtrip1 = instance1.toYaml(); + PermissionCompletedPayload fromYaml1 = PermissionCompletedPayload.fromYaml(yamlRoundtrip1); + assertEquals("perm_abc123", fromYaml1.requestId, "Expected requestId"); + assertEquals("call_abc123", fromYaml1.toolCallId, "Expected toolCallId"); + assertEquals("tool.execute", fromYaml1.permission, "Expected permission"); + assertEquals(true, fromYaml1.approved, "Expected approved"); + assertEquals("user_approved", fromYaml1.reason, "Expected reason"); + PermissionCompletedPayload reloaded1 = PermissionCompletedPayload.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("perm_abc123", reloaded1.requestId, "Expected requestId"); + assertEquals("call_abc123", reloaded1.toolCallId, "Expected toolCallId"); + assertEquals("tool.execute", reloaded1.permission, "Expected permission"); + assertEquals(true, reloaded1.approved, "Expected approved"); + assertEquals("user_approved", reloaded1.reason, "Expected reason"); + + assertThrows(() -> PermissionCompletedPayload.fromJson("{"), "PermissionCompletedPayload.fromJson should reject malformed JSON"); + + assertThrows(() -> PermissionCompletedPayload.fromYaml(":\n broken"), "PermissionCompletedPayload.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/PermissionDecisionGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/PermissionDecisionGeneratedTest.java new file mode 100644 index 000000000..0c81b9f05 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/PermissionDecisionGeneratedTest.java @@ -0,0 +1,77 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class PermissionDecisionGeneratedTest { + private PermissionDecisionGeneratedTest() { } + + static void run() { + + // PermissionDecision example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "requestId": "perm_abc123", + "toolCallId": "call_abc123", + "permission": "tool.execute", + "approved": true, + "reason": "user_approved" + } + """; + PermissionDecision instance1 = PermissionDecision.fromJson(jsonData1); + assertEquals("perm_abc123", instance1.requestId, "Expected requestId"); + assertEquals("call_abc123", instance1.toolCallId, "Expected toolCallId"); + assertEquals("tool.execute", instance1.permission, "Expected permission"); + assertEquals(true, instance1.approved, "Expected approved"); + assertEquals("user_approved", instance1.reason, "Expected reason"); + String yamlRoundtrip1 = instance1.toYaml(); + PermissionDecision fromYaml1 = PermissionDecision.fromYaml(yamlRoundtrip1); + assertEquals("perm_abc123", fromYaml1.requestId, "Expected requestId"); + assertEquals("call_abc123", fromYaml1.toolCallId, "Expected toolCallId"); + assertEquals("tool.execute", fromYaml1.permission, "Expected permission"); + assertEquals(true, fromYaml1.approved, "Expected approved"); + assertEquals("user_approved", fromYaml1.reason, "Expected reason"); + PermissionDecision reloaded1 = PermissionDecision.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("perm_abc123", reloaded1.requestId, "Expected requestId"); + assertEquals("call_abc123", reloaded1.toolCallId, "Expected toolCallId"); + assertEquals("tool.execute", reloaded1.permission, "Expected permission"); + assertEquals(true, reloaded1.approved, "Expected approved"); + assertEquals("user_approved", reloaded1.reason, "Expected reason"); + + assertThrows(() -> PermissionDecision.fromJson("{"), "PermissionDecision.fromJson should reject malformed JSON"); + + assertThrows(() -> PermissionDecision.fromYaml(":\n broken"), "PermissionDecision.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/PermissionRequestGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/PermissionRequestGeneratedTest.java new file mode 100644 index 000000000..e4b3f0f29 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/PermissionRequestGeneratedTest.java @@ -0,0 +1,77 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class PermissionRequestGeneratedTest { + private PermissionRequestGeneratedTest() { } + + static void run() { + + // PermissionRequest example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "requestId": "perm_abc123", + "toolCallId": "call_abc123", + "permission": "tool.execute", + "target": "shell", + "promptRequest": "Allow shell to run tests?" + } + """; + PermissionRequest instance1 = PermissionRequest.fromJson(jsonData1); + assertEquals("perm_abc123", instance1.requestId, "Expected requestId"); + assertEquals("call_abc123", instance1.toolCallId, "Expected toolCallId"); + assertEquals("tool.execute", instance1.permission, "Expected permission"); + assertEquals("shell", instance1.target, "Expected target"); + assertEquals("Allow shell to run tests?", instance1.promptRequest, "Expected promptRequest"); + String yamlRoundtrip1 = instance1.toYaml(); + PermissionRequest fromYaml1 = PermissionRequest.fromYaml(yamlRoundtrip1); + assertEquals("perm_abc123", fromYaml1.requestId, "Expected requestId"); + assertEquals("call_abc123", fromYaml1.toolCallId, "Expected toolCallId"); + assertEquals("tool.execute", fromYaml1.permission, "Expected permission"); + assertEquals("shell", fromYaml1.target, "Expected target"); + assertEquals("Allow shell to run tests?", fromYaml1.promptRequest, "Expected promptRequest"); + PermissionRequest reloaded1 = PermissionRequest.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("perm_abc123", reloaded1.requestId, "Expected requestId"); + assertEquals("call_abc123", reloaded1.toolCallId, "Expected toolCallId"); + assertEquals("tool.execute", reloaded1.permission, "Expected permission"); + assertEquals("shell", reloaded1.target, "Expected target"); + assertEquals("Allow shell to run tests?", reloaded1.promptRequest, "Expected promptRequest"); + + assertThrows(() -> PermissionRequest.fromJson("{"), "PermissionRequest.fromJson should reject malformed JSON"); + + assertThrows(() -> PermissionRequest.fromYaml(":\n broken"), "PermissionRequest.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/PermissionRequestedPayloadGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/PermissionRequestedPayloadGeneratedTest.java new file mode 100644 index 000000000..559ee45ad --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/PermissionRequestedPayloadGeneratedTest.java @@ -0,0 +1,77 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class PermissionRequestedPayloadGeneratedTest { + private PermissionRequestedPayloadGeneratedTest() { } + + static void run() { + + // PermissionRequestedPayload example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "requestId": "perm_abc123", + "toolCallId": "call_abc123", + "permission": "tool.execute", + "target": "shell", + "promptRequest": "Allow shell to run tests?" + } + """; + PermissionRequestedPayload instance1 = PermissionRequestedPayload.fromJson(jsonData1); + assertEquals("perm_abc123", instance1.requestId, "Expected requestId"); + assertEquals("call_abc123", instance1.toolCallId, "Expected toolCallId"); + assertEquals("tool.execute", instance1.permission, "Expected permission"); + assertEquals("shell", instance1.target, "Expected target"); + assertEquals("Allow shell to run tests?", instance1.promptRequest, "Expected promptRequest"); + String yamlRoundtrip1 = instance1.toYaml(); + PermissionRequestedPayload fromYaml1 = PermissionRequestedPayload.fromYaml(yamlRoundtrip1); + assertEquals("perm_abc123", fromYaml1.requestId, "Expected requestId"); + assertEquals("call_abc123", fromYaml1.toolCallId, "Expected toolCallId"); + assertEquals("tool.execute", fromYaml1.permission, "Expected permission"); + assertEquals("shell", fromYaml1.target, "Expected target"); + assertEquals("Allow shell to run tests?", fromYaml1.promptRequest, "Expected promptRequest"); + PermissionRequestedPayload reloaded1 = PermissionRequestedPayload.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("perm_abc123", reloaded1.requestId, "Expected requestId"); + assertEquals("call_abc123", reloaded1.toolCallId, "Expected toolCallId"); + assertEquals("tool.execute", reloaded1.permission, "Expected permission"); + assertEquals("shell", reloaded1.target, "Expected target"); + assertEquals("Allow shell to run tests?", reloaded1.promptRequest, "Expected promptRequest"); + + assertThrows(() -> PermissionRequestedPayload.fromJson("{"), "PermissionRequestedPayload.fromJson should reject malformed JSON"); + + assertThrows(() -> PermissionRequestedPayload.fromYaml(":\n broken"), "PermissionRequestedPayload.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ProjectInfoGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ProjectInfoGeneratedTest.java new file mode 100644 index 000000000..3c447cf15 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ProjectInfoGeneratedTest.java @@ -0,0 +1,43 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class ProjectInfoGeneratedTest { + private ProjectInfoGeneratedTest() { } + + static void run() { + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/PromptyGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/PromptyGeneratedTest.java new file mode 100644 index 000000000..9b385549c --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/PromptyGeneratedTest.java @@ -0,0 +1,979 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class PromptyGeneratedTest { + private PromptyGeneratedTest() { } + + static void run() { + + // Prompty example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "name": "basic-prompt", + "displayName": "Basic Prompt", + "description": "A basic prompt that uses the GPT-3 chat API to answer questions", + "metadata": { + "authors": [ + "sethjuarez", + "jietong" + ], + "tags": [ + "example", + "prompt" + ] + }, + "inputs": { + "firstName": { + "kind": "string", + "default": "Jane" + }, + "lastName": { + "kind": "string", + "default": "Doe" + }, + "question": { + "kind": "string", + "default": "What is the meaning of life?" + } + }, + "outputs": { + "answer": { + "kind": "string", + "description": "The answer to the user's question." + } + }, + "model": { + "id": "gpt-35-turbo", + "connection": { + "kind": "key", + "endpoint": "https://{your-custom-endpoint}.openai.azure.com/", + "apiKey": "{your-api-key}" + } + }, + "tools": [ + { + "name": "getCurrentWeather", + "kind": "function", + "description": "Get the current weather in a given location", + "parameters": { + "location": { + "kind": "string", + "description": "The city and state, e.g. San Francisco, CA" + }, + "unit": { + "kind": "string", + "description": "The unit of temperature, e.g. Celsius or Fahrenheit" + } + } + } + ], + "template": { + "format": "mustache", + "parser": "prompty" + }, + "instructions": "system:\\nYou are an AI assistant who helps people find information.\\nAs the assistant, you answer questions briefly, succinctly,\\nand in a personable manner using markdown and even add some \\npersonal flair with appropriate emojis.\\n\\n# Customer\\nYou are helping {{firstName}} {{lastName}} to find answers to \\ntheir questions. Use their name to address them in your responses.\\nuser:\\n{{question}}" + } + """; + Prompty instance1 = Prompty.fromJson(jsonData1); + assertEquals("basic-prompt", instance1.name, "Expected name"); + assertEquals("Basic Prompt", instance1.displayName, "Expected displayName"); + assertEquals("A basic prompt that uses the GPT-3 chat API to answer questions", instance1.description, "Expected description"); + assertEquals("system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some \npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to \ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}", instance1.instructions, "Expected instructions"); + assertEquals("gpt-35-turbo", instance1.model.id, "Expected instance1.model.id"); + assertEquals(1, instance1.tools.size(), "Expected tools size"); + assertTrue(instance1.tools.get(0) instanceof FunctionTool, "Expected tools[0] to be FunctionTool"); + FunctionTool instance1Tools0Value = (FunctionTool) instance1.tools.get(0); + assertEquals("function", instance1Tools0Value.kind, "Expected kind"); + String yamlRoundtrip1 = instance1.toYaml(); + Prompty fromYaml1 = Prompty.fromYaml(yamlRoundtrip1); + assertEquals("basic-prompt", fromYaml1.name, "Expected name"); + assertEquals("Basic Prompt", fromYaml1.displayName, "Expected displayName"); + assertEquals("A basic prompt that uses the GPT-3 chat API to answer questions", fromYaml1.description, "Expected description"); + assertEquals("system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some \npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to \ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}", fromYaml1.instructions, "Expected instructions"); + assertEquals("gpt-35-turbo", fromYaml1.model.id, "Expected fromYaml1.model.id"); + assertEquals(1, fromYaml1.tools.size(), "Expected tools size"); + assertTrue(fromYaml1.tools.get(0) instanceof FunctionTool, "Expected tools[0] to be FunctionTool"); + FunctionTool fromYaml1Tools0Value = (FunctionTool) fromYaml1.tools.get(0); + assertEquals("function", fromYaml1Tools0Value.kind, "Expected kind"); + Prompty reloaded1 = Prompty.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("basic-prompt", reloaded1.name, "Expected name"); + assertEquals("Basic Prompt", reloaded1.displayName, "Expected displayName"); + assertEquals("A basic prompt that uses the GPT-3 chat API to answer questions", reloaded1.description, "Expected description"); + assertEquals("system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some \npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to \ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}", reloaded1.instructions, "Expected instructions"); + assertEquals("gpt-35-turbo", reloaded1.model.id, "Expected reloaded1.model.id"); + assertEquals(1, reloaded1.tools.size(), "Expected tools size"); + assertTrue(reloaded1.tools.get(0) instanceof FunctionTool, "Expected tools[0] to be FunctionTool"); + FunctionTool reloaded1Tools0Value = (FunctionTool) reloaded1.tools.get(0); + assertEquals("function", reloaded1Tools0Value.kind, "Expected kind"); + + // Prompty example 2: fromJson, fromYaml, save, and reload + String jsonData2 = """ + { + "name": "basic-prompt", + "displayName": "Basic Prompt", + "description": "A basic prompt that uses the GPT-3 chat API to answer questions", + "metadata": { + "authors": [ + "sethjuarez", + "jietong" + ], + "tags": [ + "example", + "prompt" + ] + }, + "inputs": { + "firstName": { + "kind": "string", + "default": "Jane" + }, + "lastName": { + "kind": "string", + "default": "Doe" + }, + "question": { + "kind": "string", + "default": "What is the meaning of life?" + } + }, + "outputs": { + "answer": { + "kind": "string", + "description": "The answer to the user's question." + } + }, + "model": { + "id": "gpt-35-turbo", + "connection": { + "kind": "key", + "endpoint": "https://{your-custom-endpoint}.openai.azure.com/", + "apiKey": "{your-api-key}" + } + }, + "tools": { + "getCurrentWeather": { + "kind": "function", + "description": "Get the current weather in a given location", + "parameters": { + "location": { + "kind": "string", + "description": "The city and state, e.g. San Francisco, CA" + }, + "unit": { + "kind": "string", + "description": "The unit of temperature, e.g. Celsius or Fahrenheit" + } + } + } + }, + "template": { + "format": "mustache", + "parser": "prompty" + }, + "instructions": "system:\\nYou are an AI assistant who helps people find information.\\nAs the assistant, you answer questions briefly, succinctly,\\nand in a personable manner using markdown and even add some \\npersonal flair with appropriate emojis.\\n\\n# Customer\\nYou are helping {{firstName}} {{lastName}} to find answers to \\ntheir questions. Use their name to address them in your responses.\\nuser:\\n{{question}}" + } + """; + Prompty instance2 = Prompty.fromJson(jsonData2); + assertEquals("basic-prompt", instance2.name, "Expected name"); + assertEquals("Basic Prompt", instance2.displayName, "Expected displayName"); + assertEquals("A basic prompt that uses the GPT-3 chat API to answer questions", instance2.description, "Expected description"); + assertEquals("system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some \npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to \ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}", instance2.instructions, "Expected instructions"); + assertEquals("gpt-35-turbo", instance2.model.id, "Expected instance2.model.id"); + String yamlRoundtrip2 = instance2.toYaml(); + Prompty fromYaml2 = Prompty.fromYaml(yamlRoundtrip2); + assertEquals("basic-prompt", fromYaml2.name, "Expected name"); + assertEquals("Basic Prompt", fromYaml2.displayName, "Expected displayName"); + assertEquals("A basic prompt that uses the GPT-3 chat API to answer questions", fromYaml2.description, "Expected description"); + assertEquals("system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some \npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to \ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}", fromYaml2.instructions, "Expected instructions"); + assertEquals("gpt-35-turbo", fromYaml2.model.id, "Expected fromYaml2.model.id"); + Prompty reloaded2 = Prompty.load(instance2.save(new SaveContext()), new LoadContext()); + assertEquals("basic-prompt", reloaded2.name, "Expected name"); + assertEquals("Basic Prompt", reloaded2.displayName, "Expected displayName"); + assertEquals("A basic prompt that uses the GPT-3 chat API to answer questions", reloaded2.description, "Expected description"); + assertEquals("system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some \npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to \ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}", reloaded2.instructions, "Expected instructions"); + assertEquals("gpt-35-turbo", reloaded2.model.id, "Expected reloaded2.model.id"); + + // Prompty example 3: fromJson, fromYaml, save, and reload + String jsonData3 = """ + { + "name": "basic-prompt", + "displayName": "Basic Prompt", + "description": "A basic prompt that uses the GPT-3 chat API to answer questions", + "metadata": { + "authors": [ + "sethjuarez", + "jietong" + ], + "tags": [ + "example", + "prompt" + ] + }, + "inputs": { + "firstName": { + "kind": "string", + "default": "Jane" + }, + "lastName": { + "kind": "string", + "default": "Doe" + }, + "question": { + "kind": "string", + "default": "What is the meaning of life?" + } + }, + "outputs": [ + { + "name": "answer", + "kind": "string", + "description": "The answer to the user's question." + } + ], + "model": { + "id": "gpt-35-turbo", + "connection": { + "kind": "key", + "endpoint": "https://{your-custom-endpoint}.openai.azure.com/", + "apiKey": "{your-api-key}" + } + }, + "tools": [ + { + "name": "getCurrentWeather", + "kind": "function", + "description": "Get the current weather in a given location", + "parameters": { + "location": { + "kind": "string", + "description": "The city and state, e.g. San Francisco, CA" + }, + "unit": { + "kind": "string", + "description": "The unit of temperature, e.g. Celsius or Fahrenheit" + } + } + } + ], + "template": { + "format": "mustache", + "parser": "prompty" + }, + "instructions": "system:\\nYou are an AI assistant who helps people find information.\\nAs the assistant, you answer questions briefly, succinctly,\\nand in a personable manner using markdown and even add some \\npersonal flair with appropriate emojis.\\n\\n# Customer\\nYou are helping {{firstName}} {{lastName}} to find answers to \\ntheir questions. Use their name to address them in your responses.\\nuser:\\n{{question}}" + } + """; + Prompty instance3 = Prompty.fromJson(jsonData3); + assertEquals("basic-prompt", instance3.name, "Expected name"); + assertEquals("Basic Prompt", instance3.displayName, "Expected displayName"); + assertEquals("A basic prompt that uses the GPT-3 chat API to answer questions", instance3.description, "Expected description"); + assertEquals("system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some \npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to \ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}", instance3.instructions, "Expected instructions"); + assertEquals(1, instance3.outputs.size(), "Expected outputs size"); + assertEquals("answer", instance3.outputs.get(0).name, "Expected instance3.outputs.get(0).name"); + assertEquals("string", instance3.outputs.get(0).kind, "Expected instance3.outputs.get(0).kind"); + assertEquals("The answer to the user's question.", instance3.outputs.get(0).description, "Expected instance3.outputs.get(0).description"); + assertEquals("gpt-35-turbo", instance3.model.id, "Expected instance3.model.id"); + assertEquals(1, instance3.tools.size(), "Expected tools size"); + assertTrue(instance3.tools.get(0) instanceof FunctionTool, "Expected tools[0] to be FunctionTool"); + FunctionTool instance3Tools0Value = (FunctionTool) instance3.tools.get(0); + assertEquals("function", instance3Tools0Value.kind, "Expected kind"); + String yamlRoundtrip3 = instance3.toYaml(); + Prompty fromYaml3 = Prompty.fromYaml(yamlRoundtrip3); + assertEquals("basic-prompt", fromYaml3.name, "Expected name"); + assertEquals("Basic Prompt", fromYaml3.displayName, "Expected displayName"); + assertEquals("A basic prompt that uses the GPT-3 chat API to answer questions", fromYaml3.description, "Expected description"); + assertEquals("system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some \npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to \ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}", fromYaml3.instructions, "Expected instructions"); + assertEquals(1, fromYaml3.outputs.size(), "Expected outputs size"); + assertEquals("answer", fromYaml3.outputs.get(0).name, "Expected fromYaml3.outputs.get(0).name"); + assertEquals("string", fromYaml3.outputs.get(0).kind, "Expected fromYaml3.outputs.get(0).kind"); + assertEquals("The answer to the user's question.", fromYaml3.outputs.get(0).description, "Expected fromYaml3.outputs.get(0).description"); + assertEquals("gpt-35-turbo", fromYaml3.model.id, "Expected fromYaml3.model.id"); + assertEquals(1, fromYaml3.tools.size(), "Expected tools size"); + assertTrue(fromYaml3.tools.get(0) instanceof FunctionTool, "Expected tools[0] to be FunctionTool"); + FunctionTool fromYaml3Tools0Value = (FunctionTool) fromYaml3.tools.get(0); + assertEquals("function", fromYaml3Tools0Value.kind, "Expected kind"); + Prompty reloaded3 = Prompty.load(instance3.save(new SaveContext()), new LoadContext()); + assertEquals("basic-prompt", reloaded3.name, "Expected name"); + assertEquals("Basic Prompt", reloaded3.displayName, "Expected displayName"); + assertEquals("A basic prompt that uses the GPT-3 chat API to answer questions", reloaded3.description, "Expected description"); + assertEquals("system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some \npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to \ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}", reloaded3.instructions, "Expected instructions"); + assertEquals(1, reloaded3.outputs.size(), "Expected outputs size"); + assertEquals("answer", reloaded3.outputs.get(0).name, "Expected reloaded3.outputs.get(0).name"); + assertEquals("string", reloaded3.outputs.get(0).kind, "Expected reloaded3.outputs.get(0).kind"); + assertEquals("The answer to the user's question.", reloaded3.outputs.get(0).description, "Expected reloaded3.outputs.get(0).description"); + assertEquals("gpt-35-turbo", reloaded3.model.id, "Expected reloaded3.model.id"); + assertEquals(1, reloaded3.tools.size(), "Expected tools size"); + assertTrue(reloaded3.tools.get(0) instanceof FunctionTool, "Expected tools[0] to be FunctionTool"); + FunctionTool reloaded3Tools0Value = (FunctionTool) reloaded3.tools.get(0); + assertEquals("function", reloaded3Tools0Value.kind, "Expected kind"); + + // Prompty example 4: fromJson, fromYaml, save, and reload + String jsonData4 = """ + { + "name": "basic-prompt", + "displayName": "Basic Prompt", + "description": "A basic prompt that uses the GPT-3 chat API to answer questions", + "metadata": { + "authors": [ + "sethjuarez", + "jietong" + ], + "tags": [ + "example", + "prompt" + ] + }, + "inputs": { + "firstName": { + "kind": "string", + "default": "Jane" + }, + "lastName": { + "kind": "string", + "default": "Doe" + }, + "question": { + "kind": "string", + "default": "What is the meaning of life?" + } + }, + "outputs": [ + { + "name": "answer", + "kind": "string", + "description": "The answer to the user's question." + } + ], + "model": { + "id": "gpt-35-turbo", + "connection": { + "kind": "key", + "endpoint": "https://{your-custom-endpoint}.openai.azure.com/", + "apiKey": "{your-api-key}" + } + }, + "tools": { + "getCurrentWeather": { + "kind": "function", + "description": "Get the current weather in a given location", + "parameters": { + "location": { + "kind": "string", + "description": "The city and state, e.g. San Francisco, CA" + }, + "unit": { + "kind": "string", + "description": "The unit of temperature, e.g. Celsius or Fahrenheit" + } + } + } + }, + "template": { + "format": "mustache", + "parser": "prompty" + }, + "instructions": "system:\\nYou are an AI assistant who helps people find information.\\nAs the assistant, you answer questions briefly, succinctly,\\nand in a personable manner using markdown and even add some \\npersonal flair with appropriate emojis.\\n\\n# Customer\\nYou are helping {{firstName}} {{lastName}} to find answers to \\ntheir questions. Use their name to address them in your responses.\\nuser:\\n{{question}}" + } + """; + Prompty instance4 = Prompty.fromJson(jsonData4); + assertEquals("basic-prompt", instance4.name, "Expected name"); + assertEquals("Basic Prompt", instance4.displayName, "Expected displayName"); + assertEquals("A basic prompt that uses the GPT-3 chat API to answer questions", instance4.description, "Expected description"); + assertEquals("system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some \npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to \ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}", instance4.instructions, "Expected instructions"); + assertEquals(1, instance4.outputs.size(), "Expected outputs size"); + assertEquals("answer", instance4.outputs.get(0).name, "Expected instance4.outputs.get(0).name"); + assertEquals("string", instance4.outputs.get(0).kind, "Expected instance4.outputs.get(0).kind"); + assertEquals("The answer to the user's question.", instance4.outputs.get(0).description, "Expected instance4.outputs.get(0).description"); + assertEquals("gpt-35-turbo", instance4.model.id, "Expected instance4.model.id"); + String yamlRoundtrip4 = instance4.toYaml(); + Prompty fromYaml4 = Prompty.fromYaml(yamlRoundtrip4); + assertEquals("basic-prompt", fromYaml4.name, "Expected name"); + assertEquals("Basic Prompt", fromYaml4.displayName, "Expected displayName"); + assertEquals("A basic prompt that uses the GPT-3 chat API to answer questions", fromYaml4.description, "Expected description"); + assertEquals("system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some \npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to \ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}", fromYaml4.instructions, "Expected instructions"); + assertEquals(1, fromYaml4.outputs.size(), "Expected outputs size"); + assertEquals("answer", fromYaml4.outputs.get(0).name, "Expected fromYaml4.outputs.get(0).name"); + assertEquals("string", fromYaml4.outputs.get(0).kind, "Expected fromYaml4.outputs.get(0).kind"); + assertEquals("The answer to the user's question.", fromYaml4.outputs.get(0).description, "Expected fromYaml4.outputs.get(0).description"); + assertEquals("gpt-35-turbo", fromYaml4.model.id, "Expected fromYaml4.model.id"); + Prompty reloaded4 = Prompty.load(instance4.save(new SaveContext()), new LoadContext()); + assertEquals("basic-prompt", reloaded4.name, "Expected name"); + assertEquals("Basic Prompt", reloaded4.displayName, "Expected displayName"); + assertEquals("A basic prompt that uses the GPT-3 chat API to answer questions", reloaded4.description, "Expected description"); + assertEquals("system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some \npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to \ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}", reloaded4.instructions, "Expected instructions"); + assertEquals(1, reloaded4.outputs.size(), "Expected outputs size"); + assertEquals("answer", reloaded4.outputs.get(0).name, "Expected reloaded4.outputs.get(0).name"); + assertEquals("string", reloaded4.outputs.get(0).kind, "Expected reloaded4.outputs.get(0).kind"); + assertEquals("The answer to the user's question.", reloaded4.outputs.get(0).description, "Expected reloaded4.outputs.get(0).description"); + assertEquals("gpt-35-turbo", reloaded4.model.id, "Expected reloaded4.model.id"); + + // Prompty example 5: fromJson, fromYaml, save, and reload + String jsonData5 = """ + { + "name": "basic-prompt", + "displayName": "Basic Prompt", + "description": "A basic prompt that uses the GPT-3 chat API to answer questions", + "metadata": { + "authors": [ + "sethjuarez", + "jietong" + ], + "tags": [ + "example", + "prompt" + ] + }, + "inputs": [ + { + "name": "firstName", + "kind": "string", + "default": "Jane" + }, + { + "name": "lastName", + "kind": "string", + "default": "Doe" + }, + { + "name": "question", + "kind": "string", + "default": "What is the meaning of life?" + } + ], + "outputs": { + "answer": { + "kind": "string", + "description": "The answer to the user's question." + } + }, + "model": { + "id": "gpt-35-turbo", + "connection": { + "kind": "key", + "endpoint": "https://{your-custom-endpoint}.openai.azure.com/", + "apiKey": "{your-api-key}" + } + }, + "tools": [ + { + "name": "getCurrentWeather", + "kind": "function", + "description": "Get the current weather in a given location", + "parameters": { + "location": { + "kind": "string", + "description": "The city and state, e.g. San Francisco, CA" + }, + "unit": { + "kind": "string", + "description": "The unit of temperature, e.g. Celsius or Fahrenheit" + } + } + } + ], + "template": { + "format": "mustache", + "parser": "prompty" + }, + "instructions": "system:\\nYou are an AI assistant who helps people find information.\\nAs the assistant, you answer questions briefly, succinctly,\\nand in a personable manner using markdown and even add some \\npersonal flair with appropriate emojis.\\n\\n# Customer\\nYou are helping {{firstName}} {{lastName}} to find answers to \\ntheir questions. Use their name to address them in your responses.\\nuser:\\n{{question}}" + } + """; + Prompty instance5 = Prompty.fromJson(jsonData5); + assertEquals("basic-prompt", instance5.name, "Expected name"); + assertEquals("Basic Prompt", instance5.displayName, "Expected displayName"); + assertEquals("A basic prompt that uses the GPT-3 chat API to answer questions", instance5.description, "Expected description"); + assertEquals("system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some \npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to \ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}", instance5.instructions, "Expected instructions"); + assertEquals(3, instance5.inputs.size(), "Expected inputs size"); + assertEquals("firstName", instance5.inputs.get(0).name, "Expected instance5.inputs.get(0).name"); + assertEquals("string", instance5.inputs.get(0).kind, "Expected instance5.inputs.get(0).kind"); + assertEquals("Jane", instance5.inputs.get(0).defaultValue, "Expected instance5.inputs.get(0).default"); + assertEquals("lastName", instance5.inputs.get(1).name, "Expected instance5.inputs.get(1).name"); + assertEquals("string", instance5.inputs.get(1).kind, "Expected instance5.inputs.get(1).kind"); + assertEquals("Doe", instance5.inputs.get(1).defaultValue, "Expected instance5.inputs.get(1).default"); + assertEquals("question", instance5.inputs.get(2).name, "Expected instance5.inputs.get(2).name"); + assertEquals("string", instance5.inputs.get(2).kind, "Expected instance5.inputs.get(2).kind"); + assertEquals("What is the meaning of life?", instance5.inputs.get(2).defaultValue, "Expected instance5.inputs.get(2).default"); + assertEquals("gpt-35-turbo", instance5.model.id, "Expected instance5.model.id"); + assertEquals(1, instance5.tools.size(), "Expected tools size"); + assertTrue(instance5.tools.get(0) instanceof FunctionTool, "Expected tools[0] to be FunctionTool"); + FunctionTool instance5Tools0Value = (FunctionTool) instance5.tools.get(0); + assertEquals("function", instance5Tools0Value.kind, "Expected kind"); + String yamlRoundtrip5 = instance5.toYaml(); + Prompty fromYaml5 = Prompty.fromYaml(yamlRoundtrip5); + assertEquals("basic-prompt", fromYaml5.name, "Expected name"); + assertEquals("Basic Prompt", fromYaml5.displayName, "Expected displayName"); + assertEquals("A basic prompt that uses the GPT-3 chat API to answer questions", fromYaml5.description, "Expected description"); + assertEquals("system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some \npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to \ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}", fromYaml5.instructions, "Expected instructions"); + assertEquals(3, fromYaml5.inputs.size(), "Expected inputs size"); + assertEquals("firstName", fromYaml5.inputs.get(0).name, "Expected fromYaml5.inputs.get(0).name"); + assertEquals("string", fromYaml5.inputs.get(0).kind, "Expected fromYaml5.inputs.get(0).kind"); + assertEquals("Jane", fromYaml5.inputs.get(0).defaultValue, "Expected fromYaml5.inputs.get(0).default"); + assertEquals("lastName", fromYaml5.inputs.get(1).name, "Expected fromYaml5.inputs.get(1).name"); + assertEquals("string", fromYaml5.inputs.get(1).kind, "Expected fromYaml5.inputs.get(1).kind"); + assertEquals("Doe", fromYaml5.inputs.get(1).defaultValue, "Expected fromYaml5.inputs.get(1).default"); + assertEquals("question", fromYaml5.inputs.get(2).name, "Expected fromYaml5.inputs.get(2).name"); + assertEquals("string", fromYaml5.inputs.get(2).kind, "Expected fromYaml5.inputs.get(2).kind"); + assertEquals("What is the meaning of life?", fromYaml5.inputs.get(2).defaultValue, "Expected fromYaml5.inputs.get(2).default"); + assertEquals("gpt-35-turbo", fromYaml5.model.id, "Expected fromYaml5.model.id"); + assertEquals(1, fromYaml5.tools.size(), "Expected tools size"); + assertTrue(fromYaml5.tools.get(0) instanceof FunctionTool, "Expected tools[0] to be FunctionTool"); + FunctionTool fromYaml5Tools0Value = (FunctionTool) fromYaml5.tools.get(0); + assertEquals("function", fromYaml5Tools0Value.kind, "Expected kind"); + Prompty reloaded5 = Prompty.load(instance5.save(new SaveContext()), new LoadContext()); + assertEquals("basic-prompt", reloaded5.name, "Expected name"); + assertEquals("Basic Prompt", reloaded5.displayName, "Expected displayName"); + assertEquals("A basic prompt that uses the GPT-3 chat API to answer questions", reloaded5.description, "Expected description"); + assertEquals("system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some \npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to \ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}", reloaded5.instructions, "Expected instructions"); + assertEquals(3, reloaded5.inputs.size(), "Expected inputs size"); + assertEquals("firstName", reloaded5.inputs.get(0).name, "Expected reloaded5.inputs.get(0).name"); + assertEquals("string", reloaded5.inputs.get(0).kind, "Expected reloaded5.inputs.get(0).kind"); + assertEquals("Jane", reloaded5.inputs.get(0).defaultValue, "Expected reloaded5.inputs.get(0).default"); + assertEquals("lastName", reloaded5.inputs.get(1).name, "Expected reloaded5.inputs.get(1).name"); + assertEquals("string", reloaded5.inputs.get(1).kind, "Expected reloaded5.inputs.get(1).kind"); + assertEquals("Doe", reloaded5.inputs.get(1).defaultValue, "Expected reloaded5.inputs.get(1).default"); + assertEquals("question", reloaded5.inputs.get(2).name, "Expected reloaded5.inputs.get(2).name"); + assertEquals("string", reloaded5.inputs.get(2).kind, "Expected reloaded5.inputs.get(2).kind"); + assertEquals("What is the meaning of life?", reloaded5.inputs.get(2).defaultValue, "Expected reloaded5.inputs.get(2).default"); + assertEquals("gpt-35-turbo", reloaded5.model.id, "Expected reloaded5.model.id"); + assertEquals(1, reloaded5.tools.size(), "Expected tools size"); + assertTrue(reloaded5.tools.get(0) instanceof FunctionTool, "Expected tools[0] to be FunctionTool"); + FunctionTool reloaded5Tools0Value = (FunctionTool) reloaded5.tools.get(0); + assertEquals("function", reloaded5Tools0Value.kind, "Expected kind"); + + // Prompty example 6: fromJson, fromYaml, save, and reload + String jsonData6 = """ + { + "name": "basic-prompt", + "displayName": "Basic Prompt", + "description": "A basic prompt that uses the GPT-3 chat API to answer questions", + "metadata": { + "authors": [ + "sethjuarez", + "jietong" + ], + "tags": [ + "example", + "prompt" + ] + }, + "inputs": [ + { + "name": "firstName", + "kind": "string", + "default": "Jane" + }, + { + "name": "lastName", + "kind": "string", + "default": "Doe" + }, + { + "name": "question", + "kind": "string", + "default": "What is the meaning of life?" + } + ], + "outputs": { + "answer": { + "kind": "string", + "description": "The answer to the user's question." + } + }, + "model": { + "id": "gpt-35-turbo", + "connection": { + "kind": "key", + "endpoint": "https://{your-custom-endpoint}.openai.azure.com/", + "apiKey": "{your-api-key}" + } + }, + "tools": { + "getCurrentWeather": { + "kind": "function", + "description": "Get the current weather in a given location", + "parameters": { + "location": { + "kind": "string", + "description": "The city and state, e.g. San Francisco, CA" + }, + "unit": { + "kind": "string", + "description": "The unit of temperature, e.g. Celsius or Fahrenheit" + } + } + } + }, + "template": { + "format": "mustache", + "parser": "prompty" + }, + "instructions": "system:\\nYou are an AI assistant who helps people find information.\\nAs the assistant, you answer questions briefly, succinctly,\\nand in a personable manner using markdown and even add some \\npersonal flair with appropriate emojis.\\n\\n# Customer\\nYou are helping {{firstName}} {{lastName}} to find answers to \\ntheir questions. Use their name to address them in your responses.\\nuser:\\n{{question}}" + } + """; + Prompty instance6 = Prompty.fromJson(jsonData6); + assertEquals("basic-prompt", instance6.name, "Expected name"); + assertEquals("Basic Prompt", instance6.displayName, "Expected displayName"); + assertEquals("A basic prompt that uses the GPT-3 chat API to answer questions", instance6.description, "Expected description"); + assertEquals("system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some \npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to \ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}", instance6.instructions, "Expected instructions"); + assertEquals(3, instance6.inputs.size(), "Expected inputs size"); + assertEquals("firstName", instance6.inputs.get(0).name, "Expected instance6.inputs.get(0).name"); + assertEquals("string", instance6.inputs.get(0).kind, "Expected instance6.inputs.get(0).kind"); + assertEquals("Jane", instance6.inputs.get(0).defaultValue, "Expected instance6.inputs.get(0).default"); + assertEquals("lastName", instance6.inputs.get(1).name, "Expected instance6.inputs.get(1).name"); + assertEquals("string", instance6.inputs.get(1).kind, "Expected instance6.inputs.get(1).kind"); + assertEquals("Doe", instance6.inputs.get(1).defaultValue, "Expected instance6.inputs.get(1).default"); + assertEquals("question", instance6.inputs.get(2).name, "Expected instance6.inputs.get(2).name"); + assertEquals("string", instance6.inputs.get(2).kind, "Expected instance6.inputs.get(2).kind"); + assertEquals("What is the meaning of life?", instance6.inputs.get(2).defaultValue, "Expected instance6.inputs.get(2).default"); + assertEquals("gpt-35-turbo", instance6.model.id, "Expected instance6.model.id"); + String yamlRoundtrip6 = instance6.toYaml(); + Prompty fromYaml6 = Prompty.fromYaml(yamlRoundtrip6); + assertEquals("basic-prompt", fromYaml6.name, "Expected name"); + assertEquals("Basic Prompt", fromYaml6.displayName, "Expected displayName"); + assertEquals("A basic prompt that uses the GPT-3 chat API to answer questions", fromYaml6.description, "Expected description"); + assertEquals("system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some \npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to \ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}", fromYaml6.instructions, "Expected instructions"); + assertEquals(3, fromYaml6.inputs.size(), "Expected inputs size"); + assertEquals("firstName", fromYaml6.inputs.get(0).name, "Expected fromYaml6.inputs.get(0).name"); + assertEquals("string", fromYaml6.inputs.get(0).kind, "Expected fromYaml6.inputs.get(0).kind"); + assertEquals("Jane", fromYaml6.inputs.get(0).defaultValue, "Expected fromYaml6.inputs.get(0).default"); + assertEquals("lastName", fromYaml6.inputs.get(1).name, "Expected fromYaml6.inputs.get(1).name"); + assertEquals("string", fromYaml6.inputs.get(1).kind, "Expected fromYaml6.inputs.get(1).kind"); + assertEquals("Doe", fromYaml6.inputs.get(1).defaultValue, "Expected fromYaml6.inputs.get(1).default"); + assertEquals("question", fromYaml6.inputs.get(2).name, "Expected fromYaml6.inputs.get(2).name"); + assertEquals("string", fromYaml6.inputs.get(2).kind, "Expected fromYaml6.inputs.get(2).kind"); + assertEquals("What is the meaning of life?", fromYaml6.inputs.get(2).defaultValue, "Expected fromYaml6.inputs.get(2).default"); + assertEquals("gpt-35-turbo", fromYaml6.model.id, "Expected fromYaml6.model.id"); + Prompty reloaded6 = Prompty.load(instance6.save(new SaveContext()), new LoadContext()); + assertEquals("basic-prompt", reloaded6.name, "Expected name"); + assertEquals("Basic Prompt", reloaded6.displayName, "Expected displayName"); + assertEquals("A basic prompt that uses the GPT-3 chat API to answer questions", reloaded6.description, "Expected description"); + assertEquals("system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some \npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to \ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}", reloaded6.instructions, "Expected instructions"); + assertEquals(3, reloaded6.inputs.size(), "Expected inputs size"); + assertEquals("firstName", reloaded6.inputs.get(0).name, "Expected reloaded6.inputs.get(0).name"); + assertEquals("string", reloaded6.inputs.get(0).kind, "Expected reloaded6.inputs.get(0).kind"); + assertEquals("Jane", reloaded6.inputs.get(0).defaultValue, "Expected reloaded6.inputs.get(0).default"); + assertEquals("lastName", reloaded6.inputs.get(1).name, "Expected reloaded6.inputs.get(1).name"); + assertEquals("string", reloaded6.inputs.get(1).kind, "Expected reloaded6.inputs.get(1).kind"); + assertEquals("Doe", reloaded6.inputs.get(1).defaultValue, "Expected reloaded6.inputs.get(1).default"); + assertEquals("question", reloaded6.inputs.get(2).name, "Expected reloaded6.inputs.get(2).name"); + assertEquals("string", reloaded6.inputs.get(2).kind, "Expected reloaded6.inputs.get(2).kind"); + assertEquals("What is the meaning of life?", reloaded6.inputs.get(2).defaultValue, "Expected reloaded6.inputs.get(2).default"); + assertEquals("gpt-35-turbo", reloaded6.model.id, "Expected reloaded6.model.id"); + + // Prompty example 7: fromJson, fromYaml, save, and reload + String jsonData7 = """ + { + "name": "basic-prompt", + "displayName": "Basic Prompt", + "description": "A basic prompt that uses the GPT-3 chat API to answer questions", + "metadata": { + "authors": [ + "sethjuarez", + "jietong" + ], + "tags": [ + "example", + "prompt" + ] + }, + "inputs": [ + { + "name": "firstName", + "kind": "string", + "default": "Jane" + }, + { + "name": "lastName", + "kind": "string", + "default": "Doe" + }, + { + "name": "question", + "kind": "string", + "default": "What is the meaning of life?" + } + ], + "outputs": [ + { + "name": "answer", + "kind": "string", + "description": "The answer to the user's question." + } + ], + "model": { + "id": "gpt-35-turbo", + "connection": { + "kind": "key", + "endpoint": "https://{your-custom-endpoint}.openai.azure.com/", + "apiKey": "{your-api-key}" + } + }, + "tools": [ + { + "name": "getCurrentWeather", + "kind": "function", + "description": "Get the current weather in a given location", + "parameters": { + "location": { + "kind": "string", + "description": "The city and state, e.g. San Francisco, CA" + }, + "unit": { + "kind": "string", + "description": "The unit of temperature, e.g. Celsius or Fahrenheit" + } + } + } + ], + "template": { + "format": "mustache", + "parser": "prompty" + }, + "instructions": "system:\\nYou are an AI assistant who helps people find information.\\nAs the assistant, you answer questions briefly, succinctly,\\nand in a personable manner using markdown and even add some \\npersonal flair with appropriate emojis.\\n\\n# Customer\\nYou are helping {{firstName}} {{lastName}} to find answers to \\ntheir questions. Use their name to address them in your responses.\\nuser:\\n{{question}}" + } + """; + Prompty instance7 = Prompty.fromJson(jsonData7); + assertEquals("basic-prompt", instance7.name, "Expected name"); + assertEquals("Basic Prompt", instance7.displayName, "Expected displayName"); + assertEquals("A basic prompt that uses the GPT-3 chat API to answer questions", instance7.description, "Expected description"); + assertEquals("system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some \npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to \ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}", instance7.instructions, "Expected instructions"); + assertEquals(3, instance7.inputs.size(), "Expected inputs size"); + assertEquals("firstName", instance7.inputs.get(0).name, "Expected instance7.inputs.get(0).name"); + assertEquals("string", instance7.inputs.get(0).kind, "Expected instance7.inputs.get(0).kind"); + assertEquals("Jane", instance7.inputs.get(0).defaultValue, "Expected instance7.inputs.get(0).default"); + assertEquals("lastName", instance7.inputs.get(1).name, "Expected instance7.inputs.get(1).name"); + assertEquals("string", instance7.inputs.get(1).kind, "Expected instance7.inputs.get(1).kind"); + assertEquals("Doe", instance7.inputs.get(1).defaultValue, "Expected instance7.inputs.get(1).default"); + assertEquals("question", instance7.inputs.get(2).name, "Expected instance7.inputs.get(2).name"); + assertEquals("string", instance7.inputs.get(2).kind, "Expected instance7.inputs.get(2).kind"); + assertEquals("What is the meaning of life?", instance7.inputs.get(2).defaultValue, "Expected instance7.inputs.get(2).default"); + assertEquals(1, instance7.outputs.size(), "Expected outputs size"); + assertEquals("answer", instance7.outputs.get(0).name, "Expected instance7.outputs.get(0).name"); + assertEquals("string", instance7.outputs.get(0).kind, "Expected instance7.outputs.get(0).kind"); + assertEquals("The answer to the user's question.", instance7.outputs.get(0).description, "Expected instance7.outputs.get(0).description"); + assertEquals("gpt-35-turbo", instance7.model.id, "Expected instance7.model.id"); + assertEquals(1, instance7.tools.size(), "Expected tools size"); + assertTrue(instance7.tools.get(0) instanceof FunctionTool, "Expected tools[0] to be FunctionTool"); + FunctionTool instance7Tools0Value = (FunctionTool) instance7.tools.get(0); + assertEquals("function", instance7Tools0Value.kind, "Expected kind"); + String yamlRoundtrip7 = instance7.toYaml(); + Prompty fromYaml7 = Prompty.fromYaml(yamlRoundtrip7); + assertEquals("basic-prompt", fromYaml7.name, "Expected name"); + assertEquals("Basic Prompt", fromYaml7.displayName, "Expected displayName"); + assertEquals("A basic prompt that uses the GPT-3 chat API to answer questions", fromYaml7.description, "Expected description"); + assertEquals("system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some \npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to \ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}", fromYaml7.instructions, "Expected instructions"); + assertEquals(3, fromYaml7.inputs.size(), "Expected inputs size"); + assertEquals("firstName", fromYaml7.inputs.get(0).name, "Expected fromYaml7.inputs.get(0).name"); + assertEquals("string", fromYaml7.inputs.get(0).kind, "Expected fromYaml7.inputs.get(0).kind"); + assertEquals("Jane", fromYaml7.inputs.get(0).defaultValue, "Expected fromYaml7.inputs.get(0).default"); + assertEquals("lastName", fromYaml7.inputs.get(1).name, "Expected fromYaml7.inputs.get(1).name"); + assertEquals("string", fromYaml7.inputs.get(1).kind, "Expected fromYaml7.inputs.get(1).kind"); + assertEquals("Doe", fromYaml7.inputs.get(1).defaultValue, "Expected fromYaml7.inputs.get(1).default"); + assertEquals("question", fromYaml7.inputs.get(2).name, "Expected fromYaml7.inputs.get(2).name"); + assertEquals("string", fromYaml7.inputs.get(2).kind, "Expected fromYaml7.inputs.get(2).kind"); + assertEquals("What is the meaning of life?", fromYaml7.inputs.get(2).defaultValue, "Expected fromYaml7.inputs.get(2).default"); + assertEquals(1, fromYaml7.outputs.size(), "Expected outputs size"); + assertEquals("answer", fromYaml7.outputs.get(0).name, "Expected fromYaml7.outputs.get(0).name"); + assertEquals("string", fromYaml7.outputs.get(0).kind, "Expected fromYaml7.outputs.get(0).kind"); + assertEquals("The answer to the user's question.", fromYaml7.outputs.get(0).description, "Expected fromYaml7.outputs.get(0).description"); + assertEquals("gpt-35-turbo", fromYaml7.model.id, "Expected fromYaml7.model.id"); + assertEquals(1, fromYaml7.tools.size(), "Expected tools size"); + assertTrue(fromYaml7.tools.get(0) instanceof FunctionTool, "Expected tools[0] to be FunctionTool"); + FunctionTool fromYaml7Tools0Value = (FunctionTool) fromYaml7.tools.get(0); + assertEquals("function", fromYaml7Tools0Value.kind, "Expected kind"); + Prompty reloaded7 = Prompty.load(instance7.save(new SaveContext()), new LoadContext()); + assertEquals("basic-prompt", reloaded7.name, "Expected name"); + assertEquals("Basic Prompt", reloaded7.displayName, "Expected displayName"); + assertEquals("A basic prompt that uses the GPT-3 chat API to answer questions", reloaded7.description, "Expected description"); + assertEquals("system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some \npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to \ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}", reloaded7.instructions, "Expected instructions"); + assertEquals(3, reloaded7.inputs.size(), "Expected inputs size"); + assertEquals("firstName", reloaded7.inputs.get(0).name, "Expected reloaded7.inputs.get(0).name"); + assertEquals("string", reloaded7.inputs.get(0).kind, "Expected reloaded7.inputs.get(0).kind"); + assertEquals("Jane", reloaded7.inputs.get(0).defaultValue, "Expected reloaded7.inputs.get(0).default"); + assertEquals("lastName", reloaded7.inputs.get(1).name, "Expected reloaded7.inputs.get(1).name"); + assertEquals("string", reloaded7.inputs.get(1).kind, "Expected reloaded7.inputs.get(1).kind"); + assertEquals("Doe", reloaded7.inputs.get(1).defaultValue, "Expected reloaded7.inputs.get(1).default"); + assertEquals("question", reloaded7.inputs.get(2).name, "Expected reloaded7.inputs.get(2).name"); + assertEquals("string", reloaded7.inputs.get(2).kind, "Expected reloaded7.inputs.get(2).kind"); + assertEquals("What is the meaning of life?", reloaded7.inputs.get(2).defaultValue, "Expected reloaded7.inputs.get(2).default"); + assertEquals(1, reloaded7.outputs.size(), "Expected outputs size"); + assertEquals("answer", reloaded7.outputs.get(0).name, "Expected reloaded7.outputs.get(0).name"); + assertEquals("string", reloaded7.outputs.get(0).kind, "Expected reloaded7.outputs.get(0).kind"); + assertEquals("The answer to the user's question.", reloaded7.outputs.get(0).description, "Expected reloaded7.outputs.get(0).description"); + assertEquals("gpt-35-turbo", reloaded7.model.id, "Expected reloaded7.model.id"); + assertEquals(1, reloaded7.tools.size(), "Expected tools size"); + assertTrue(reloaded7.tools.get(0) instanceof FunctionTool, "Expected tools[0] to be FunctionTool"); + FunctionTool reloaded7Tools0Value = (FunctionTool) reloaded7.tools.get(0); + assertEquals("function", reloaded7Tools0Value.kind, "Expected kind"); + + // Prompty example 8: fromJson, fromYaml, save, and reload + String jsonData8 = """ + { + "name": "basic-prompt", + "displayName": "Basic Prompt", + "description": "A basic prompt that uses the GPT-3 chat API to answer questions", + "metadata": { + "authors": [ + "sethjuarez", + "jietong" + ], + "tags": [ + "example", + "prompt" + ] + }, + "inputs": [ + { + "name": "firstName", + "kind": "string", + "default": "Jane" + }, + { + "name": "lastName", + "kind": "string", + "default": "Doe" + }, + { + "name": "question", + "kind": "string", + "default": "What is the meaning of life?" + } + ], + "outputs": [ + { + "name": "answer", + "kind": "string", + "description": "The answer to the user's question." + } + ], + "model": { + "id": "gpt-35-turbo", + "connection": { + "kind": "key", + "endpoint": "https://{your-custom-endpoint}.openai.azure.com/", + "apiKey": "{your-api-key}" + } + }, + "tools": { + "getCurrentWeather": { + "kind": "function", + "description": "Get the current weather in a given location", + "parameters": { + "location": { + "kind": "string", + "description": "The city and state, e.g. San Francisco, CA" + }, + "unit": { + "kind": "string", + "description": "The unit of temperature, e.g. Celsius or Fahrenheit" + } + } + } + }, + "template": { + "format": "mustache", + "parser": "prompty" + }, + "instructions": "system:\\nYou are an AI assistant who helps people find information.\\nAs the assistant, you answer questions briefly, succinctly,\\nand in a personable manner using markdown and even add some \\npersonal flair with appropriate emojis.\\n\\n# Customer\\nYou are helping {{firstName}} {{lastName}} to find answers to \\ntheir questions. Use their name to address them in your responses.\\nuser:\\n{{question}}" + } + """; + Prompty instance8 = Prompty.fromJson(jsonData8); + assertEquals("basic-prompt", instance8.name, "Expected name"); + assertEquals("Basic Prompt", instance8.displayName, "Expected displayName"); + assertEquals("A basic prompt that uses the GPT-3 chat API to answer questions", instance8.description, "Expected description"); + assertEquals("system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some \npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to \ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}", instance8.instructions, "Expected instructions"); + assertEquals(3, instance8.inputs.size(), "Expected inputs size"); + assertEquals("firstName", instance8.inputs.get(0).name, "Expected instance8.inputs.get(0).name"); + assertEquals("string", instance8.inputs.get(0).kind, "Expected instance8.inputs.get(0).kind"); + assertEquals("Jane", instance8.inputs.get(0).defaultValue, "Expected instance8.inputs.get(0).default"); + assertEquals("lastName", instance8.inputs.get(1).name, "Expected instance8.inputs.get(1).name"); + assertEquals("string", instance8.inputs.get(1).kind, "Expected instance8.inputs.get(1).kind"); + assertEquals("Doe", instance8.inputs.get(1).defaultValue, "Expected instance8.inputs.get(1).default"); + assertEquals("question", instance8.inputs.get(2).name, "Expected instance8.inputs.get(2).name"); + assertEquals("string", instance8.inputs.get(2).kind, "Expected instance8.inputs.get(2).kind"); + assertEquals("What is the meaning of life?", instance8.inputs.get(2).defaultValue, "Expected instance8.inputs.get(2).default"); + assertEquals(1, instance8.outputs.size(), "Expected outputs size"); + assertEquals("answer", instance8.outputs.get(0).name, "Expected instance8.outputs.get(0).name"); + assertEquals("string", instance8.outputs.get(0).kind, "Expected instance8.outputs.get(0).kind"); + assertEquals("The answer to the user's question.", instance8.outputs.get(0).description, "Expected instance8.outputs.get(0).description"); + assertEquals("gpt-35-turbo", instance8.model.id, "Expected instance8.model.id"); + String yamlRoundtrip8 = instance8.toYaml(); + Prompty fromYaml8 = Prompty.fromYaml(yamlRoundtrip8); + assertEquals("basic-prompt", fromYaml8.name, "Expected name"); + assertEquals("Basic Prompt", fromYaml8.displayName, "Expected displayName"); + assertEquals("A basic prompt that uses the GPT-3 chat API to answer questions", fromYaml8.description, "Expected description"); + assertEquals("system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some \npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to \ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}", fromYaml8.instructions, "Expected instructions"); + assertEquals(3, fromYaml8.inputs.size(), "Expected inputs size"); + assertEquals("firstName", fromYaml8.inputs.get(0).name, "Expected fromYaml8.inputs.get(0).name"); + assertEquals("string", fromYaml8.inputs.get(0).kind, "Expected fromYaml8.inputs.get(0).kind"); + assertEquals("Jane", fromYaml8.inputs.get(0).defaultValue, "Expected fromYaml8.inputs.get(0).default"); + assertEquals("lastName", fromYaml8.inputs.get(1).name, "Expected fromYaml8.inputs.get(1).name"); + assertEquals("string", fromYaml8.inputs.get(1).kind, "Expected fromYaml8.inputs.get(1).kind"); + assertEquals("Doe", fromYaml8.inputs.get(1).defaultValue, "Expected fromYaml8.inputs.get(1).default"); + assertEquals("question", fromYaml8.inputs.get(2).name, "Expected fromYaml8.inputs.get(2).name"); + assertEquals("string", fromYaml8.inputs.get(2).kind, "Expected fromYaml8.inputs.get(2).kind"); + assertEquals("What is the meaning of life?", fromYaml8.inputs.get(2).defaultValue, "Expected fromYaml8.inputs.get(2).default"); + assertEquals(1, fromYaml8.outputs.size(), "Expected outputs size"); + assertEquals("answer", fromYaml8.outputs.get(0).name, "Expected fromYaml8.outputs.get(0).name"); + assertEquals("string", fromYaml8.outputs.get(0).kind, "Expected fromYaml8.outputs.get(0).kind"); + assertEquals("The answer to the user's question.", fromYaml8.outputs.get(0).description, "Expected fromYaml8.outputs.get(0).description"); + assertEquals("gpt-35-turbo", fromYaml8.model.id, "Expected fromYaml8.model.id"); + Prompty reloaded8 = Prompty.load(instance8.save(new SaveContext()), new LoadContext()); + assertEquals("basic-prompt", reloaded8.name, "Expected name"); + assertEquals("Basic Prompt", reloaded8.displayName, "Expected displayName"); + assertEquals("A basic prompt that uses the GPT-3 chat API to answer questions", reloaded8.description, "Expected description"); + assertEquals("system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some \npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to \ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}", reloaded8.instructions, "Expected instructions"); + assertEquals(3, reloaded8.inputs.size(), "Expected inputs size"); + assertEquals("firstName", reloaded8.inputs.get(0).name, "Expected reloaded8.inputs.get(0).name"); + assertEquals("string", reloaded8.inputs.get(0).kind, "Expected reloaded8.inputs.get(0).kind"); + assertEquals("Jane", reloaded8.inputs.get(0).defaultValue, "Expected reloaded8.inputs.get(0).default"); + assertEquals("lastName", reloaded8.inputs.get(1).name, "Expected reloaded8.inputs.get(1).name"); + assertEquals("string", reloaded8.inputs.get(1).kind, "Expected reloaded8.inputs.get(1).kind"); + assertEquals("Doe", reloaded8.inputs.get(1).defaultValue, "Expected reloaded8.inputs.get(1).default"); + assertEquals("question", reloaded8.inputs.get(2).name, "Expected reloaded8.inputs.get(2).name"); + assertEquals("string", reloaded8.inputs.get(2).kind, "Expected reloaded8.inputs.get(2).kind"); + assertEquals("What is the meaning of life?", reloaded8.inputs.get(2).defaultValue, "Expected reloaded8.inputs.get(2).default"); + assertEquals(1, reloaded8.outputs.size(), "Expected outputs size"); + assertEquals("answer", reloaded8.outputs.get(0).name, "Expected reloaded8.outputs.get(0).name"); + assertEquals("string", reloaded8.outputs.get(0).kind, "Expected reloaded8.outputs.get(0).kind"); + assertEquals("The answer to the user's question.", reloaded8.outputs.get(0).description, "Expected reloaded8.outputs.get(0).description"); + assertEquals("gpt-35-turbo", reloaded8.model.id, "Expected reloaded8.model.id"); + + assertThrows(() -> Prompty.fromJson("{"), "Prompty.fromJson should reject malformed JSON"); + + assertThrows(() -> Prompty.fromYaml(":\n broken"), "Prompty.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/PromptyToolGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/PromptyToolGeneratedTest.java new file mode 100644 index 000000000..e6d81a9b4 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/PromptyToolGeneratedTest.java @@ -0,0 +1,69 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class PromptyToolGeneratedTest { + private PromptyToolGeneratedTest() { } + + static void run() { + + // PromptyTool example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "kind": "prompty", + "path": "./summarize.prompty", + "mode": "single" + } + """; + PromptyTool instance1 = PromptyTool.fromJson(jsonData1); + assertEquals("prompty", instance1.kind, "Expected kind"); + assertEquals("./summarize.prompty", instance1.path, "Expected path"); + assertEquals("single", instance1.mode, "Expected mode"); + String yamlRoundtrip1 = instance1.toYaml(); + PromptyTool fromYaml1 = PromptyTool.fromYaml(yamlRoundtrip1); + assertEquals("prompty", fromYaml1.kind, "Expected kind"); + assertEquals("./summarize.prompty", fromYaml1.path, "Expected path"); + assertEquals("single", fromYaml1.mode, "Expected mode"); + PromptyTool reloaded1 = PromptyTool.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("prompty", reloaded1.kind, "Expected kind"); + assertEquals("./summarize.prompty", reloaded1.path, "Expected path"); + assertEquals("single", reloaded1.mode, "Expected mode"); + + assertThrows(() -> PromptyTool.fromJson("{"), "PromptyTool.fromJson should reject malformed JSON"); + + assertThrows(() -> PromptyTool.fromYaml(":\n broken"), "PromptyTool.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/PropertyGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/PropertyGeneratedTest.java new file mode 100644 index 000000000..83b7eeb35 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/PropertyGeneratedTest.java @@ -0,0 +1,102 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class PropertyGeneratedTest { + private PropertyGeneratedTest() { } + + static void run() { + + // Property example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "name": "my-input", + "kind": "string", + "description": "A description of the input property", + "required": true, + "nullable": true, + "default": "default value", + "example": "example value", + "enumValues": [ + "value1", + "value2", + "value3" + ] + } + """; + Property instance1 = Property.fromJson(jsonData1); + assertEquals("my-input", instance1.name, "Expected name"); + assertEquals("string", instance1.kind, "Expected kind"); + assertEquals("A description of the input property", instance1.description, "Expected description"); + assertEquals(true, instance1.required, "Expected required"); + assertEquals(true, instance1.nullable, "Expected nullable"); + assertEquals("default value", instance1.defaultValue, "Expected default"); + assertEquals("example value", instance1.example, "Expected example"); + assertEquals(3, instance1.enumValues.size(), "Expected enumValues size"); + assertEquals("value1", instance1.enumValues.get(0), "Expected enumValues[0]"); + assertEquals("value2", instance1.enumValues.get(1), "Expected enumValues[1]"); + assertEquals("value3", instance1.enumValues.get(2), "Expected enumValues[2]"); + String yamlRoundtrip1 = instance1.toYaml(); + Property fromYaml1 = Property.fromYaml(yamlRoundtrip1); + assertEquals("my-input", fromYaml1.name, "Expected name"); + assertEquals("string", fromYaml1.kind, "Expected kind"); + assertEquals("A description of the input property", fromYaml1.description, "Expected description"); + assertEquals(true, fromYaml1.required, "Expected required"); + assertEquals(true, fromYaml1.nullable, "Expected nullable"); + assertEquals("default value", fromYaml1.defaultValue, "Expected default"); + assertEquals("example value", fromYaml1.example, "Expected example"); + assertEquals(3, fromYaml1.enumValues.size(), "Expected enumValues size"); + assertEquals("value1", fromYaml1.enumValues.get(0), "Expected enumValues[0]"); + assertEquals("value2", fromYaml1.enumValues.get(1), "Expected enumValues[1]"); + assertEquals("value3", fromYaml1.enumValues.get(2), "Expected enumValues[2]"); + Property reloaded1 = Property.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("my-input", reloaded1.name, "Expected name"); + assertEquals("string", reloaded1.kind, "Expected kind"); + assertEquals("A description of the input property", reloaded1.description, "Expected description"); + assertEquals(true, reloaded1.required, "Expected required"); + assertEquals(true, reloaded1.nullable, "Expected nullable"); + assertEquals("default value", reloaded1.defaultValue, "Expected default"); + assertEquals("example value", reloaded1.example, "Expected example"); + assertEquals(3, reloaded1.enumValues.size(), "Expected enumValues size"); + assertEquals("value1", reloaded1.enumValues.get(0), "Expected enumValues[0]"); + assertEquals("value2", reloaded1.enumValues.get(1), "Expected enumValues[1]"); + assertEquals("value3", reloaded1.enumValues.get(2), "Expected enumValues[2]"); + + assertThrows(() -> Property.fromJson("{"), "Property.fromJson should reject malformed JSON"); + + assertThrows(() -> Property.fromYaml(":\n broken"), "Property.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/RedactedFieldGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/RedactedFieldGeneratedTest.java new file mode 100644 index 000000000..15f22f890 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/RedactedFieldGeneratedTest.java @@ -0,0 +1,69 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class RedactedFieldGeneratedTest { + private RedactedFieldGeneratedTest() { } + + static void run() { + + // RedactedField example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "path": "$.arguments.apiKey", + "mode": "redacted", + "reason": "secret" + } + """; + RedactedField instance1 = RedactedField.fromJson(jsonData1); + assertEquals("$.arguments.apiKey", instance1.path, "Expected path"); + assertEquals(RedactionMode.fromValue("redacted"), instance1.mode, "Expected mode"); + assertEquals("secret", instance1.reason, "Expected reason"); + String yamlRoundtrip1 = instance1.toYaml(); + RedactedField fromYaml1 = RedactedField.fromYaml(yamlRoundtrip1); + assertEquals("$.arguments.apiKey", fromYaml1.path, "Expected path"); + assertEquals(RedactionMode.fromValue("redacted"), fromYaml1.mode, "Expected mode"); + assertEquals("secret", fromYaml1.reason, "Expected reason"); + RedactedField reloaded1 = RedactedField.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("$.arguments.apiKey", reloaded1.path, "Expected path"); + assertEquals(RedactionMode.fromValue("redacted"), reloaded1.mode, "Expected mode"); + assertEquals("secret", reloaded1.reason, "Expected reason"); + + assertThrows(() -> RedactedField.fromJson("{"), "RedactedField.fromJson should reject malformed JSON"); + + assertThrows(() -> RedactedField.fromYaml(":\n broken"), "RedactedField.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/RedactionMetadataGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/RedactionMetadataGeneratedTest.java new file mode 100644 index 000000000..dd40891f7 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/RedactionMetadataGeneratedTest.java @@ -0,0 +1,65 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class RedactionMetadataGeneratedTest { + private RedactionMetadataGeneratedTest() { } + + static void run() { + + // RedactionMetadata example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "sanitized": true, + "policy": "default-v1" + } + """; + RedactionMetadata instance1 = RedactionMetadata.fromJson(jsonData1); + assertEquals(true, instance1.sanitized, "Expected sanitized"); + assertEquals("default-v1", instance1.policy, "Expected policy"); + String yamlRoundtrip1 = instance1.toYaml(); + RedactionMetadata fromYaml1 = RedactionMetadata.fromYaml(yamlRoundtrip1); + assertEquals(true, fromYaml1.sanitized, "Expected sanitized"); + assertEquals("default-v1", fromYaml1.policy, "Expected policy"); + RedactionMetadata reloaded1 = RedactionMetadata.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals(true, reloaded1.sanitized, "Expected sanitized"); + assertEquals("default-v1", reloaded1.policy, "Expected policy"); + + assertThrows(() -> RedactionMetadata.fromJson("{"), "RedactionMetadata.fromJson should reject malformed JSON"); + + assertThrows(() -> RedactionMetadata.fromYaml(":\n broken"), "RedactionMetadata.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ReferenceConnectionGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ReferenceConnectionGeneratedTest.java new file mode 100644 index 000000000..8a643fe61 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ReferenceConnectionGeneratedTest.java @@ -0,0 +1,69 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class ReferenceConnectionGeneratedTest { + private ReferenceConnectionGeneratedTest() { } + + static void run() { + + // ReferenceConnection example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "kind": "reference", + "name": "my-reference-connection", + "target": "my-target-resource" + } + """; + ReferenceConnection instance1 = ReferenceConnection.fromJson(jsonData1); + assertEquals("reference", instance1.kind, "Expected kind"); + assertEquals("my-reference-connection", instance1.name, "Expected name"); + assertEquals("my-target-resource", instance1.target, "Expected target"); + String yamlRoundtrip1 = instance1.toYaml(); + ReferenceConnection fromYaml1 = ReferenceConnection.fromYaml(yamlRoundtrip1); + assertEquals("reference", fromYaml1.kind, "Expected kind"); + assertEquals("my-reference-connection", fromYaml1.name, "Expected name"); + assertEquals("my-target-resource", fromYaml1.target, "Expected target"); + ReferenceConnection reloaded1 = ReferenceConnection.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("reference", reloaded1.kind, "Expected kind"); + assertEquals("my-reference-connection", reloaded1.name, "Expected name"); + assertEquals("my-target-resource", reloaded1.target, "Expected target"); + + assertThrows(() -> ReferenceConnection.fromJson("{"), "ReferenceConnection.fromJson should reject malformed JSON"); + + assertThrows(() -> ReferenceConnection.fromYaml(":\n broken"), "ReferenceConnection.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/RemoteConnectionGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/RemoteConnectionGeneratedTest.java new file mode 100644 index 000000000..022f1b183 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/RemoteConnectionGeneratedTest.java @@ -0,0 +1,69 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class RemoteConnectionGeneratedTest { + private RemoteConnectionGeneratedTest() { } + + static void run() { + + // RemoteConnection example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "kind": "remote", + "name": "my-reference-connection", + "endpoint": "https://{your-custom-endpoint}.openai.azure.com/" + } + """; + RemoteConnection instance1 = RemoteConnection.fromJson(jsonData1); + assertEquals("remote", instance1.kind, "Expected kind"); + assertEquals("my-reference-connection", instance1.name, "Expected name"); + assertEquals("https://{your-custom-endpoint}.openai.azure.com/", instance1.endpoint, "Expected endpoint"); + String yamlRoundtrip1 = instance1.toYaml(); + RemoteConnection fromYaml1 = RemoteConnection.fromYaml(yamlRoundtrip1); + assertEquals("remote", fromYaml1.kind, "Expected kind"); + assertEquals("my-reference-connection", fromYaml1.name, "Expected name"); + assertEquals("https://{your-custom-endpoint}.openai.azure.com/", fromYaml1.endpoint, "Expected endpoint"); + RemoteConnection reloaded1 = RemoteConnection.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("remote", reloaded1.kind, "Expected kind"); + assertEquals("my-reference-connection", reloaded1.name, "Expected name"); + assertEquals("https://{your-custom-endpoint}.openai.azure.com/", reloaded1.endpoint, "Expected endpoint"); + + assertThrows(() -> RemoteConnection.fromJson("{"), "RemoteConnection.fromJson should reject malformed JSON"); + + assertThrows(() -> RemoteConnection.fromYaml(":\n broken"), "RemoteConnection.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ReplayJournalRecordGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ReplayJournalRecordGeneratedTest.java new file mode 100644 index 000000000..305214f83 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ReplayJournalRecordGeneratedTest.java @@ -0,0 +1,43 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class ReplayJournalRecordGeneratedTest { + private ReplayJournalRecordGeneratedTest() { } + + static void run() { + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ReplayMismatchGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ReplayMismatchGeneratedTest.java new file mode 100644 index 000000000..fe4a4f146 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ReplayMismatchGeneratedTest.java @@ -0,0 +1,43 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class ReplayMismatchGeneratedTest { + private ReplayMismatchGeneratedTest() { } + + static void run() { + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ReplayVerificationRequestGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ReplayVerificationRequestGeneratedTest.java new file mode 100644 index 000000000..435cecd6b --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ReplayVerificationRequestGeneratedTest.java @@ -0,0 +1,43 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class ReplayVerificationRequestGeneratedTest { + private ReplayVerificationRequestGeneratedTest() { } + + static void run() { + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ReplayVerificationResultGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ReplayVerificationResultGeneratedTest.java new file mode 100644 index 000000000..605a398b0 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ReplayVerificationResultGeneratedTest.java @@ -0,0 +1,43 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class ReplayVerificationResultGeneratedTest { + private ReplayVerificationResultGeneratedTest() { } + + static void run() { + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ResumeContextGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ResumeContextGeneratedTest.java new file mode 100644 index 000000000..2aa5475d7 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ResumeContextGeneratedTest.java @@ -0,0 +1,61 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class ResumeContextGeneratedTest { + private ResumeContextGeneratedTest() { } + + static void run() { + + // ResumeContext example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "lastJournalSequence": 12 + } + """; + ResumeContext instance1 = ResumeContext.fromJson(jsonData1); + assertEquals(12, instance1.lastJournalSequence, "Expected lastJournalSequence"); + String yamlRoundtrip1 = instance1.toYaml(); + ResumeContext fromYaml1 = ResumeContext.fromYaml(yamlRoundtrip1); + assertEquals(12, fromYaml1.lastJournalSequence, "Expected lastJournalSequence"); + ResumeContext reloaded1 = ResumeContext.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals(12, reloaded1.lastJournalSequence, "Expected lastJournalSequence"); + + assertThrows(() -> ResumeContext.fromJson("{"), "ResumeContext.fromJson should reject malformed JSON"); + + assertThrows(() -> ResumeContext.fromYaml(":\n broken"), "ResumeContext.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/RetryPayloadGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/RetryPayloadGeneratedTest.java new file mode 100644 index 000000000..089d15a41 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/RetryPayloadGeneratedTest.java @@ -0,0 +1,77 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class RetryPayloadGeneratedTest { + private RetryPayloadGeneratedTest() { } + + static void run() { + + // RetryPayload example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "operation": "llm", + "attempt": 2, + "maxAttempts": 3, + "delayMs": 1250, + "reason": "rate_limit" + } + """; + RetryPayload instance1 = RetryPayload.fromJson(jsonData1); + assertEquals("llm", instance1.operation, "Expected operation"); + assertEquals(2, instance1.attempt, "Expected attempt"); + assertEquals(3, instance1.maxAttempts, "Expected maxAttempts"); + assertEquals(1250, instance1.delayMs, "Expected delayMs"); + assertEquals("rate_limit", instance1.reason, "Expected reason"); + String yamlRoundtrip1 = instance1.toYaml(); + RetryPayload fromYaml1 = RetryPayload.fromYaml(yamlRoundtrip1); + assertEquals("llm", fromYaml1.operation, "Expected operation"); + assertEquals(2, fromYaml1.attempt, "Expected attempt"); + assertEquals(3, fromYaml1.maxAttempts, "Expected maxAttempts"); + assertEquals(1250, fromYaml1.delayMs, "Expected delayMs"); + assertEquals("rate_limit", fromYaml1.reason, "Expected reason"); + RetryPayload reloaded1 = RetryPayload.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("llm", reloaded1.operation, "Expected operation"); + assertEquals(2, reloaded1.attempt, "Expected attempt"); + assertEquals(3, reloaded1.maxAttempts, "Expected maxAttempts"); + assertEquals(1250, reloaded1.delayMs, "Expected delayMs"); + assertEquals("rate_limit", reloaded1.reason, "Expected reason"); + + assertThrows(() -> RetryPayload.fromJson("{"), "RetryPayload.fromJson should reject malformed JSON"); + + assertThrows(() -> RetryPayload.fromYaml(":\n broken"), "RetryPayload.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/RetryPolicyRequestGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/RetryPolicyRequestGeneratedTest.java new file mode 100644 index 000000000..a786f5975 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/RetryPolicyRequestGeneratedTest.java @@ -0,0 +1,43 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class RetryPolicyRequestGeneratedTest { + private RetryPolicyRequestGeneratedTest() { } + + static void run() { + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/RunTurnRequestGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/RunTurnRequestGeneratedTest.java new file mode 100644 index 000000000..f86a0e3a3 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/RunTurnRequestGeneratedTest.java @@ -0,0 +1,65 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class RunTurnRequestGeneratedTest { + private RunTurnRequestGeneratedTest() { } + + static void run() { + + // RunTurnRequest example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "sessionId": "sess_abc123", + "turnId": "turn_abc123" + } + """; + RunTurnRequest instance1 = RunTurnRequest.fromJson(jsonData1); + assertEquals("sess_abc123", instance1.sessionId, "Expected sessionId"); + assertEquals("turn_abc123", instance1.turnId, "Expected turnId"); + String yamlRoundtrip1 = instance1.toYaml(); + RunTurnRequest fromYaml1 = RunTurnRequest.fromYaml(yamlRoundtrip1); + assertEquals("sess_abc123", fromYaml1.sessionId, "Expected sessionId"); + assertEquals("turn_abc123", fromYaml1.turnId, "Expected turnId"); + RunTurnRequest reloaded1 = RunTurnRequest.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("sess_abc123", reloaded1.sessionId, "Expected sessionId"); + assertEquals("turn_abc123", reloaded1.turnId, "Expected turnId"); + + assertThrows(() -> RunTurnRequest.fromJson("{"), "RunTurnRequest.fromJson should reject malformed JSON"); + + assertThrows(() -> RunTurnRequest.fromYaml(":\n broken"), "RunTurnRequest.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/RunTurnResultGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/RunTurnResultGeneratedTest.java new file mode 100644 index 000000000..948aa84b8 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/RunTurnResultGeneratedTest.java @@ -0,0 +1,69 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class RunTurnResultGeneratedTest { + private RunTurnResultGeneratedTest() { } + + static void run() { + + // RunTurnResult example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "sessionId": "sess_abc123", + "turnId": "turn_abc123", + "iterations": 1 + } + """; + RunTurnResult instance1 = RunTurnResult.fromJson(jsonData1); + assertEquals("sess_abc123", instance1.sessionId, "Expected sessionId"); + assertEquals("turn_abc123", instance1.turnId, "Expected turnId"); + assertEquals(1, instance1.iterations, "Expected iterations"); + String yamlRoundtrip1 = instance1.toYaml(); + RunTurnResult fromYaml1 = RunTurnResult.fromYaml(yamlRoundtrip1); + assertEquals("sess_abc123", fromYaml1.sessionId, "Expected sessionId"); + assertEquals("turn_abc123", fromYaml1.turnId, "Expected turnId"); + assertEquals(1, fromYaml1.iterations, "Expected iterations"); + RunTurnResult reloaded1 = RunTurnResult.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("sess_abc123", reloaded1.sessionId, "Expected sessionId"); + assertEquals("turn_abc123", reloaded1.turnId, "Expected turnId"); + assertEquals(1, reloaded1.iterations, "Expected iterations"); + + assertThrows(() -> RunTurnResult.fromJson("{"), "RunTurnResult.fromJson should reject malformed JSON"); + + assertThrows(() -> RunTurnResult.fromYaml(":\n broken"), "RunTurnResult.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/SessionEndPayloadGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/SessionEndPayloadGeneratedTest.java new file mode 100644 index 000000000..a17a8e930 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/SessionEndPayloadGeneratedTest.java @@ -0,0 +1,73 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class SessionEndPayloadGeneratedTest { + private SessionEndPayloadGeneratedTest() { } + + static void run() { + + // SessionEndPayload example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "sessionId": "sess_abc123", + "status": "success", + "reason": "complete", + "durationMs": 12500 + } + """; + SessionEndPayload instance1 = SessionEndPayload.fromJson(jsonData1); + assertEquals("sess_abc123", instance1.sessionId, "Expected sessionId"); + assertEquals(SessionEndStatus.fromValue("success"), instance1.status, "Expected status"); + assertEquals("complete", instance1.reason, "Expected reason"); + assertEquals(12500, instance1.durationMs, "Expected durationMs"); + String yamlRoundtrip1 = instance1.toYaml(); + SessionEndPayload fromYaml1 = SessionEndPayload.fromYaml(yamlRoundtrip1); + assertEquals("sess_abc123", fromYaml1.sessionId, "Expected sessionId"); + assertEquals(SessionEndStatus.fromValue("success"), fromYaml1.status, "Expected status"); + assertEquals("complete", fromYaml1.reason, "Expected reason"); + assertEquals(12500, fromYaml1.durationMs, "Expected durationMs"); + SessionEndPayload reloaded1 = SessionEndPayload.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("sess_abc123", reloaded1.sessionId, "Expected sessionId"); + assertEquals(SessionEndStatus.fromValue("success"), reloaded1.status, "Expected status"); + assertEquals("complete", reloaded1.reason, "Expected reason"); + assertEquals(12500, reloaded1.durationMs, "Expected durationMs"); + + assertThrows(() -> SessionEndPayload.fromJson("{"), "SessionEndPayload.fromJson should reject malformed JSON"); + + assertThrows(() -> SessionEndPayload.fromYaml(":\n broken"), "SessionEndPayload.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/SessionEventGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/SessionEventGeneratedTest.java new file mode 100644 index 000000000..0a7806e73 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/SessionEventGeneratedTest.java @@ -0,0 +1,81 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class SessionEventGeneratedTest { + private SessionEventGeneratedTest() { } + + static void run() { + + // SessionEvent example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "id": "evt_abc123", + "timestamp": "2026-06-09T20:00:00Z", + "sessionId": "sess_abc123", + "turnId": "turn_001", + "parentId": "evt_parent", + "spanId": "span_hook_001" + } + """; + SessionEvent instance1 = SessionEvent.fromJson(jsonData1); + assertEquals("evt_abc123", instance1.id, "Expected id"); + assertEquals("2026-06-09T20:00:00Z", instance1.timestamp, "Expected timestamp"); + assertEquals("sess_abc123", instance1.sessionId, "Expected sessionId"); + assertEquals("turn_001", instance1.turnId, "Expected turnId"); + assertEquals("evt_parent", instance1.parentId, "Expected parentId"); + assertEquals("span_hook_001", instance1.spanId, "Expected spanId"); + String yamlRoundtrip1 = instance1.toYaml(); + SessionEvent fromYaml1 = SessionEvent.fromYaml(yamlRoundtrip1); + assertEquals("evt_abc123", fromYaml1.id, "Expected id"); + assertEquals("2026-06-09T20:00:00Z", fromYaml1.timestamp, "Expected timestamp"); + assertEquals("sess_abc123", fromYaml1.sessionId, "Expected sessionId"); + assertEquals("turn_001", fromYaml1.turnId, "Expected turnId"); + assertEquals("evt_parent", fromYaml1.parentId, "Expected parentId"); + assertEquals("span_hook_001", fromYaml1.spanId, "Expected spanId"); + SessionEvent reloaded1 = SessionEvent.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("evt_abc123", reloaded1.id, "Expected id"); + assertEquals("2026-06-09T20:00:00Z", reloaded1.timestamp, "Expected timestamp"); + assertEquals("sess_abc123", reloaded1.sessionId, "Expected sessionId"); + assertEquals("turn_001", reloaded1.turnId, "Expected turnId"); + assertEquals("evt_parent", reloaded1.parentId, "Expected parentId"); + assertEquals("span_hook_001", reloaded1.spanId, "Expected spanId"); + + assertThrows(() -> SessionEvent.fromJson("{"), "SessionEvent.fromJson should reject malformed JSON"); + + assertThrows(() -> SessionEvent.fromYaml(":\n broken"), "SessionEvent.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/SessionFileRefGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/SessionFileRefGeneratedTest.java new file mode 100644 index 000000000..e8507b0cb --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/SessionFileRefGeneratedTest.java @@ -0,0 +1,77 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class SessionFileRefGeneratedTest { + private SessionFileRefGeneratedTest() { } + + static void run() { + + // SessionFileRef example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "sessionId": "sess_abc123", + "path": "src/index.ts", + "toolName": "view", + "turnIndex": 2, + "firstSeenAt": "2026-06-09T20:00:00Z" + } + """; + SessionFileRef instance1 = SessionFileRef.fromJson(jsonData1); + assertEquals("sess_abc123", instance1.sessionId, "Expected sessionId"); + assertEquals("src/index.ts", instance1.path, "Expected path"); + assertEquals("view", instance1.toolName, "Expected toolName"); + assertEquals(2, instance1.turnIndex, "Expected turnIndex"); + assertEquals("2026-06-09T20:00:00Z", instance1.firstSeenAt, "Expected firstSeenAt"); + String yamlRoundtrip1 = instance1.toYaml(); + SessionFileRef fromYaml1 = SessionFileRef.fromYaml(yamlRoundtrip1); + assertEquals("sess_abc123", fromYaml1.sessionId, "Expected sessionId"); + assertEquals("src/index.ts", fromYaml1.path, "Expected path"); + assertEquals("view", fromYaml1.toolName, "Expected toolName"); + assertEquals(2, fromYaml1.turnIndex, "Expected turnIndex"); + assertEquals("2026-06-09T20:00:00Z", fromYaml1.firstSeenAt, "Expected firstSeenAt"); + SessionFileRef reloaded1 = SessionFileRef.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("sess_abc123", reloaded1.sessionId, "Expected sessionId"); + assertEquals("src/index.ts", reloaded1.path, "Expected path"); + assertEquals("view", reloaded1.toolName, "Expected toolName"); + assertEquals(2, reloaded1.turnIndex, "Expected turnIndex"); + assertEquals("2026-06-09T20:00:00Z", reloaded1.firstSeenAt, "Expected firstSeenAt"); + + assertThrows(() -> SessionFileRef.fromJson("{"), "SessionFileRef.fromJson should reject malformed JSON"); + + assertThrows(() -> SessionFileRef.fromYaml(":\n broken"), "SessionFileRef.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/SessionRefGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/SessionRefGeneratedTest.java new file mode 100644 index 000000000..cb28e17c5 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/SessionRefGeneratedTest.java @@ -0,0 +1,77 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class SessionRefGeneratedTest { + private SessionRefGeneratedTest() { } + + static void run() { + + // SessionRef example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "sessionId": "sess_abc123", + "refType": "issue", + "refValue": "owner/repo#123", + "turnIndex": 2, + "createdAt": "2026-06-09T20:00:00Z" + } + """; + SessionRef instance1 = SessionRef.fromJson(jsonData1); + assertEquals("sess_abc123", instance1.sessionId, "Expected sessionId"); + assertEquals("issue", instance1.refType, "Expected refType"); + assertEquals("owner/repo#123", instance1.refValue, "Expected refValue"); + assertEquals(2, instance1.turnIndex, "Expected turnIndex"); + assertEquals("2026-06-09T20:00:00Z", instance1.createdAt, "Expected createdAt"); + String yamlRoundtrip1 = instance1.toYaml(); + SessionRef fromYaml1 = SessionRef.fromYaml(yamlRoundtrip1); + assertEquals("sess_abc123", fromYaml1.sessionId, "Expected sessionId"); + assertEquals("issue", fromYaml1.refType, "Expected refType"); + assertEquals("owner/repo#123", fromYaml1.refValue, "Expected refValue"); + assertEquals(2, fromYaml1.turnIndex, "Expected turnIndex"); + assertEquals("2026-06-09T20:00:00Z", fromYaml1.createdAt, "Expected createdAt"); + SessionRef reloaded1 = SessionRef.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("sess_abc123", reloaded1.sessionId, "Expected sessionId"); + assertEquals("issue", reloaded1.refType, "Expected refType"); + assertEquals("owner/repo#123", reloaded1.refValue, "Expected refValue"); + assertEquals(2, reloaded1.turnIndex, "Expected turnIndex"); + assertEquals("2026-06-09T20:00:00Z", reloaded1.createdAt, "Expected createdAt"); + + assertThrows(() -> SessionRef.fromJson("{"), "SessionRef.fromJson should reject malformed JSON"); + + assertThrows(() -> SessionRef.fromYaml(":\n broken"), "SessionRef.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/SessionStartPayloadGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/SessionStartPayloadGeneratedTest.java new file mode 100644 index 000000000..6f28d8675 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/SessionStartPayloadGeneratedTest.java @@ -0,0 +1,89 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class SessionStartPayloadGeneratedTest { + private SessionStartPayloadGeneratedTest() { } + + static void run() { + + // SessionStartPayload example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "sessionId": "sess_abc123", + "schemaVersion": "1", + "producer": "prompty-agent", + "runtime": "typescript", + "promptyVersion": "2.0.0", + "startTime": "2026-06-09T20:00:00Z", + "selectedModel": "gpt-4o-mini", + "reasoningEffort": "medium" + } + """; + SessionStartPayload instance1 = SessionStartPayload.fromJson(jsonData1); + assertEquals("sess_abc123", instance1.sessionId, "Expected sessionId"); + assertEquals("1", instance1.schemaVersion, "Expected schemaVersion"); + assertEquals("prompty-agent", instance1.producer, "Expected producer"); + assertEquals("typescript", instance1.runtime, "Expected runtime"); + assertEquals("2.0.0", instance1.promptyVersion, "Expected promptyVersion"); + assertEquals("2026-06-09T20:00:00Z", instance1.startTime, "Expected startTime"); + assertEquals("gpt-4o-mini", instance1.selectedModel, "Expected selectedModel"); + assertEquals("medium", instance1.reasoningEffort, "Expected reasoningEffort"); + String yamlRoundtrip1 = instance1.toYaml(); + SessionStartPayload fromYaml1 = SessionStartPayload.fromYaml(yamlRoundtrip1); + assertEquals("sess_abc123", fromYaml1.sessionId, "Expected sessionId"); + assertEquals("1", fromYaml1.schemaVersion, "Expected schemaVersion"); + assertEquals("prompty-agent", fromYaml1.producer, "Expected producer"); + assertEquals("typescript", fromYaml1.runtime, "Expected runtime"); + assertEquals("2.0.0", fromYaml1.promptyVersion, "Expected promptyVersion"); + assertEquals("2026-06-09T20:00:00Z", fromYaml1.startTime, "Expected startTime"); + assertEquals("gpt-4o-mini", fromYaml1.selectedModel, "Expected selectedModel"); + assertEquals("medium", fromYaml1.reasoningEffort, "Expected reasoningEffort"); + SessionStartPayload reloaded1 = SessionStartPayload.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("sess_abc123", reloaded1.sessionId, "Expected sessionId"); + assertEquals("1", reloaded1.schemaVersion, "Expected schemaVersion"); + assertEquals("prompty-agent", reloaded1.producer, "Expected producer"); + assertEquals("typescript", reloaded1.runtime, "Expected runtime"); + assertEquals("2.0.0", reloaded1.promptyVersion, "Expected promptyVersion"); + assertEquals("2026-06-09T20:00:00Z", reloaded1.startTime, "Expected startTime"); + assertEquals("gpt-4o-mini", reloaded1.selectedModel, "Expected selectedModel"); + assertEquals("medium", reloaded1.reasoningEffort, "Expected reasoningEffort"); + + assertThrows(() -> SessionStartPayload.fromJson("{"), "SessionStartPayload.fromJson should reject malformed JSON"); + + assertThrows(() -> SessionStartPayload.fromYaml(":\n broken"), "SessionStartPayload.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/SessionSummaryGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/SessionSummaryGeneratedTest.java new file mode 100644 index 000000000..45c0cc261 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/SessionSummaryGeneratedTest.java @@ -0,0 +1,77 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class SessionSummaryGeneratedTest { + private SessionSummaryGeneratedTest() { } + + static void run() { + + // SessionSummary example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "sessionId": "sess_abc123", + "status": "success", + "turns": 5, + "checkpoints": 2, + "durationMs": 12500 + } + """; + SessionSummary instance1 = SessionSummary.fromJson(jsonData1); + assertEquals("sess_abc123", instance1.sessionId, "Expected sessionId"); + assertEquals(SessionSummaryStatus.fromValue("success"), instance1.status, "Expected status"); + assertEquals(5, instance1.turns, "Expected turns"); + assertEquals(2, instance1.checkpoints, "Expected checkpoints"); + assertEquals(12500, instance1.durationMs, "Expected durationMs"); + String yamlRoundtrip1 = instance1.toYaml(); + SessionSummary fromYaml1 = SessionSummary.fromYaml(yamlRoundtrip1); + assertEquals("sess_abc123", fromYaml1.sessionId, "Expected sessionId"); + assertEquals(SessionSummaryStatus.fromValue("success"), fromYaml1.status, "Expected status"); + assertEquals(5, fromYaml1.turns, "Expected turns"); + assertEquals(2, fromYaml1.checkpoints, "Expected checkpoints"); + assertEquals(12500, fromYaml1.durationMs, "Expected durationMs"); + SessionSummary reloaded1 = SessionSummary.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("sess_abc123", reloaded1.sessionId, "Expected sessionId"); + assertEquals(SessionSummaryStatus.fromValue("success"), reloaded1.status, "Expected status"); + assertEquals(5, reloaded1.turns, "Expected turns"); + assertEquals(2, reloaded1.checkpoints, "Expected checkpoints"); + assertEquals(12500, reloaded1.durationMs, "Expected durationMs"); + + assertThrows(() -> SessionSummary.fromJson("{"), "SessionSummary.fromJson should reject malformed JSON"); + + assertThrows(() -> SessionSummary.fromYaml(":\n broken"), "SessionSummary.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/SessionTraceGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/SessionTraceGeneratedTest.java new file mode 100644 index 000000000..309c396b6 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/SessionTraceGeneratedTest.java @@ -0,0 +1,73 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class SessionTraceGeneratedTest { + private SessionTraceGeneratedTest() { } + + static void run() { + + // SessionTrace example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "version": "1", + "runtime": "typescript", + "promptyVersion": "2.0.0", + "sessionId": "sess_abc123" + } + """; + SessionTrace instance1 = SessionTrace.fromJson(jsonData1); + assertEquals("1", instance1.version, "Expected version"); + assertEquals("typescript", instance1.runtime, "Expected runtime"); + assertEquals("2.0.0", instance1.promptyVersion, "Expected promptyVersion"); + assertEquals("sess_abc123", instance1.sessionId, "Expected sessionId"); + String yamlRoundtrip1 = instance1.toYaml(); + SessionTrace fromYaml1 = SessionTrace.fromYaml(yamlRoundtrip1); + assertEquals("1", fromYaml1.version, "Expected version"); + assertEquals("typescript", fromYaml1.runtime, "Expected runtime"); + assertEquals("2.0.0", fromYaml1.promptyVersion, "Expected promptyVersion"); + assertEquals("sess_abc123", fromYaml1.sessionId, "Expected sessionId"); + SessionTrace reloaded1 = SessionTrace.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("1", reloaded1.version, "Expected version"); + assertEquals("typescript", reloaded1.runtime, "Expected runtime"); + assertEquals("2.0.0", reloaded1.promptyVersion, "Expected promptyVersion"); + assertEquals("sess_abc123", reloaded1.sessionId, "Expected sessionId"); + + assertThrows(() -> SessionTrace.fromJson("{"), "SessionTrace.fromJson should reject malformed JSON"); + + assertThrows(() -> SessionTrace.fromYaml(":\n broken"), "SessionTrace.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/SessionWarningPayloadGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/SessionWarningPayloadGeneratedTest.java new file mode 100644 index 000000000..fb6841b1b --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/SessionWarningPayloadGeneratedTest.java @@ -0,0 +1,65 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class SessionWarningPayloadGeneratedTest { + private SessionWarningPayloadGeneratedTest() { } + + static void run() { + + // SessionWarningPayload example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "warningType": "remote", + "message": "Remote session disabled" + } + """; + SessionWarningPayload instance1 = SessionWarningPayload.fromJson(jsonData1); + assertEquals("remote", instance1.warningType, "Expected warningType"); + assertEquals("Remote session disabled", instance1.message, "Expected message"); + String yamlRoundtrip1 = instance1.toYaml(); + SessionWarningPayload fromYaml1 = SessionWarningPayload.fromYaml(yamlRoundtrip1); + assertEquals("remote", fromYaml1.warningType, "Expected warningType"); + assertEquals("Remote session disabled", fromYaml1.message, "Expected message"); + SessionWarningPayload reloaded1 = SessionWarningPayload.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("remote", reloaded1.warningType, "Expected warningType"); + assertEquals("Remote session disabled", reloaded1.message, "Expected message"); + + assertThrows(() -> SessionWarningPayload.fromJson("{"), "SessionWarningPayload.fromJson should reject malformed JSON"); + + assertThrows(() -> SessionWarningPayload.fromYaml(":\n broken"), "SessionWarningPayload.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/StatusEventPayloadGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/StatusEventPayloadGeneratedTest.java new file mode 100644 index 000000000..8e1227959 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/StatusEventPayloadGeneratedTest.java @@ -0,0 +1,61 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class StatusEventPayloadGeneratedTest { + private StatusEventPayloadGeneratedTest() { } + + static void run() { + + // StatusEventPayload example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "message": "Starting iteration 3" + } + """; + StatusEventPayload instance1 = StatusEventPayload.fromJson(jsonData1); + assertEquals("Starting iteration 3", instance1.message, "Expected message"); + String yamlRoundtrip1 = instance1.toYaml(); + StatusEventPayload fromYaml1 = StatusEventPayload.fromYaml(yamlRoundtrip1); + assertEquals("Starting iteration 3", fromYaml1.message, "Expected message"); + StatusEventPayload reloaded1 = StatusEventPayload.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("Starting iteration 3", reloaded1.message, "Expected message"); + + assertThrows(() -> StatusEventPayload.fromJson("{"), "StatusEventPayload.fromJson should reject malformed JSON"); + + assertThrows(() -> StatusEventPayload.fromYaml(":\n broken"), "StatusEventPayload.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/StreamChunkGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/StreamChunkGeneratedTest.java new file mode 100644 index 000000000..9b942b4e5 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/StreamChunkGeneratedTest.java @@ -0,0 +1,43 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class StreamChunkGeneratedTest { + private StreamChunkGeneratedTest() { } + + static void run() { + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/StreamOptionsGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/StreamOptionsGeneratedTest.java new file mode 100644 index 000000000..87ef391cc --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/StreamOptionsGeneratedTest.java @@ -0,0 +1,61 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class StreamOptionsGeneratedTest { + private StreamOptionsGeneratedTest() { } + + static void run() { + + // StreamOptions example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "includeUsage": true + } + """; + StreamOptions instance1 = StreamOptions.fromJson(jsonData1); + assertEquals(true, instance1.includeUsage, "Expected includeUsage"); + String yamlRoundtrip1 = instance1.toYaml(); + StreamOptions fromYaml1 = StreamOptions.fromYaml(yamlRoundtrip1); + assertEquals(true, fromYaml1.includeUsage, "Expected includeUsage"); + StreamOptions reloaded1 = StreamOptions.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals(true, reloaded1.includeUsage, "Expected includeUsage"); + + assertThrows(() -> StreamOptions.fromJson("{"), "StreamOptions.fromJson should reject malformed JSON"); + + assertThrows(() -> StreamOptions.fromYaml(":\n broken"), "StreamOptions.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/SubscriptionInfoGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/SubscriptionInfoGeneratedTest.java new file mode 100644 index 000000000..fca150e3e --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/SubscriptionInfoGeneratedTest.java @@ -0,0 +1,43 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class SubscriptionInfoGeneratedTest { + private SubscriptionInfoGeneratedTest() { } + + static void run() { + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TemplateGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TemplateGeneratedTest.java new file mode 100644 index 000000000..7feb34a63 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TemplateGeneratedTest.java @@ -0,0 +1,69 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class TemplateGeneratedTest { + private TemplateGeneratedTest() { } + + static void run() { + + // Template example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "format": { + "kind": "mustache" + }, + "parser": { + "kind": "mustache" + } + } + """; + Template instance1 = Template.fromJson(jsonData1); + assertEquals("mustache", instance1.format.kind, "Expected instance1.format.kind"); + assertEquals("mustache", instance1.parser.kind, "Expected instance1.parser.kind"); + String yamlRoundtrip1 = instance1.toYaml(); + Template fromYaml1 = Template.fromYaml(yamlRoundtrip1); + assertEquals("mustache", fromYaml1.format.kind, "Expected fromYaml1.format.kind"); + assertEquals("mustache", fromYaml1.parser.kind, "Expected fromYaml1.parser.kind"); + Template reloaded1 = Template.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("mustache", reloaded1.format.kind, "Expected reloaded1.format.kind"); + assertEquals("mustache", reloaded1.parser.kind, "Expected reloaded1.parser.kind"); + + assertThrows(() -> Template.fromJson("{"), "Template.fromJson should reject malformed JSON"); + + assertThrows(() -> Template.fromYaml(":\n broken"), "Template.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TextChunkGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TextChunkGeneratedTest.java new file mode 100644 index 000000000..6143330a9 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TextChunkGeneratedTest.java @@ -0,0 +1,61 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class TextChunkGeneratedTest { + private TextChunkGeneratedTest() { } + + static void run() { + + // TextChunk example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "value": "Hello" + } + """; + TextChunk instance1 = TextChunk.fromJson(jsonData1); + assertEquals("Hello", instance1.value, "Expected value"); + String yamlRoundtrip1 = instance1.toYaml(); + TextChunk fromYaml1 = TextChunk.fromYaml(yamlRoundtrip1); + assertEquals("Hello", fromYaml1.value, "Expected value"); + TextChunk reloaded1 = TextChunk.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("Hello", reloaded1.value, "Expected value"); + + assertThrows(() -> TextChunk.fromJson("{"), "TextChunk.fromJson should reject malformed JSON"); + + assertThrows(() -> TextChunk.fromYaml(":\n broken"), "TextChunk.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TextPartGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TextPartGeneratedTest.java new file mode 100644 index 000000000..a43985148 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TextPartGeneratedTest.java @@ -0,0 +1,61 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class TextPartGeneratedTest { + private TextPartGeneratedTest() { } + + static void run() { + + // TextPart example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "value": "Hello, world!" + } + """; + TextPart instance1 = TextPart.fromJson(jsonData1); + assertEquals("Hello, world!", instance1.value, "Expected value"); + String yamlRoundtrip1 = instance1.toYaml(); + TextPart fromYaml1 = TextPart.fromYaml(yamlRoundtrip1); + assertEquals("Hello, world!", fromYaml1.value, "Expected value"); + TextPart reloaded1 = TextPart.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("Hello, world!", reloaded1.value, "Expected value"); + + assertThrows(() -> TextPart.fromJson("{"), "TextPart.fromJson should reject malformed JSON"); + + assertThrows(() -> TextPart.fromYaml(":\n broken"), "TextPart.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ThinkingChunkGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ThinkingChunkGeneratedTest.java new file mode 100644 index 000000000..38e2c1cd0 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ThinkingChunkGeneratedTest.java @@ -0,0 +1,61 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class ThinkingChunkGeneratedTest { + private ThinkingChunkGeneratedTest() { } + + static void run() { + + // ThinkingChunk example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "value": "Let me consider..." + } + """; + ThinkingChunk instance1 = ThinkingChunk.fromJson(jsonData1); + assertEquals("Let me consider...", instance1.value, "Expected value"); + String yamlRoundtrip1 = instance1.toYaml(); + ThinkingChunk fromYaml1 = ThinkingChunk.fromYaml(yamlRoundtrip1); + assertEquals("Let me consider...", fromYaml1.value, "Expected value"); + ThinkingChunk reloaded1 = ThinkingChunk.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("Let me consider...", reloaded1.value, "Expected value"); + + assertThrows(() -> ThinkingChunk.fromJson("{"), "ThinkingChunk.fromJson should reject malformed JSON"); + + assertThrows(() -> ThinkingChunk.fromYaml(":\n broken"), "ThinkingChunk.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ThinkingEventPayloadGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ThinkingEventPayloadGeneratedTest.java new file mode 100644 index 000000000..fb94d4f6e --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ThinkingEventPayloadGeneratedTest.java @@ -0,0 +1,61 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class ThinkingEventPayloadGeneratedTest { + private ThinkingEventPayloadGeneratedTest() { } + + static void run() { + + // ThinkingEventPayload example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "token": "Let me consider..." + } + """; + ThinkingEventPayload instance1 = ThinkingEventPayload.fromJson(jsonData1); + assertEquals("Let me consider...", instance1.token, "Expected token"); + String yamlRoundtrip1 = instance1.toYaml(); + ThinkingEventPayload fromYaml1 = ThinkingEventPayload.fromYaml(yamlRoundtrip1); + assertEquals("Let me consider...", fromYaml1.token, "Expected token"); + ThinkingEventPayload reloaded1 = ThinkingEventPayload.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("Let me consider...", reloaded1.token, "Expected token"); + + assertThrows(() -> ThinkingEventPayload.fromJson("{"), "ThinkingEventPayload.fromJson should reject malformed JSON"); + + assertThrows(() -> ThinkingEventPayload.fromYaml(":\n broken"), "ThinkingEventPayload.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ThreadMarkerGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ThreadMarkerGeneratedTest.java new file mode 100644 index 000000000..18651144d --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ThreadMarkerGeneratedTest.java @@ -0,0 +1,65 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class ThreadMarkerGeneratedTest { + private ThreadMarkerGeneratedTest() { } + + static void run() { + + // ThreadMarker example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "name": "thread", + "kind": "thread" + } + """; + ThreadMarker instance1 = ThreadMarker.fromJson(jsonData1); + assertEquals("thread", instance1.name, "Expected name"); + assertEquals("thread", instance1.kind, "Expected kind"); + String yamlRoundtrip1 = instance1.toYaml(); + ThreadMarker fromYaml1 = ThreadMarker.fromYaml(yamlRoundtrip1); + assertEquals("thread", fromYaml1.name, "Expected name"); + assertEquals("thread", fromYaml1.kind, "Expected kind"); + ThreadMarker reloaded1 = ThreadMarker.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("thread", reloaded1.name, "Expected name"); + assertEquals("thread", reloaded1.kind, "Expected kind"); + + assertThrows(() -> ThreadMarker.fromJson("{"), "ThreadMarker.fromJson should reject malformed JSON"); + + assertThrows(() -> ThreadMarker.fromYaml(":\n broken"), "ThreadMarker.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TokenEventPayloadGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TokenEventPayloadGeneratedTest.java new file mode 100644 index 000000000..6c486a041 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TokenEventPayloadGeneratedTest.java @@ -0,0 +1,61 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class TokenEventPayloadGeneratedTest { + private TokenEventPayloadGeneratedTest() { } + + static void run() { + + // TokenEventPayload example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "token": "Hello" + } + """; + TokenEventPayload instance1 = TokenEventPayload.fromJson(jsonData1); + assertEquals("Hello", instance1.token, "Expected token"); + String yamlRoundtrip1 = instance1.toYaml(); + TokenEventPayload fromYaml1 = TokenEventPayload.fromYaml(yamlRoundtrip1); + assertEquals("Hello", fromYaml1.token, "Expected token"); + TokenEventPayload reloaded1 = TokenEventPayload.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("Hello", reloaded1.token, "Expected token"); + + assertThrows(() -> TokenEventPayload.fromJson("{"), "TokenEventPayload.fromJson should reject malformed JSON"); + + assertThrows(() -> TokenEventPayload.fromYaml(":\n broken"), "TokenEventPayload.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TokenUsageGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TokenUsageGeneratedTest.java new file mode 100644 index 000000000..0781da423 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TokenUsageGeneratedTest.java @@ -0,0 +1,83 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class TokenUsageGeneratedTest { + private TokenUsageGeneratedTest() { } + + static void run() { + + // TokenUsage example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "promptTokens": 150, + "completionTokens": 42, + "totalTokens": 192 + } + """; + TokenUsage instance1 = TokenUsage.fromJson(jsonData1); + assertEquals(150, instance1.promptTokens, "Expected promptTokens"); + assertEquals(42, instance1.completionTokens, "Expected completionTokens"); + assertEquals(192, instance1.totalTokens, "Expected totalTokens"); + String yamlRoundtrip1 = instance1.toYaml(); + TokenUsage fromYaml1 = TokenUsage.fromYaml(yamlRoundtrip1); + assertEquals(150, fromYaml1.promptTokens, "Expected promptTokens"); + assertEquals(42, fromYaml1.completionTokens, "Expected completionTokens"); + assertEquals(192, fromYaml1.totalTokens, "Expected totalTokens"); + TokenUsage reloaded1 = TokenUsage.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals(150, reloaded1.promptTokens, "Expected promptTokens"); + assertEquals(42, reloaded1.completionTokens, "Expected completionTokens"); + assertEquals(192, reloaded1.totalTokens, "Expected totalTokens"); + + assertThrows(() -> TokenUsage.fromJson("{"), "TokenUsage.fromJson should reject malformed JSON"); + + assertThrows(() -> TokenUsage.fromYaml(":\n broken"), "TokenUsage.fromYaml should reject malformed YAML"); + + TokenUsage wireInstance = TokenUsage.fromJson("{\n \"promptTokens\": 150,\n \"completionTokens\": 42,\n \"totalTokens\": 192\n}"); + java.util.Map openaiWire = wireInstance.toWire("openai"); + assertTrue(openaiWire.containsKey("prompt_tokens"), "Expected openai wire output to include prompt_tokens"); + assertTrue(!openaiWire.containsKey("promptTokens"), "Expected openai wire output to omit promptTokens"); + assertTrue(openaiWire.containsKey("completion_tokens"), "Expected openai wire output to include completion_tokens"); + assertTrue(!openaiWire.containsKey("completionTokens"), "Expected openai wire output to omit completionTokens"); + assertTrue(openaiWire.containsKey("total_tokens"), "Expected openai wire output to include total_tokens"); + assertTrue(!openaiWire.containsKey("totalTokens"), "Expected openai wire output to omit totalTokens"); + java.util.Map anthropicWire = wireInstance.toWire("anthropic"); + assertTrue(anthropicWire.containsKey("input_tokens"), "Expected anthropic wire output to include input_tokens"); + assertTrue(!anthropicWire.containsKey("promptTokens"), "Expected anthropic wire output to omit promptTokens"); + assertTrue(anthropicWire.containsKey("output_tokens"), "Expected anthropic wire output to include output_tokens"); + assertTrue(!anthropicWire.containsKey("completionTokens"), "Expected anthropic wire output to omit completionTokens"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolCallCompletePayloadGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolCallCompletePayloadGeneratedTest.java new file mode 100644 index 000000000..c19e65442 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolCallCompletePayloadGeneratedTest.java @@ -0,0 +1,77 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class ToolCallCompletePayloadGeneratedTest { + private ToolCallCompletePayloadGeneratedTest() { } + + static void run() { + + // ToolCallCompletePayload example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "id": "call_abc123", + "name": "get_weather", + "success": true, + "durationMs": 42, + "errorKind": "timeout" + } + """; + ToolCallCompletePayload instance1 = ToolCallCompletePayload.fromJson(jsonData1); + assertEquals("call_abc123", instance1.id, "Expected id"); + assertEquals("get_weather", instance1.name, "Expected name"); + assertEquals(true, instance1.success, "Expected success"); + assertEquals(42, instance1.durationMs, "Expected durationMs"); + assertEquals("timeout", instance1.errorKind, "Expected errorKind"); + String yamlRoundtrip1 = instance1.toYaml(); + ToolCallCompletePayload fromYaml1 = ToolCallCompletePayload.fromYaml(yamlRoundtrip1); + assertEquals("call_abc123", fromYaml1.id, "Expected id"); + assertEquals("get_weather", fromYaml1.name, "Expected name"); + assertEquals(true, fromYaml1.success, "Expected success"); + assertEquals(42, fromYaml1.durationMs, "Expected durationMs"); + assertEquals("timeout", fromYaml1.errorKind, "Expected errorKind"); + ToolCallCompletePayload reloaded1 = ToolCallCompletePayload.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("call_abc123", reloaded1.id, "Expected id"); + assertEquals("get_weather", reloaded1.name, "Expected name"); + assertEquals(true, reloaded1.success, "Expected success"); + assertEquals(42, reloaded1.durationMs, "Expected durationMs"); + assertEquals("timeout", reloaded1.errorKind, "Expected errorKind"); + + assertThrows(() -> ToolCallCompletePayload.fromJson("{"), "ToolCallCompletePayload.fromJson should reject malformed JSON"); + + assertThrows(() -> ToolCallCompletePayload.fromYaml(":\n broken"), "ToolCallCompletePayload.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolCallGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolCallGeneratedTest.java new file mode 100644 index 000000000..efc79b93c --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolCallGeneratedTest.java @@ -0,0 +1,69 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class ToolCallGeneratedTest { + private ToolCallGeneratedTest() { } + + static void run() { + + // ToolCall example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "id": "call_abc123", + "name": "get_weather", + "arguments": "{\\"city\\": \\"Paris\\"}" + } + """; + ToolCall instance1 = ToolCall.fromJson(jsonData1); + assertEquals("call_abc123", instance1.id, "Expected id"); + assertEquals("get_weather", instance1.name, "Expected name"); + assertEquals("{\"city\": \"Paris\"}", instance1.arguments, "Expected arguments"); + String yamlRoundtrip1 = instance1.toYaml(); + ToolCall fromYaml1 = ToolCall.fromYaml(yamlRoundtrip1); + assertEquals("call_abc123", fromYaml1.id, "Expected id"); + assertEquals("get_weather", fromYaml1.name, "Expected name"); + assertEquals("{\"city\": \"Paris\"}", fromYaml1.arguments, "Expected arguments"); + ToolCall reloaded1 = ToolCall.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("call_abc123", reloaded1.id, "Expected id"); + assertEquals("get_weather", reloaded1.name, "Expected name"); + assertEquals("{\"city\": \"Paris\"}", reloaded1.arguments, "Expected arguments"); + + assertThrows(() -> ToolCall.fromJson("{"), "ToolCall.fromJson should reject malformed JSON"); + + assertThrows(() -> ToolCall.fromYaml(":\n broken"), "ToolCall.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolCallStartPayloadGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolCallStartPayloadGeneratedTest.java new file mode 100644 index 000000000..0dbd7f09d --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolCallStartPayloadGeneratedTest.java @@ -0,0 +1,69 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class ToolCallStartPayloadGeneratedTest { + private ToolCallStartPayloadGeneratedTest() { } + + static void run() { + + // ToolCallStartPayload example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "id": "call_abc123", + "name": "get_weather", + "arguments": "{\\"city\\": \\"Paris\\"}" + } + """; + ToolCallStartPayload instance1 = ToolCallStartPayload.fromJson(jsonData1); + assertEquals("call_abc123", instance1.id, "Expected id"); + assertEquals("get_weather", instance1.name, "Expected name"); + assertEquals("{\"city\": \"Paris\"}", instance1.arguments, "Expected arguments"); + String yamlRoundtrip1 = instance1.toYaml(); + ToolCallStartPayload fromYaml1 = ToolCallStartPayload.fromYaml(yamlRoundtrip1); + assertEquals("call_abc123", fromYaml1.id, "Expected id"); + assertEquals("get_weather", fromYaml1.name, "Expected name"); + assertEquals("{\"city\": \"Paris\"}", fromYaml1.arguments, "Expected arguments"); + ToolCallStartPayload reloaded1 = ToolCallStartPayload.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("call_abc123", reloaded1.id, "Expected id"); + assertEquals("get_weather", reloaded1.name, "Expected name"); + assertEquals("{\"city\": \"Paris\"}", reloaded1.arguments, "Expected arguments"); + + assertThrows(() -> ToolCallStartPayload.fromJson("{"), "ToolCallStartPayload.fromJson should reject malformed JSON"); + + assertThrows(() -> ToolCallStartPayload.fromYaml(":\n broken"), "ToolCallStartPayload.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolChunkGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolChunkGeneratedTest.java new file mode 100644 index 000000000..9da9af9f9 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolChunkGeneratedTest.java @@ -0,0 +1,71 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class ToolChunkGeneratedTest { + private ToolChunkGeneratedTest() { } + + static void run() { + + // ToolChunk example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "toolCall": { + "id": "call_abc123", + "name": "get_weather", + "arguments": "{\\"city\\": \\"Paris\\"}" + } + } + """; + ToolChunk instance1 = ToolChunk.fromJson(jsonData1); + assertEquals("call_abc123", instance1.toolCall.id, "Expected instance1.toolCall.id"); + assertEquals("get_weather", instance1.toolCall.name, "Expected instance1.toolCall.name"); + assertEquals("{\"city\": \"Paris\"}", instance1.toolCall.arguments, "Expected instance1.toolCall.arguments"); + String yamlRoundtrip1 = instance1.toYaml(); + ToolChunk fromYaml1 = ToolChunk.fromYaml(yamlRoundtrip1); + assertEquals("call_abc123", fromYaml1.toolCall.id, "Expected fromYaml1.toolCall.id"); + assertEquals("get_weather", fromYaml1.toolCall.name, "Expected fromYaml1.toolCall.name"); + assertEquals("{\"city\": \"Paris\"}", fromYaml1.toolCall.arguments, "Expected fromYaml1.toolCall.arguments"); + ToolChunk reloaded1 = ToolChunk.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("call_abc123", reloaded1.toolCall.id, "Expected reloaded1.toolCall.id"); + assertEquals("get_weather", reloaded1.toolCall.name, "Expected reloaded1.toolCall.name"); + assertEquals("{\"city\": \"Paris\"}", reloaded1.toolCall.arguments, "Expected reloaded1.toolCall.arguments"); + + assertThrows(() -> ToolChunk.fromJson("{"), "ToolChunk.fromJson should reject malformed JSON"); + + assertThrows(() -> ToolChunk.fromYaml(":\n broken"), "ToolChunk.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolContextGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolContextGeneratedTest.java new file mode 100644 index 000000000..4c36e16e7 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolContextGeneratedTest.java @@ -0,0 +1,63 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class ToolContextGeneratedTest { + private ToolContextGeneratedTest() { } + + static void run() { + + // ToolContext example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "metadata": { + "userId": "user-123" + } + } + """; + ToolContext instance1 = ToolContext.fromJson(jsonData1); + assertEquals("user-123", instance1.metadata.get("userId"), "Expected metadata.userId"); + String yamlRoundtrip1 = instance1.toYaml(); + ToolContext fromYaml1 = ToolContext.fromYaml(yamlRoundtrip1); + assertEquals("user-123", fromYaml1.metadata.get("userId"), "Expected metadata.userId"); + ToolContext reloaded1 = ToolContext.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("user-123", reloaded1.metadata.get("userId"), "Expected metadata.userId"); + + assertThrows(() -> ToolContext.fromJson("{"), "ToolContext.fromJson should reject malformed JSON"); + + assertThrows(() -> ToolContext.fromYaml(":\n broken"), "ToolContext.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolDispatchResultGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolDispatchResultGeneratedTest.java new file mode 100644 index 000000000..9d8485fd3 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolDispatchResultGeneratedTest.java @@ -0,0 +1,73 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class ToolDispatchResultGeneratedTest { + private ToolDispatchResultGeneratedTest() { } + + static void run() { + + // ToolDispatchResult example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "toolCallId": "call_abc123", + "name": "get_weather", + "result": { + "parts": [ + { + "kind": "text", + "value": "72°F and sunny" + } + ] + } + } + """; + ToolDispatchResult instance1 = ToolDispatchResult.fromJson(jsonData1); + assertEquals("call_abc123", instance1.toolCallId, "Expected toolCallId"); + assertEquals("get_weather", instance1.name, "Expected name"); + String yamlRoundtrip1 = instance1.toYaml(); + ToolDispatchResult fromYaml1 = ToolDispatchResult.fromYaml(yamlRoundtrip1); + assertEquals("call_abc123", fromYaml1.toolCallId, "Expected toolCallId"); + assertEquals("get_weather", fromYaml1.name, "Expected name"); + ToolDispatchResult reloaded1 = ToolDispatchResult.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("call_abc123", reloaded1.toolCallId, "Expected toolCallId"); + assertEquals("get_weather", reloaded1.name, "Expected name"); + + assertThrows(() -> ToolDispatchResult.fromJson("{"), "ToolDispatchResult.fromJson should reject malformed JSON"); + + assertThrows(() -> ToolDispatchResult.fromYaml(":\n broken"), "ToolDispatchResult.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolExecutionCompletePayloadGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolExecutionCompletePayloadGeneratedTest.java new file mode 100644 index 000000000..34256ec0c --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolExecutionCompletePayloadGeneratedTest.java @@ -0,0 +1,85 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class ToolExecutionCompletePayloadGeneratedTest { + private ToolExecutionCompletePayloadGeneratedTest() { } + + static void run() { + + // ToolExecutionCompletePayload example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "requestId": "exec_abc123", + "toolCallId": "call_abc123", + "toolName": "powershell", + "success": true, + "exitCode": 0, + "durationMs": 250, + "errorKind": "timeout" + } + """; + ToolExecutionCompletePayload instance1 = ToolExecutionCompletePayload.fromJson(jsonData1); + assertEquals("exec_abc123", instance1.requestId, "Expected requestId"); + assertEquals("call_abc123", instance1.toolCallId, "Expected toolCallId"); + assertEquals("powershell", instance1.toolName, "Expected toolName"); + assertEquals(true, instance1.success, "Expected success"); + assertEquals(0, instance1.exitCode, "Expected exitCode"); + assertEquals(250, instance1.durationMs, "Expected durationMs"); + assertEquals("timeout", instance1.errorKind, "Expected errorKind"); + String yamlRoundtrip1 = instance1.toYaml(); + ToolExecutionCompletePayload fromYaml1 = ToolExecutionCompletePayload.fromYaml(yamlRoundtrip1); + assertEquals("exec_abc123", fromYaml1.requestId, "Expected requestId"); + assertEquals("call_abc123", fromYaml1.toolCallId, "Expected toolCallId"); + assertEquals("powershell", fromYaml1.toolName, "Expected toolName"); + assertEquals(true, fromYaml1.success, "Expected success"); + assertEquals(0, fromYaml1.exitCode, "Expected exitCode"); + assertEquals(250, fromYaml1.durationMs, "Expected durationMs"); + assertEquals("timeout", fromYaml1.errorKind, "Expected errorKind"); + ToolExecutionCompletePayload reloaded1 = ToolExecutionCompletePayload.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("exec_abc123", reloaded1.requestId, "Expected requestId"); + assertEquals("call_abc123", reloaded1.toolCallId, "Expected toolCallId"); + assertEquals("powershell", reloaded1.toolName, "Expected toolName"); + assertEquals(true, reloaded1.success, "Expected success"); + assertEquals(0, reloaded1.exitCode, "Expected exitCode"); + assertEquals(250, reloaded1.durationMs, "Expected durationMs"); + assertEquals("timeout", reloaded1.errorKind, "Expected errorKind"); + + assertThrows(() -> ToolExecutionCompletePayload.fromJson("{"), "ToolExecutionCompletePayload.fromJson should reject malformed JSON"); + + assertThrows(() -> ToolExecutionCompletePayload.fromYaml(":\n broken"), "ToolExecutionCompletePayload.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolExecutionStartPayloadGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolExecutionStartPayloadGeneratedTest.java new file mode 100644 index 000000000..ebcb3dd58 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolExecutionStartPayloadGeneratedTest.java @@ -0,0 +1,73 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class ToolExecutionStartPayloadGeneratedTest { + private ToolExecutionStartPayloadGeneratedTest() { } + + static void run() { + + // ToolExecutionStartPayload example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "requestId": "exec_abc123", + "toolCallId": "call_abc123", + "toolName": "powershell", + "workingDirectory": "/workspace/project" + } + """; + ToolExecutionStartPayload instance1 = ToolExecutionStartPayload.fromJson(jsonData1); + assertEquals("exec_abc123", instance1.requestId, "Expected requestId"); + assertEquals("call_abc123", instance1.toolCallId, "Expected toolCallId"); + assertEquals("powershell", instance1.toolName, "Expected toolName"); + assertEquals("/workspace/project", instance1.workingDirectory, "Expected workingDirectory"); + String yamlRoundtrip1 = instance1.toYaml(); + ToolExecutionStartPayload fromYaml1 = ToolExecutionStartPayload.fromYaml(yamlRoundtrip1); + assertEquals("exec_abc123", fromYaml1.requestId, "Expected requestId"); + assertEquals("call_abc123", fromYaml1.toolCallId, "Expected toolCallId"); + assertEquals("powershell", fromYaml1.toolName, "Expected toolName"); + assertEquals("/workspace/project", fromYaml1.workingDirectory, "Expected workingDirectory"); + ToolExecutionStartPayload reloaded1 = ToolExecutionStartPayload.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("exec_abc123", reloaded1.requestId, "Expected requestId"); + assertEquals("call_abc123", reloaded1.toolCallId, "Expected toolCallId"); + assertEquals("powershell", reloaded1.toolName, "Expected toolName"); + assertEquals("/workspace/project", reloaded1.workingDirectory, "Expected workingDirectory"); + + assertThrows(() -> ToolExecutionStartPayload.fromJson("{"), "ToolExecutionStartPayload.fromJson should reject malformed JSON"); + + assertThrows(() -> ToolExecutionStartPayload.fromYaml(":\n broken"), "ToolExecutionStartPayload.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolGeneratedTest.java new file mode 100644 index 000000000..58a2f6800 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolGeneratedTest.java @@ -0,0 +1,72 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class ToolGeneratedTest { + private ToolGeneratedTest() { } + + static void run() { + + // Tool example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "name": "my-tool", + "kind": "function", + "description": "A description of the tool", + "bindings": { + "input": "value" + } + } + """; + Tool instance1 = Tool.fromJson(jsonData1); + assertEquals("my-tool", instance1.name, "Expected name"); + assertEquals("function", instance1.kind, "Expected kind"); + assertEquals("A description of the tool", instance1.description, "Expected description"); + String yamlRoundtrip1 = instance1.toYaml(); + Tool fromYaml1 = Tool.fromYaml(yamlRoundtrip1); + assertEquals("my-tool", fromYaml1.name, "Expected name"); + assertEquals("function", fromYaml1.kind, "Expected kind"); + assertEquals("A description of the tool", fromYaml1.description, "Expected description"); + Tool reloaded1 = Tool.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("my-tool", reloaded1.name, "Expected name"); + assertEquals("function", reloaded1.kind, "Expected kind"); + assertEquals("A description of the tool", reloaded1.description, "Expected description"); + + assertThrows(() -> Tool.fromJson("{"), "Tool.fromJson should reject malformed JSON"); + + assertThrows(() -> Tool.fromYaml(":\n broken"), "Tool.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolResultGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolResultGeneratedTest.java new file mode 100644 index 000000000..082bb5848 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolResultGeneratedTest.java @@ -0,0 +1,90 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class ToolResultGeneratedTest { + private ToolResultGeneratedTest() { } + + static void run() { + + // ToolResult example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "parts": [ + { + "kind": "text", + "value": "72°F and sunny" + } + ], + "errorKind": "missing_tool", + "errorMessage": "Tool 'get_weather' is not registered", + "durationMs": 42 + } + """; + ToolResult instance1 = ToolResult.fromJson(jsonData1); + assertEquals("missing_tool", instance1.errorKind, "Expected errorKind"); + assertEquals("Tool 'get_weather' is not registered", instance1.errorMessage, "Expected errorMessage"); + assertEquals(42, instance1.durationMs, "Expected durationMs"); + assertEquals(1, instance1.parts.size(), "Expected parts size"); + assertTrue(instance1.parts.get(0) instanceof TextPart, "Expected parts[0] to be TextPart"); + TextPart instance1Parts0Value = (TextPart) instance1.parts.get(0); + assertEquals("text", instance1Parts0Value.kind, "Expected kind"); + assertEquals("72°F and sunny", instance1Parts0Value.value, "Expected value"); + String yamlRoundtrip1 = instance1.toYaml(); + ToolResult fromYaml1 = ToolResult.fromYaml(yamlRoundtrip1); + assertEquals("missing_tool", fromYaml1.errorKind, "Expected errorKind"); + assertEquals("Tool 'get_weather' is not registered", fromYaml1.errorMessage, "Expected errorMessage"); + assertEquals(42, fromYaml1.durationMs, "Expected durationMs"); + assertEquals(1, fromYaml1.parts.size(), "Expected parts size"); + assertTrue(fromYaml1.parts.get(0) instanceof TextPart, "Expected parts[0] to be TextPart"); + TextPart fromYaml1Parts0Value = (TextPart) fromYaml1.parts.get(0); + assertEquals("text", fromYaml1Parts0Value.kind, "Expected kind"); + assertEquals("72°F and sunny", fromYaml1Parts0Value.value, "Expected value"); + ToolResult reloaded1 = ToolResult.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("missing_tool", reloaded1.errorKind, "Expected errorKind"); + assertEquals("Tool 'get_weather' is not registered", reloaded1.errorMessage, "Expected errorMessage"); + assertEquals(42, reloaded1.durationMs, "Expected durationMs"); + assertEquals(1, reloaded1.parts.size(), "Expected parts size"); + assertTrue(reloaded1.parts.get(0) instanceof TextPart, "Expected parts[0] to be TextPart"); + TextPart reloaded1Parts0Value = (TextPart) reloaded1.parts.get(0); + assertEquals("text", reloaded1Parts0Value.kind, "Expected kind"); + assertEquals("72°F and sunny", reloaded1Parts0Value.value, "Expected value"); + + assertThrows(() -> ToolResult.fromJson("{"), "ToolResult.fromJson should reject malformed JSON"); + + assertThrows(() -> ToolResult.fromYaml(":\n broken"), "ToolResult.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolResultPayloadGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolResultPayloadGeneratedTest.java new file mode 100644 index 000000000..eb7950460 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolResultPayloadGeneratedTest.java @@ -0,0 +1,69 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class ToolResultPayloadGeneratedTest { + private ToolResultPayloadGeneratedTest() { } + + static void run() { + + // ToolResultPayload example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "name": "get_weather", + "result": { + "parts": [ + { + "kind": "text", + "value": "72°F and sunny" + } + ] + } + } + """; + ToolResultPayload instance1 = ToolResultPayload.fromJson(jsonData1); + assertEquals("get_weather", instance1.name, "Expected name"); + String yamlRoundtrip1 = instance1.toYaml(); + ToolResultPayload fromYaml1 = ToolResultPayload.fromYaml(yamlRoundtrip1); + assertEquals("get_weather", fromYaml1.name, "Expected name"); + ToolResultPayload reloaded1 = ToolResultPayload.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("get_weather", reloaded1.name, "Expected name"); + + assertThrows(() -> ToolResultPayload.fromJson("{"), "ToolResultPayload.fromJson should reject malformed JSON"); + + assertThrows(() -> ToolResultPayload.fromYaml(":\n broken"), "ToolResultPayload.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TraceFileGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TraceFileGeneratedTest.java new file mode 100644 index 000000000..e1d5dd90e --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TraceFileGeneratedTest.java @@ -0,0 +1,65 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class TraceFileGeneratedTest { + private TraceFileGeneratedTest() { } + + static void run() { + + // TraceFile example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "runtime": "python", + "version": "2.0.0" + } + """; + TraceFile instance1 = TraceFile.fromJson(jsonData1); + assertEquals("python", instance1.runtime, "Expected runtime"); + assertEquals("2.0.0", instance1.version, "Expected version"); + String yamlRoundtrip1 = instance1.toYaml(); + TraceFile fromYaml1 = TraceFile.fromYaml(yamlRoundtrip1); + assertEquals("python", fromYaml1.runtime, "Expected runtime"); + assertEquals("2.0.0", fromYaml1.version, "Expected version"); + TraceFile reloaded1 = TraceFile.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("python", reloaded1.runtime, "Expected runtime"); + assertEquals("2.0.0", reloaded1.version, "Expected version"); + + assertThrows(() -> TraceFile.fromJson("{"), "TraceFile.fromJson should reject malformed JSON"); + + assertThrows(() -> TraceFile.fromYaml(":\n broken"), "TraceFile.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TraceSpanGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TraceSpanGeneratedTest.java new file mode 100644 index 000000000..a157aef6b --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TraceSpanGeneratedTest.java @@ -0,0 +1,69 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class TraceSpanGeneratedTest { + private TraceSpanGeneratedTest() { } + + static void run() { + + // TraceSpan example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "name": "prompty.core.pipeline.run", + "signature": "prompty.core.pipeline.run", + "error": "Connection refused" + } + """; + TraceSpan instance1 = TraceSpan.fromJson(jsonData1); + assertEquals("prompty.core.pipeline.run", instance1.name, "Expected name"); + assertEquals("prompty.core.pipeline.run", instance1.signature, "Expected signature"); + assertEquals("Connection refused", instance1.error, "Expected error"); + String yamlRoundtrip1 = instance1.toYaml(); + TraceSpan fromYaml1 = TraceSpan.fromYaml(yamlRoundtrip1); + assertEquals("prompty.core.pipeline.run", fromYaml1.name, "Expected name"); + assertEquals("prompty.core.pipeline.run", fromYaml1.signature, "Expected signature"); + assertEquals("Connection refused", fromYaml1.error, "Expected error"); + TraceSpan reloaded1 = TraceSpan.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("prompty.core.pipeline.run", reloaded1.name, "Expected name"); + assertEquals("prompty.core.pipeline.run", reloaded1.signature, "Expected signature"); + assertEquals("Connection refused", reloaded1.error, "Expected error"); + + assertThrows(() -> TraceSpan.fromJson("{"), "TraceSpan.fromJson should reject malformed JSON"); + + assertThrows(() -> TraceSpan.fromYaml(":\n broken"), "TraceSpan.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TraceTimeGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TraceTimeGeneratedTest.java new file mode 100644 index 000000000..7df593174 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TraceTimeGeneratedTest.java @@ -0,0 +1,69 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class TraceTimeGeneratedTest { + private TraceTimeGeneratedTest() { } + + static void run() { + + // TraceTime example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "start": "2026-04-04T12:00:00Z", + "end": "2026-04-04T12:00:01Z", + "duration": 1000 + } + """; + TraceTime instance1 = TraceTime.fromJson(jsonData1); + assertEquals("2026-04-04T12:00:00Z", instance1.start, "Expected start"); + assertEquals("2026-04-04T12:00:01Z", instance1.end, "Expected end"); + assertEquals(1000, instance1.duration, "Expected duration"); + String yamlRoundtrip1 = instance1.toYaml(); + TraceTime fromYaml1 = TraceTime.fromYaml(yamlRoundtrip1); + assertEquals("2026-04-04T12:00:00Z", fromYaml1.start, "Expected start"); + assertEquals("2026-04-04T12:00:01Z", fromYaml1.end, "Expected end"); + assertEquals(1000, fromYaml1.duration, "Expected duration"); + TraceTime reloaded1 = TraceTime.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("2026-04-04T12:00:00Z", reloaded1.start, "Expected start"); + assertEquals("2026-04-04T12:00:01Z", reloaded1.end, "Expected end"); + assertEquals(1000, reloaded1.duration, "Expected duration"); + + assertThrows(() -> TraceTime.fromJson("{"), "TraceTime.fromJson should reject malformed JSON"); + + assertThrows(() -> TraceTime.fromYaml(":\n broken"), "TraceTime.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TrajectoryEventGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TrajectoryEventGeneratedTest.java new file mode 100644 index 000000000..db6d6fb87 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TrajectoryEventGeneratedTest.java @@ -0,0 +1,85 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class TrajectoryEventGeneratedTest { + private TrajectoryEventGeneratedTest() { } + + static void run() { + + // TrajectoryEvent example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "id": "traj_abc123", + "sessionId": "sess_abc123", + "turnId": "turn_001", + "toolCallId": "call_abc123", + "turnIndex": 4, + "eventType": "command", + "createdAt": "2026-06-09T20:00:00Z" + } + """; + TrajectoryEvent instance1 = TrajectoryEvent.fromJson(jsonData1); + assertEquals("traj_abc123", instance1.id, "Expected id"); + assertEquals("sess_abc123", instance1.sessionId, "Expected sessionId"); + assertEquals("turn_001", instance1.turnId, "Expected turnId"); + assertEquals("call_abc123", instance1.toolCallId, "Expected toolCallId"); + assertEquals(4, instance1.turnIndex, "Expected turnIndex"); + assertEquals("command", instance1.eventType, "Expected eventType"); + assertEquals("2026-06-09T20:00:00Z", instance1.createdAt, "Expected createdAt"); + String yamlRoundtrip1 = instance1.toYaml(); + TrajectoryEvent fromYaml1 = TrajectoryEvent.fromYaml(yamlRoundtrip1); + assertEquals("traj_abc123", fromYaml1.id, "Expected id"); + assertEquals("sess_abc123", fromYaml1.sessionId, "Expected sessionId"); + assertEquals("turn_001", fromYaml1.turnId, "Expected turnId"); + assertEquals("call_abc123", fromYaml1.toolCallId, "Expected toolCallId"); + assertEquals(4, fromYaml1.turnIndex, "Expected turnIndex"); + assertEquals("command", fromYaml1.eventType, "Expected eventType"); + assertEquals("2026-06-09T20:00:00Z", fromYaml1.createdAt, "Expected createdAt"); + TrajectoryEvent reloaded1 = TrajectoryEvent.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("traj_abc123", reloaded1.id, "Expected id"); + assertEquals("sess_abc123", reloaded1.sessionId, "Expected sessionId"); + assertEquals("turn_001", reloaded1.turnId, "Expected turnId"); + assertEquals("call_abc123", reloaded1.toolCallId, "Expected toolCallId"); + assertEquals(4, reloaded1.turnIndex, "Expected turnIndex"); + assertEquals("command", reloaded1.eventType, "Expected eventType"); + assertEquals("2026-06-09T20:00:00Z", reloaded1.createdAt, "Expected createdAt"); + + assertThrows(() -> TrajectoryEvent.fromJson("{"), "TrajectoryEvent.fromJson should reject malformed JSON"); + + assertThrows(() -> TrajectoryEvent.fromYaml(":\n broken"), "TrajectoryEvent.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TurnCommitGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TurnCommitGeneratedTest.java new file mode 100644 index 000000000..ae98090a7 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TurnCommitGeneratedTest.java @@ -0,0 +1,65 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class TurnCommitGeneratedTest { + private TurnCommitGeneratedTest() { } + + static void run() { + + // TurnCommit example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "sessionId": "sess_abc123", + "turnId": "turn_abc123" + } + """; + TurnCommit instance1 = TurnCommit.fromJson(jsonData1); + assertEquals("sess_abc123", instance1.sessionId, "Expected sessionId"); + assertEquals("turn_abc123", instance1.turnId, "Expected turnId"); + String yamlRoundtrip1 = instance1.toYaml(); + TurnCommit fromYaml1 = TurnCommit.fromYaml(yamlRoundtrip1); + assertEquals("sess_abc123", fromYaml1.sessionId, "Expected sessionId"); + assertEquals("turn_abc123", fromYaml1.turnId, "Expected turnId"); + TurnCommit reloaded1 = TurnCommit.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("sess_abc123", reloaded1.sessionId, "Expected sessionId"); + assertEquals("turn_abc123", reloaded1.turnId, "Expected turnId"); + + assertThrows(() -> TurnCommit.fromJson("{"), "TurnCommit.fromJson should reject malformed JSON"); + + assertThrows(() -> TurnCommit.fromYaml(":\n broken"), "TurnCommit.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TurnEndPayloadGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TurnEndPayloadGeneratedTest.java new file mode 100644 index 000000000..e6bc33eaf --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TurnEndPayloadGeneratedTest.java @@ -0,0 +1,65 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class TurnEndPayloadGeneratedTest { + private TurnEndPayloadGeneratedTest() { } + + static void run() { + + // TurnEndPayload example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "iterations": 2, + "durationMs": 1500 + } + """; + TurnEndPayload instance1 = TurnEndPayload.fromJson(jsonData1); + assertEquals(2, instance1.iterations, "Expected iterations"); + assertEquals(1500, instance1.durationMs, "Expected durationMs"); + String yamlRoundtrip1 = instance1.toYaml(); + TurnEndPayload fromYaml1 = TurnEndPayload.fromYaml(yamlRoundtrip1); + assertEquals(2, fromYaml1.iterations, "Expected iterations"); + assertEquals(1500, fromYaml1.durationMs, "Expected durationMs"); + TurnEndPayload reloaded1 = TurnEndPayload.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals(2, reloaded1.iterations, "Expected iterations"); + assertEquals(1500, reloaded1.durationMs, "Expected durationMs"); + + assertThrows(() -> TurnEndPayload.fromJson("{"), "TurnEndPayload.fromJson should reject malformed JSON"); + + assertThrows(() -> TurnEndPayload.fromYaml(":\n broken"), "TurnEndPayload.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TurnEngineResultGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TurnEngineResultGeneratedTest.java new file mode 100644 index 000000000..4b451edb5 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TurnEngineResultGeneratedTest.java @@ -0,0 +1,43 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class TurnEngineResultGeneratedTest { + private TurnEngineResultGeneratedTest() { } + + static void run() { + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TurnEventGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TurnEventGeneratedTest.java new file mode 100644 index 000000000..fbcfa77d9 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TurnEventGeneratedTest.java @@ -0,0 +1,81 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class TurnEventGeneratedTest { + private TurnEventGeneratedTest() { } + + static void run() { + + // TurnEvent example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "id": "evt_abc123", + "timestamp": "2026-06-09T20:00:00Z", + "turnId": "turn_001", + "iteration": 0, + "parentId": "evt_parent", + "spanId": "span_tool_001" + } + """; + TurnEvent instance1 = TurnEvent.fromJson(jsonData1); + assertEquals("evt_abc123", instance1.id, "Expected id"); + assertEquals("2026-06-09T20:00:00Z", instance1.timestamp, "Expected timestamp"); + assertEquals("turn_001", instance1.turnId, "Expected turnId"); + assertEquals(0, instance1.iteration, "Expected iteration"); + assertEquals("evt_parent", instance1.parentId, "Expected parentId"); + assertEquals("span_tool_001", instance1.spanId, "Expected spanId"); + String yamlRoundtrip1 = instance1.toYaml(); + TurnEvent fromYaml1 = TurnEvent.fromYaml(yamlRoundtrip1); + assertEquals("evt_abc123", fromYaml1.id, "Expected id"); + assertEquals("2026-06-09T20:00:00Z", fromYaml1.timestamp, "Expected timestamp"); + assertEquals("turn_001", fromYaml1.turnId, "Expected turnId"); + assertEquals(0, fromYaml1.iteration, "Expected iteration"); + assertEquals("evt_parent", fromYaml1.parentId, "Expected parentId"); + assertEquals("span_tool_001", fromYaml1.spanId, "Expected spanId"); + TurnEvent reloaded1 = TurnEvent.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("evt_abc123", reloaded1.id, "Expected id"); + assertEquals("2026-06-09T20:00:00Z", reloaded1.timestamp, "Expected timestamp"); + assertEquals("turn_001", reloaded1.turnId, "Expected turnId"); + assertEquals(0, reloaded1.iteration, "Expected iteration"); + assertEquals("evt_parent", reloaded1.parentId, "Expected parentId"); + assertEquals("span_tool_001", reloaded1.spanId, "Expected spanId"); + + assertThrows(() -> TurnEvent.fromJson("{"), "TurnEvent.fromJson should reject malformed JSON"); + + assertThrows(() -> TurnEvent.fromYaml(":\n broken"), "TurnEvent.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TurnModelRequestGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TurnModelRequestGeneratedTest.java new file mode 100644 index 000000000..87c75e041 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TurnModelRequestGeneratedTest.java @@ -0,0 +1,69 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class TurnModelRequestGeneratedTest { + private TurnModelRequestGeneratedTest() { } + + static void run() { + + // TurnModelRequest example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "sessionId": "sess_abc123", + "turnId": "turn_abc123", + "iteration": 0 + } + """; + TurnModelRequest instance1 = TurnModelRequest.fromJson(jsonData1); + assertEquals("sess_abc123", instance1.sessionId, "Expected sessionId"); + assertEquals("turn_abc123", instance1.turnId, "Expected turnId"); + assertEquals(0, instance1.iteration, "Expected iteration"); + String yamlRoundtrip1 = instance1.toYaml(); + TurnModelRequest fromYaml1 = TurnModelRequest.fromYaml(yamlRoundtrip1); + assertEquals("sess_abc123", fromYaml1.sessionId, "Expected sessionId"); + assertEquals("turn_abc123", fromYaml1.turnId, "Expected turnId"); + assertEquals(0, fromYaml1.iteration, "Expected iteration"); + TurnModelRequest reloaded1 = TurnModelRequest.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("sess_abc123", reloaded1.sessionId, "Expected sessionId"); + assertEquals("turn_abc123", reloaded1.turnId, "Expected turnId"); + assertEquals(0, reloaded1.iteration, "Expected iteration"); + + assertThrows(() -> TurnModelRequest.fromJson("{"), "TurnModelRequest.fromJson should reject malformed JSON"); + + assertThrows(() -> TurnModelRequest.fromYaml(":\n broken"), "TurnModelRequest.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TurnModelResponseGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TurnModelResponseGeneratedTest.java new file mode 100644 index 000000000..122791956 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TurnModelResponseGeneratedTest.java @@ -0,0 +1,43 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class TurnModelResponseGeneratedTest { + private TurnModelResponseGeneratedTest() { } + + static void run() { + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TurnOptionsGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TurnOptionsGeneratedTest.java new file mode 100644 index 000000000..47cbcb440 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TurnOptionsGeneratedTest.java @@ -0,0 +1,87 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class TurnOptionsGeneratedTest { + private TurnOptionsGeneratedTest() { } + + static void run() { + + // TurnOptions example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "maxIterations": 10, + "maxLlmRetries": 3, + "contextBudget": 100000, + "parallelToolCalls": true, + "raw": false, + "turn": 1, + "compaction": { + "strategy": "summarize" + } + } + """; + TurnOptions instance1 = TurnOptions.fromJson(jsonData1); + assertEquals(10, instance1.maxIterations, "Expected maxIterations"); + assertEquals(3, instance1.maxLlmRetries, "Expected maxLlmRetries"); + assertEquals(100000, instance1.contextBudget, "Expected contextBudget"); + assertEquals(true, instance1.parallelToolCalls, "Expected parallelToolCalls"); + assertEquals(false, instance1.raw, "Expected raw"); + assertEquals(1, instance1.turn, "Expected turn"); + assertEquals("summarize", instance1.compaction.strategy, "Expected instance1.compaction.strategy"); + String yamlRoundtrip1 = instance1.toYaml(); + TurnOptions fromYaml1 = TurnOptions.fromYaml(yamlRoundtrip1); + assertEquals(10, fromYaml1.maxIterations, "Expected maxIterations"); + assertEquals(3, fromYaml1.maxLlmRetries, "Expected maxLlmRetries"); + assertEquals(100000, fromYaml1.contextBudget, "Expected contextBudget"); + assertEquals(true, fromYaml1.parallelToolCalls, "Expected parallelToolCalls"); + assertEquals(false, fromYaml1.raw, "Expected raw"); + assertEquals(1, fromYaml1.turn, "Expected turn"); + assertEquals("summarize", fromYaml1.compaction.strategy, "Expected fromYaml1.compaction.strategy"); + TurnOptions reloaded1 = TurnOptions.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals(10, reloaded1.maxIterations, "Expected maxIterations"); + assertEquals(3, reloaded1.maxLlmRetries, "Expected maxLlmRetries"); + assertEquals(100000, reloaded1.contextBudget, "Expected contextBudget"); + assertEquals(true, reloaded1.parallelToolCalls, "Expected parallelToolCalls"); + assertEquals(false, reloaded1.raw, "Expected raw"); + assertEquals(1, reloaded1.turn, "Expected turn"); + assertEquals("summarize", reloaded1.compaction.strategy, "Expected reloaded1.compaction.strategy"); + + assertThrows(() -> TurnOptions.fromJson("{"), "TurnOptions.fromJson should reject malformed JSON"); + + assertThrows(() -> TurnOptions.fromYaml(":\n broken"), "TurnOptions.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TurnStartPayloadGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TurnStartPayloadGeneratedTest.java new file mode 100644 index 000000000..b1f8115c5 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TurnStartPayloadGeneratedTest.java @@ -0,0 +1,65 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class TurnStartPayloadGeneratedTest { + private TurnStartPayloadGeneratedTest() { } + + static void run() { + + // TurnStartPayload example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "agent": "weather-agent", + "maxIterations": 10 + } + """; + TurnStartPayload instance1 = TurnStartPayload.fromJson(jsonData1); + assertEquals("weather-agent", instance1.agent, "Expected agent"); + assertEquals(10, instance1.maxIterations, "Expected maxIterations"); + String yamlRoundtrip1 = instance1.toYaml(); + TurnStartPayload fromYaml1 = TurnStartPayload.fromYaml(yamlRoundtrip1); + assertEquals("weather-agent", fromYaml1.agent, "Expected agent"); + assertEquals(10, fromYaml1.maxIterations, "Expected maxIterations"); + TurnStartPayload reloaded1 = TurnStartPayload.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("weather-agent", reloaded1.agent, "Expected agent"); + assertEquals(10, reloaded1.maxIterations, "Expected maxIterations"); + + assertThrows(() -> TurnStartPayload.fromJson("{"), "TurnStartPayload.fromJson should reject malformed JSON"); + + assertThrows(() -> TurnStartPayload.fromYaml(":\n broken"), "TurnStartPayload.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TurnSummaryGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TurnSummaryGeneratedTest.java new file mode 100644 index 000000000..2c3f2509e --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TurnSummaryGeneratedTest.java @@ -0,0 +1,85 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class TurnSummaryGeneratedTest { + private TurnSummaryGeneratedTest() { } + + static void run() { + + // TurnSummary example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "turnId": "turn_001", + "status": "success", + "iterations": 2, + "llmCalls": 3, + "toolCalls": 2, + "retries": 1, + "durationMs": 2500 + } + """; + TurnSummary instance1 = TurnSummary.fromJson(jsonData1); + assertEquals("turn_001", instance1.turnId, "Expected turnId"); + assertEquals("success", instance1.status, "Expected status"); + assertEquals(2, instance1.iterations, "Expected iterations"); + assertEquals(3, instance1.llmCalls, "Expected llmCalls"); + assertEquals(2, instance1.toolCalls, "Expected toolCalls"); + assertEquals(1, instance1.retries, "Expected retries"); + assertEquals(2500, instance1.durationMs, "Expected durationMs"); + String yamlRoundtrip1 = instance1.toYaml(); + TurnSummary fromYaml1 = TurnSummary.fromYaml(yamlRoundtrip1); + assertEquals("turn_001", fromYaml1.turnId, "Expected turnId"); + assertEquals("success", fromYaml1.status, "Expected status"); + assertEquals(2, fromYaml1.iterations, "Expected iterations"); + assertEquals(3, fromYaml1.llmCalls, "Expected llmCalls"); + assertEquals(2, fromYaml1.toolCalls, "Expected toolCalls"); + assertEquals(1, fromYaml1.retries, "Expected retries"); + assertEquals(2500, fromYaml1.durationMs, "Expected durationMs"); + TurnSummary reloaded1 = TurnSummary.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("turn_001", reloaded1.turnId, "Expected turnId"); + assertEquals("success", reloaded1.status, "Expected status"); + assertEquals(2, reloaded1.iterations, "Expected iterations"); + assertEquals(3, reloaded1.llmCalls, "Expected llmCalls"); + assertEquals(2, reloaded1.toolCalls, "Expected toolCalls"); + assertEquals(1, reloaded1.retries, "Expected retries"); + assertEquals(2500, reloaded1.durationMs, "Expected durationMs"); + + assertThrows(() -> TurnSummary.fromJson("{"), "TurnSummary.fromJson should reject malformed JSON"); + + assertThrows(() -> TurnSummary.fromYaml(":\n broken"), "TurnSummary.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TurnTraceGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TurnTraceGeneratedTest.java new file mode 100644 index 000000000..bf12a5d38 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TurnTraceGeneratedTest.java @@ -0,0 +1,69 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class TurnTraceGeneratedTest { + private TurnTraceGeneratedTest() { } + + static void run() { + + // TurnTrace example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "version": "1", + "runtime": "typescript", + "promptyVersion": "2.0.0" + } + """; + TurnTrace instance1 = TurnTrace.fromJson(jsonData1); + assertEquals("1", instance1.version, "Expected version"); + assertEquals("typescript", instance1.runtime, "Expected runtime"); + assertEquals("2.0.0", instance1.promptyVersion, "Expected promptyVersion"); + String yamlRoundtrip1 = instance1.toYaml(); + TurnTrace fromYaml1 = TurnTrace.fromYaml(yamlRoundtrip1); + assertEquals("1", fromYaml1.version, "Expected version"); + assertEquals("typescript", fromYaml1.runtime, "Expected runtime"); + assertEquals("2.0.0", fromYaml1.promptyVersion, "Expected promptyVersion"); + TurnTrace reloaded1 = TurnTrace.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("1", reloaded1.version, "Expected version"); + assertEquals("typescript", reloaded1.runtime, "Expected runtime"); + assertEquals("2.0.0", reloaded1.promptyVersion, "Expected promptyVersion"); + + assertThrows(() -> TurnTrace.fromJson("{"), "TurnTrace.fromJson should reject malformed JSON"); + + assertThrows(() -> TurnTrace.fromYaml(":\n broken"), "TurnTrace.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TypraGeneratedTests.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TypraGeneratedTests.java new file mode 100644 index 000000000..484e0faef --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TypraGeneratedTests.java @@ -0,0 +1,157 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +public final class TypraGeneratedTests { + private TypraGeneratedTests() { } + + public static void main(String[] args) { + PropertyGeneratedTest.run(); + UnionPropertyGeneratedTest.run(); + ObjectPropertyGeneratedTest.run(); + ArrayPropertyGeneratedTest.run(); + ConnectionGeneratedTest.run(); + ReferenceConnectionGeneratedTest.run(); + RemoteConnectionGeneratedTest.run(); + ApiKeyConnectionGeneratedTest.run(); + AnonymousConnectionGeneratedTest.run(); + OAuthConnectionGeneratedTest.run(); + FoundryConnectionGeneratedTest.run(); + ModelOptionsGeneratedTest.run(); + ModelGeneratedTest.run(); + BindingGeneratedTest.run(); + ToolGeneratedTest.run(); + FunctionToolGeneratedTest.run(); + CustomToolGeneratedTest.run(); + McpApprovalModeGeneratedTest.run(); + McpToolGeneratedTest.run(); + OpenApiToolGeneratedTest.run(); + PromptyToolGeneratedTest.run(); + FormatConfigGeneratedTest.run(); + ParserConfigGeneratedTest.run(); + TemplateGeneratedTest.run(); + PromptyGeneratedTest.run(); + ContentPartGeneratedTest.run(); + TextPartGeneratedTest.run(); + ImagePartGeneratedTest.run(); + FilePartGeneratedTest.run(); + AudioPartGeneratedTest.run(); + MessageGeneratedTest.run(); + ToolContextGeneratedTest.run(); + ToolResultGeneratedTest.run(); + ToolDispatchResultGeneratedTest.run(); + ToolCallGeneratedTest.run(); + GuardrailResultGeneratedTest.run(); + ThreadMarkerGeneratedTest.run(); + InvokerErrorGeneratedTest.run(); + ValidationErrorGeneratedTest.run(); + FileNotFoundErrorGeneratedTest.run(); + ValidationResultGeneratedTest.run(); + TokenUsageGeneratedTest.run(); + InvocationUsageGeneratedTest.run(); + ModelInfoGeneratedTest.run(); + SubscriptionInfoGeneratedTest.run(); + AiResourceInfoGeneratedTest.run(); + ProjectInfoGeneratedTest.run(); + MemoryEntryGeneratedTest.run(); + MemoryStoreGeneratedTest.run(); + DelegatedStateReferenceGeneratedTest.run(); + InvocationContextStateGeneratedTest.run(); + InvocationContextDecisionGeneratedTest.run(); + ModelInvocationContextSnapshotGeneratedTest.run(); + ModelInvocationRequestGeneratedTest.run(); + ModelToolRequestGeneratedTest.run(); + ModelToolResultGeneratedTest.run(); + ModelInvocationResponseGeneratedTest.run(); + EngineEventGeneratedTest.run(); + ModelReconciliationStateGeneratedTest.run(); + EnginePermissionDecisionGeneratedTest.run(); + EngineCheckpointGeneratedTest.run(); + ResumeContextGeneratedTest.run(); + TurnCommitGeneratedTest.run(); + TurnEngineResultGeneratedTest.run(); + HostPolicyRequestGeneratedTest.run(); + HostPolicyResultGeneratedTest.run(); + FinalOutputPolicyRequestGeneratedTest.run(); + FinalOutputPolicyResultGeneratedTest.run(); + RetryPolicyRequestGeneratedTest.run(); + ContextRequestGeneratedTest.run(); + ContextCandidateGeneratedTest.run(); + CompactionConfigGeneratedTest.run(); + TurnOptionsGeneratedTest.run(); + HostToolResultGeneratedTest.run(); + TurnModelRequestGeneratedTest.run(); + HostToolRequestGeneratedTest.run(); + TurnModelResponseGeneratedTest.run(); + RunTurnRequestGeneratedTest.run(); + RedactedFieldGeneratedTest.run(); + RedactionMetadataGeneratedTest.run(); + CheckpointGeneratedTest.run(); + RunTurnResultGeneratedTest.run(); + ReplayJournalRecordGeneratedTest.run(); + ReplayVerificationRequestGeneratedTest.run(); + ReplayMismatchGeneratedTest.run(); + ReplayVerificationResultGeneratedTest.run(); + TurnEventGeneratedTest.run(); + TurnStartPayloadGeneratedTest.run(); + TurnEndPayloadGeneratedTest.run(); + LlmStartPayloadGeneratedTest.run(); + LlmCompletePayloadGeneratedTest.run(); + RetryPayloadGeneratedTest.run(); + PermissionRequestedPayloadGeneratedTest.run(); + PermissionCompletedPayloadGeneratedTest.run(); + PermissionRequestGeneratedTest.run(); + PermissionDecisionGeneratedTest.run(); + TokenEventPayloadGeneratedTest.run(); + ThinkingEventPayloadGeneratedTest.run(); + ToolCallStartPayloadGeneratedTest.run(); + ToolCallCompletePayloadGeneratedTest.run(); + ToolExecutionStartPayloadGeneratedTest.run(); + ToolExecutionCompletePayloadGeneratedTest.run(); + HookStartPayloadGeneratedTest.run(); + HookEndPayloadGeneratedTest.run(); + ToolResultPayloadGeneratedTest.run(); + StatusEventPayloadGeneratedTest.run(); + MessagesUpdatedPayloadGeneratedTest.run(); + DoneEventPayloadGeneratedTest.run(); + ErrorEventPayloadGeneratedTest.run(); + CompactionStartPayloadGeneratedTest.run(); + CompactionCompletePayloadGeneratedTest.run(); + CompactionFailedPayloadGeneratedTest.run(); + TurnSummaryGeneratedTest.run(); + TurnTraceGeneratedTest.run(); + HarnessContextGeneratedTest.run(); + SessionStartPayloadGeneratedTest.run(); + SessionEndPayloadGeneratedTest.run(); + SessionWarningPayloadGeneratedTest.run(); + SessionEventGeneratedTest.run(); + TrajectoryEventGeneratedTest.run(); + SessionFileRefGeneratedTest.run(); + SessionRefGeneratedTest.run(); + SessionSummaryGeneratedTest.run(); + SessionTraceGeneratedTest.run(); + StreamChunkGeneratedTest.run(); + TextChunkGeneratedTest.run(); + ThinkingChunkGeneratedTest.run(); + ToolChunkGeneratedTest.run(); + UsageChunkGeneratedTest.run(); + ErrorChunkGeneratedTest.run(); + StreamOptionsGeneratedTest.run(); + TraceTimeGeneratedTest.run(); + TraceSpanGeneratedTest.run(); + TraceFileGeneratedTest.run(); + OAuthTokenGeneratedTest.run(); + DeviceAuthorizationGeneratedTest.run(); + AuthorizationCodeFlowGeneratedTest.run(); + AnthropicTextBlockGeneratedTest.run(); + AnthropicImageSourceGeneratedTest.run(); + AnthropicImageBlockGeneratedTest.run(); + AnthropicToolUseBlockGeneratedTest.run(); + AnthropicToolResultBlockGeneratedTest.run(); + AnthropicWireMessageGeneratedTest.run(); + AnthropicToolDefinitionGeneratedTest.run(); + AnthropicMessagesRequestGeneratedTest.run(); + AnthropicUsageGeneratedTest.run(); + AnthropicMessagesResponseGeneratedTest.run(); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/UnionPropertyGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/UnionPropertyGeneratedTest.java new file mode 100644 index 000000000..6875e01e6 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/UnionPropertyGeneratedTest.java @@ -0,0 +1,74 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class UnionPropertyGeneratedTest { + private UnionPropertyGeneratedTest() { } + + static void run() { + + // UnionProperty example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "anyOf": [ + { + "kind": "string" + }, + { + "kind": "boolean" + } + ] + } + """; + UnionProperty instance1 = UnionProperty.fromJson(jsonData1); + assertEquals(2, instance1.anyOf.size(), "Expected anyOf size"); + assertEquals("string", instance1.anyOf.get(0).kind, "Expected instance1.anyOf.get(0).kind"); + assertEquals("boolean", instance1.anyOf.get(1).kind, "Expected instance1.anyOf.get(1).kind"); + String yamlRoundtrip1 = instance1.toYaml(); + UnionProperty fromYaml1 = UnionProperty.fromYaml(yamlRoundtrip1); + assertEquals(2, fromYaml1.anyOf.size(), "Expected anyOf size"); + assertEquals("string", fromYaml1.anyOf.get(0).kind, "Expected fromYaml1.anyOf.get(0).kind"); + assertEquals("boolean", fromYaml1.anyOf.get(1).kind, "Expected fromYaml1.anyOf.get(1).kind"); + UnionProperty reloaded1 = UnionProperty.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals(2, reloaded1.anyOf.size(), "Expected anyOf size"); + assertEquals("string", reloaded1.anyOf.get(0).kind, "Expected reloaded1.anyOf.get(0).kind"); + assertEquals("boolean", reloaded1.anyOf.get(1).kind, "Expected reloaded1.anyOf.get(1).kind"); + + assertThrows(() -> UnionProperty.fromJson("{"), "UnionProperty.fromJson should reject malformed JSON"); + + assertThrows(() -> UnionProperty.fromYaml(":\n broken"), "UnionProperty.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/UsageChunkGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/UsageChunkGeneratedTest.java new file mode 100644 index 000000000..2e6735035 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/UsageChunkGeneratedTest.java @@ -0,0 +1,43 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class UsageChunkGeneratedTest { + private UsageChunkGeneratedTest() { } + + static void run() { + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ValidationErrorGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ValidationErrorGeneratedTest.java new file mode 100644 index 000000000..d14284a30 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ValidationErrorGeneratedTest.java @@ -0,0 +1,69 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class ValidationErrorGeneratedTest { + private ValidationErrorGeneratedTest() { } + + static void run() { + + // ValidationError example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "message": "Missing required input: firstName", + "property": "firstName", + "constraint": "required" + } + """; + ValidationError instance1 = ValidationError.fromJson(jsonData1); + assertEquals("Missing required input: firstName", instance1.message, "Expected message"); + assertEquals("firstName", instance1.property, "Expected property"); + assertEquals("required", instance1.constraint, "Expected constraint"); + String yamlRoundtrip1 = instance1.toYaml(); + ValidationError fromYaml1 = ValidationError.fromYaml(yamlRoundtrip1); + assertEquals("Missing required input: firstName", fromYaml1.message, "Expected message"); + assertEquals("firstName", fromYaml1.property, "Expected property"); + assertEquals("required", fromYaml1.constraint, "Expected constraint"); + ValidationError reloaded1 = ValidationError.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals("Missing required input: firstName", reloaded1.message, "Expected message"); + assertEquals("firstName", reloaded1.property, "Expected property"); + assertEquals("required", reloaded1.constraint, "Expected constraint"); + + assertThrows(() -> ValidationError.fromJson("{"), "ValidationError.fromJson should reject malformed JSON"); + + assertThrows(() -> ValidationError.fromYaml(":\n broken"), "ValidationError.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ValidationResultGeneratedTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ValidationResultGeneratedTest.java new file mode 100644 index 000000000..87e35c8cc --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ValidationResultGeneratedTest.java @@ -0,0 +1,65 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +final class ValidationResultGeneratedTest { + private ValidationResultGeneratedTest() { } + + static void run() { + + // ValidationResult example 1: fromJson, fromYaml, save, and reload + String jsonData1 = """ + { + "valid": true, + "errors": [] + } + """; + ValidationResult instance1 = ValidationResult.fromJson(jsonData1); + assertEquals(true, instance1.valid, "Expected valid"); + assertEquals(0, instance1.errors.size(), "Expected errors size"); + String yamlRoundtrip1 = instance1.toYaml(); + ValidationResult fromYaml1 = ValidationResult.fromYaml(yamlRoundtrip1); + assertEquals(true, fromYaml1.valid, "Expected valid"); + assertEquals(0, fromYaml1.errors.size(), "Expected errors size"); + ValidationResult reloaded1 = ValidationResult.load(instance1.save(new SaveContext()), new LoadContext()); + assertEquals(true, reloaded1.valid, "Expected valid"); + assertEquals(0, reloaded1.errors.size(), "Expected errors size"); + + assertThrows(() -> ValidationResult.fromJson("{"), "ValidationResult.fromJson should reject malformed JSON"); + + assertThrows(() -> ValidationResult.fromYaml(":\n broken"), "ValidationResult.fromYaml should reject malformed YAML"); + } + + private static Object unwrapEnum(Object value) { + if (value == null || !value.getClass().isEnum()) return value; + try { + return value.getClass().getField("value").get(value); + } catch (ReflectiveOperationException ignored) { + return value; + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + expected = unwrapEnum(expected); + actual = unwrapEnum(actual); + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + if (Math.abs(expectedNumber.doubleValue() - actualNumber.doubleValue()) < 0.000001d) return; + } + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + ": expected " + expected + ", got " + actual); + } + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void assertThrows(Runnable runnable, String message) { + try { + runnable.run(); + } catch (RuntimeException expected) { + return; + } + throw new AssertionError(message); + } +} diff --git a/runtime/java/prompty/src/testFixtures/java/com/microsoft/prompty/LiveEnv.java b/runtime/java/prompty/src/testFixtures/java/com/microsoft/prompty/LiveEnv.java new file mode 100644 index 000000000..7fe762e3d --- /dev/null +++ b/runtime/java/prompty/src/testFixtures/java/com/microsoft/prompty/LiveEnv.java @@ -0,0 +1,207 @@ +package com.microsoft.prompty; + +import com.microsoft.prompty.model.LoadContext; +import com.microsoft.prompty.model.Prompty; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Assumptions; + +/** + * Shared setup for the live provider suites, which call real endpoints instead of fixtures. + * + *

Credentials come from a {@code .env} beside the Gradle build rather than from the process + * environment, so a developer can run the live suites without exporting keys into every shell. The + * file is deliberately untracked; nothing here writes it, and nothing here logs a value. + * + *

Every live test asks {@link #require} for the variables it needs, so a machine holding only + * some providers' credentials skips the rest rather than failing. That keeps a partial credential + * set an honest "not exercised" instead of a red build that says nothing about the code. + */ +public final class LiveEnv { + + private static boolean loaded; + + private LiveEnv() {} + + /** + * Load {@code runtime/java/.env} into the runtime's environment overlay, once per JVM. + * + *

Real process variables win, matching the Rust suite, so CI can inject credentials without + * anyone having to delete a local file first. + */ + public static synchronized void load() { + if (loaded) { + return; + } + loaded = true; + Registry.bootstrap(); + for (Path candidate : candidates()) { + if (!Files.isRegularFile(candidate)) { + continue; + } + List lines; + try { + lines = Files.readAllLines(candidate, StandardCharsets.UTF_8); + } catch (IOException e) { + continue; + } + for (String raw : lines) { + String line = raw.trim(); + if (line.isEmpty() || line.startsWith("#")) { + continue; + } + int eq = line.indexOf('='); + if (eq <= 0) { + continue; + } + String key = line.substring(0, eq).trim(); + String value = stripQuotes(line.substring(eq + 1).trim()); + if (key.isEmpty() || Environment.lookup(key).isPresent()) { + continue; + } + Environment.set(key, value); + } + return; + } + } + + /** + * Candidate {@code .env} locations, nearest first: the module, the Gradle root, the repository + * root. + * + *

Tests run with the module directory as the working directory, so the Gradle root — which is + * where the file actually lives — is one level up. + */ + private static List candidates() { + Path module = Path.of("").toAbsolutePath(); + Path gradleRoot = module.getParent() == null ? module : module.getParent(); + Path repoRoot = module.resolve("../../..").normalize(); + return List.of(module.resolve(".env"), gradleRoot.resolve(".env"), repoRoot.resolve(".env")); + } + + private static String stripQuotes(String value) { + if (value.length() >= 2 + && ((value.startsWith("\"") && value.endsWith("\"")) + || (value.startsWith("'") && value.endsWith("'")))) { + return value.substring(1, value.length() - 1); + } + return value; + } + + /** + * Skip the calling test unless every named variable is set to a non-blank value. + * + *

Reports the missing name so a skipped run says which credential was absent rather than + * leaving someone to guess. + */ + public static void require(String... names) { + load(); + for (String name : names) { + String value = Environment.lookup(name).orElse(""); + Assumptions.assumeTrue(!value.isBlank(), () -> "live test skipped: " + name + " is not set"); + } + } + + /** Read a variable, falling back when it is absent or blank. */ + public static String get(String name, String fallback) { + load(); + String value = Environment.lookup(name).orElse(""); + return value.isBlank() ? fallback : value; + } + + /** + * Build a prompt aimed at a live endpoint. + * + *

The live suites vary only by provider, model and a handful of knobs, so they share one + * builder; a suite that assembled its own prompt could pass while disagreeing with the others + * about what was being asked. + */ + public static Prompty agent(Spec spec) { + Map connection = new LinkedHashMap<>(); + connection.put("kind", spec.connectionKind); + + Map model = new LinkedHashMap<>(); + model.put("id", spec.modelId); + model.put("provider", spec.provider); + model.put("apiType", spec.apiType); + model.put("connection", connection); + if (!spec.options.isEmpty()) { + model.put("options", spec.options); + } + + Map data = new LinkedHashMap<>(); + data.put("name", "live-" + spec.apiType); + data.put("kind", "prompt"); + data.put("model", model); + data.put("instructions", spec.instructions); + if (!spec.tools.isEmpty()) { + data.put("tools", spec.tools); + } + if (!spec.outputs.isEmpty()) { + data.put("outputs", spec.outputs); + } + return Prompty.load(data, new LoadContext()); + } + + /** Mutable description of a live prompt. */ + public static final class Spec { + private final String provider; + private String modelId; + private String apiType = "chat"; + private String connectionKind = "key"; + private String instructions = ""; + private Map options = new LinkedHashMap<>(); + private List tools = List.of(); + private List outputs = List.of(); + + public Spec(String provider, String modelId) { + this.provider = provider; + this.modelId = modelId; + } + + public Spec modelId(String value) { + this.modelId = value; + return this; + } + + public Spec apiType(String value) { + this.apiType = value; + return this; + } + + public Spec connectionKind(String value) { + this.connectionKind = value; + return this; + } + + public Spec instructions(String value) { + this.instructions = value; + return this; + } + + public Spec chat(String system, String user) { + this.instructions = "system:\n" + system + "\nuser:\n" + user; + return this; + } + + public Spec options(Map value) { + this.options = new LinkedHashMap<>(value); + return this; + } + + public Spec tools(List value) { + this.tools = value; + return this; + } + + public Spec outputs(List value) { + this.outputs = value; + return this; + } + } +} diff --git a/runtime/java/prompty/src/testFixtures/java/com/microsoft/prompty/SpecVectors.java b/runtime/java/prompty/src/testFixtures/java/com/microsoft/prompty/SpecVectors.java new file mode 100644 index 000000000..8484e4f25 --- /dev/null +++ b/runtime/java/prompty/src/testFixtures/java/com/microsoft/prompty/SpecVectors.java @@ -0,0 +1,370 @@ +package com.microsoft.prompty; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import com.microsoft.prompty.model.TypraJson; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** + * Access to the shared cross-runtime spec vectors, and the partial matcher they are written for. + * + *

The vectors under {@code spec/vectors} are the contract every Prompty runtime is measured + * against, so this harness reads them from the repository rather than copying them into the Java + * tree — a copy could drift, and drift here would be invisible. + * + *

Matching is deliberately partial: a vector asserts the fields it cares about and stays silent + * about the rest. That keeps a vector focused on the behaviour it is describing and lets runtimes + * carry additional detail without every vector needing to enumerate it. + */ +public final class SpecVectors { + + private SpecVectors() {} + + /** The repository root, located by walking up until {@code spec/vectors} is found. */ + public static Path repoRoot() { + Path current = Path.of("").toAbsolutePath(); + while (current != null) { + if (Files.isDirectory(current.resolve("spec").resolve("vectors"))) { + return current; + } + current = current.getParent(); + } + throw new IllegalStateException("could not locate the repository root from " + Path.of("").toAbsolutePath()); + } + + /** The directory holding shared {@code .prompty} fixtures. */ + public static Path fixtures() { + return repoRoot().resolve("spec").resolve("fixtures"); + } + + /** Read and parse a vector file such as {@code load/load_vectors.json}. */ + public static Object read(String relativePath) { + Path path = repoRoot().resolve("spec").resolve("vectors").resolve(relativePath); + try { + return TypraJson.parse(Files.readString(path, StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new UncheckedIOException("cannot read spec vectors: " + path, e); + } + } + + /** Read a vector file that holds a bare JSON array of cases. */ + @SuppressWarnings("unchecked") + public static List> readArray(String relativePath) { + Object parsed = read(relativePath); + if (!(parsed instanceof List list)) { + throw new IllegalStateException(relativePath + " is not a JSON array"); + } + List> cases = new ArrayList<>(list.size()); + for (Object item : list) { + cases.add((Map) item); + } + return cases; + } + + /** + * Read the cases from a vector file that wraps them under a named key. + * + *

Some suites are a bare array; the newer ones carry a description alongside the cases, which + * is worth keeping because it states the contract the vectors are asserting. + */ + @SuppressWarnings("unchecked") + public static List> readCases(String relativePath, String key) { + Object parsed = read(relativePath); + if (!(parsed instanceof Map root) || !(root.get(key) instanceof List list)) { + throw new IllegalStateException(relativePath + " has no '" + key + "' array"); + } + List> cases = new ArrayList<>(list.size()); + for (Object item : list) { + cases.add((Map) item); + } + return cases; + } + + /** A map-valued member of {@code value}, or an empty map when absent. */ + @SuppressWarnings("unchecked") + public static Map map(Map value, String key) { + Object nested = value == null ? null : value.get(key); + return nested instanceof Map ? (Map) nested : Map.of(); + } + + /** A string-valued member of {@code value}, or null when absent or not a string. */ + public static String string(Map value, String key) { + Object nested = value == null ? null : value.get(key); + return nested instanceof String text ? text : null; + } + + // ---------------------------------------------------------------- matching + + /** + * Assert that {@code actual} contains everything {@code expected} specifies. + * + *

Maps are matched key by key and lists element by element; scalars must be equal. An expected + * null asserts only that the actual value carries nothing, which is how a vector says "this field + * should not be populated" without having to describe the whole surrounding object. Null, an empty + * list and an empty map all satisfy it: the generated models materialize optional collections, so + * a field the wire never supplied still arrives as an empty collection rather than disappearing. + * An empty string is not absent — it is a value a vector has to state explicitly. + * + *

This is deliberately a subset match: a key the vector does not mention is not + * checked. That is what lets a load vector describe one corner of a prompt without restating the + * whole thing. Where a vector describes a complete artefact — a request body, a processed result — + * use {@link #assertEquivalent} instead, which also rejects fields the vector never asked for. + * + *

Numbers compare by value rather than by boxed type, since JSON makes no distinction between + * an integer parsed as {@code Long} and one parsed as {@code Integer}. + */ + public static void assertMatches(String label, Object expected, Object actual) { + assertMatches(label, "", expected, actual); + } + + /** + * Assert that {@code actual} is exactly what {@code expected} describes, with no extra fields. + * + *

Same comparison as {@link #assertMatches} with one addition: objects must have exactly the + * same set of keys. A vector that fully describes a request body is also asserting that nothing + * else is sent, and a subset match would let a runtime add a spurious field to every request + * without a single test noticing. + * + *

This is the reference implementation's comparison but for two deliberate relaxations. Numbers + * are compared to single precision rather than bit-exactly, because a 32-bit {@code temperature} + * cannot hold a value like 0.7 that the vector states in full precision. An expected null is + * satisfied by an empty collection as well as by a missing value, for the reason given on {@link + * #assertMatches}. Note that the key-set check is not relaxed: a vector that omits a key entirely + * still rejects a runtime that emits it, even as an empty collection. Every other difference fails + * here exactly as it would there. + */ + public static void assertEquivalent(String label, Object expected, Object actual) { + assertSameKeys(label, "", expected, actual); + assertMatches(label, "", expected, actual); + } + + /** + * Assert that every object in the two trees has the same key set. + * + *

Both directions matter. An unexpected key means the runtime sends something the vector never + * described; a missing one means it dropped a field the vector requires. {@link #assertMatches} + * catches neither on its own — it walks only the expected side, and it accepts an absent key + * wherever the vector states an explicit null. + */ + private static void assertSameKeys(String label, String path, Object expected, Object actual) { + String where = label + (path.isEmpty() ? "" : " at " + path); + + if (expected instanceof Map expectedMap) { + Object candidate = actual instanceof List list ? asNamedMap(list) : actual; + if (!(candidate instanceof Map actualMap)) { + // The shape mismatch itself is reported by assertMatches, in better terms than here. + return; + } + for (Object key : actualMap.keySet()) { + assertTrue( + expectedMap.containsKey(key), + where + ": unexpected field '" + key + "' the vector does not describe"); + } + for (Map.Entry entry : expectedMap.entrySet()) { + String key = String.valueOf(entry.getKey()); + assertTrue( + actualMap.containsKey(key), where + ": missing field '" + key + "' the vector requires"); + assertSameKeys(label, join(path, key), entry.getValue(), actualMap.get(key)); + } + return; + } + + if (expected instanceof List expectedList) { + Object candidate = actual instanceof Map named ? asNamedList(named) : actual; + if (!(candidate instanceof List actualList)) { + return; + } + int shared = Math.min(expectedList.size(), actualList.size()); + for (int i = 0; i < shared; i++) { + assertSameKeys(label, path + "[" + i + "]", expectedList.get(i), actualList.get(i)); + } + } + } + + private static void assertMatches(String label, String path, Object expected, Object actual) { + String where = label + (path.isEmpty() ? "" : " at " + path); + + if (expected == null) { + // A vector writes `null` for "this field carries nothing". Runtimes are free to spell that as + // an absent value or as an empty collection: the models materialize optional collections + // (`tools?: Tool[] = #[]` becomes an empty list, matching C#, TypeScript and Rust), so an + // empty list or map means the same thing to a caller as no list at all. The reference + // implementation reconciles the two at this same seam — Rust's `as_tools()` reports `None` + // for an empty vector, and Python checks length rather than identity. A non-empty value is + // still a real difference and fails here. + assertTrue(isAbsent(actual), where + ": expected absent or empty, got " + describe(actual)); + return; + } + + if (expected instanceof Map expectedMap) { + Object candidate = actual instanceof List list ? asNamedMap(list) : actual; + assertTrue(candidate instanceof Map, where + ": expected an object, got " + describe(actual)); + Map actualMap = (Map) candidate; + for (Map.Entry entry : expectedMap.entrySet()) { + String key = String.valueOf(entry.getKey()); + assertMatches(label, join(path, key), entry.getValue(), actualMap.get(key)); + } + return; + } + + if (expected instanceof List expectedList) { + Object candidate = actual instanceof Map named ? asNamedList(named) : actual; + assertTrue(candidate instanceof List, where + ": expected an array, got " + describe(actual)); + List actualList = (List) candidate; + assertEquals(expectedList.size(), actualList.size(), where + ": array length"); + for (int i = 0; i < expectedList.size(); i++) { + assertMatches(label, path + "[" + i + "]", expectedList.get(i), actualList.get(i)); + } + return; + } + + if (expected instanceof Number expectedNumber && actual instanceof Number actualNumber) { + // Vectors are written in JSON, which has one number type; runtimes store them at whatever + // width the schema declares. A 32-bit `temperature` cannot hold 0.7 exactly, so compare at + // single precision rather than demanding a bit-exact match the schema never promised. + double want = expectedNumber.doubleValue(); + double got = actualNumber.doubleValue(); + double tolerance = 1e-6 * Math.max(1.0, Math.abs(want)); + assertTrue( + Math.abs(want - got) <= tolerance, where + ": expected " + want + ", got " + got); + return; + } + + assertEquals(expected, actual, where); + } + + private static String join(String path, String key) { + return path.isEmpty() ? key : path + "." + key; + } + + /** + * Rewrite a named collection — {@code {alice: {...}, bob: {...}}} — as the list it stands for. + * + *

Several collections in the model are written with the item's name as the key, because that is + * how a prompt author naturally writes them and it rules out duplicates. Both forms round-trip to + * the same object graph, and the vectors use whichever reads better case by case, so the two are + * reconciled here rather than in every vector. + */ + private static List asNamedList(Map named) { + List items = new ArrayList<>(named.size()); + for (Map.Entry entry : named.entrySet()) { + Map item = new LinkedHashMap<>(); + item.put("name", entry.getKey()); + if (entry.getValue() instanceof Map value) { + value.forEach((k, v) -> item.put(String.valueOf(k), v)); + } + items.add(item); + } + return items; + } + + /** The inverse of {@link #asNamedList}: an array of named items keyed back by name. */ + private static Map asNamedMap(List items) { + Map named = new LinkedHashMap<>(); + for (Object item : items) { + if (!(item instanceof Map map) || !(map.get("name") instanceof String name)) { + return named; + } + Map value = new LinkedHashMap<>(); + map.forEach( + (k, v) -> { + if (!"name".equals(k)) { + value.put(String.valueOf(k), v); + } + }); + named.put(name, value); + } + return named; + } + + private static String describe(Object value) { + return value == null ? "null" : value.getClass().getSimpleName() + " " + value; + } + + /** + * Report whether a saved value carries nothing, which a vector writes as {@code null}. + * + *

An empty list or map counts as absent. Optional collections are materialized by the + * generated models, so a field the wire never supplied still saves as an empty collection rather + * than disappearing; treating that as a difference would fail every vector that states an optional + * collection as null. A collection with entries in it is a real difference and is not absent, and + * an empty string is a value rather than an absence. + */ + private static boolean isAbsent(Object value) { + if (value == null) { + return true; + } + if (value instanceof Collection collection) { + return collection.isEmpty(); + } + if (value instanceof Map map) { + return map.isEmpty(); + } + return false; + } + + // ---------------------------------------------------------------- error matching + + /** + * Assert that {@code actual} reports the failure a vector describes. + * + *

Vectors name errors loosely — "FileNotFoundError", "invalid frontmatter" — because exact + * wording is a runtime's own business and pinning it would make the shared vectors unusable. The + * match is therefore on meaning rather than text, but it requires every significant word + * of the expectation to appear: matching on any one shared word would let "invalid template" pass + * a vector that asked for "invalid frontmatter". + */ + public static void assertErrorMatches(String label, String expected, Throwable actual) { + if (actual == null) { + fail(label + ": expected an error matching \"" + expected + "\", but the call succeeded"); + } + String message = actual.getMessage() == null ? "" : actual.getMessage().toLowerCase(Locale.ROOT); + String wanted = expected.toLowerCase(Locale.ROOT); + + if (wanted.contains("filenotfounderror")) { + boolean matched = + actual instanceof LoadException load && load.kind() == LoadException.Kind.FILE_NOT_FOUND; + assertTrue(matched || message.contains("not found"), label + ": expected a not-found error, got " + actual); + return; + } + + if (message.contains(wanted)) { + return; + } + + // Every distinguishing word must be present. Short words ("not", "set") and the generic + // "error" carry no signal, so they are not required — but they are not sufficient either. + List required = new ArrayList<>(); + for (String word : wanted.split("\\W+")) { + if (word.length() > 3 && !word.equals("error")) { + required.add(word); + } + } + if (!required.isEmpty()) { + boolean all = true; + for (String word : required) { + if (!message.contains(word)) { + all = false; + break; + } + } + if (all) { + return; + } + } + + fail(label + ": expected an error matching \"" + expected + "\", got \"" + actual.getMessage() + "\""); + } +} diff --git a/runtime/java/prompty/src/testFixtures/java/com/microsoft/prompty/VectorAgents.java b/runtime/java/prompty/src/testFixtures/java/com/microsoft/prompty/VectorAgents.java new file mode 100644 index 000000000..2911757c7 --- /dev/null +++ b/runtime/java/prompty/src/testFixtures/java/com/microsoft/prompty/VectorAgents.java @@ -0,0 +1,118 @@ +package com.microsoft.prompty; + +import com.microsoft.prompty.model.LoadContext; +import com.microsoft.prompty.model.Message; +import com.microsoft.prompty.model.Prompty; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Rebuilds prompts and messages from the declarative descriptions in the shared spec vectors. + * + *

Every provider suite needs the same reconstruction, and a provider that reconstructed vectors + * its own way could pass while disagreeing with the others about what the fixture said. Keeping one + * implementation here means the suites differ only in the conversion they are grading. + */ +public final class VectorAgents { + + private VectorAgents() {} + + /** + * Rebuild the prompt a wire vector describes. + * + * @param defaultModelId the model id to use when the vector does not name one + * @param defaultProvider the provider to assume when the vector does not name one + */ + public static Prompty buildAgent( + Map input, String defaultModelId, String defaultProvider) { + Map model = new LinkedHashMap<>(); + model.put("id", input.getOrDefault("model_id", defaultModelId)); + model.put("apiType", input.getOrDefault("apiType", "chat")); + model.put("provider", input.getOrDefault("provider", defaultProvider)); + // Empty collections are omitted rather than passed through, because an empty `options` object + // and an absent one mean the same thing to a prompt but not to every loader. + if (input.get("options") instanceof Map options && !options.isEmpty()) { + model.put("options", options); + } + + Map data = new LinkedHashMap<>(); + data.put("name", "test"); + data.put("kind", "prompt"); + data.put("instructions", "test"); + data.put("model", model); + if (input.get("tools") instanceof List tools && !tools.isEmpty()) { + data.put("tools", tools); + } + if (input.get("outputs") instanceof List outputs && !outputs.isEmpty()) { + data.put("outputs", outputs); + } + return Prompty.load(data, new LoadContext()); + } + + /** + * Rebuild the messages a vector describes. + * + *

The fixtures name every part's payload {@code value} and the message's parts {@code content}, + * while the model names them {@code source} and {@code parts}. Translating here keeps the fixtures + * uniform across runtimes and confines the difference to the harness, as the Rust suite also does. + */ + public static List buildMessages(Map input) { + List messages = new ArrayList<>(); + if (!(input.get("messages") instanceof List raw)) { + return messages; + } + + LoadContext context = new LoadContext(); + for (Object entry : raw) { + if (!(entry instanceof Map message)) { + continue; + } + Map data = new LinkedHashMap<>(); + data.put("role", message.get("role")); + + List parts = new ArrayList<>(); + if (message.get("content") instanceof List content) { + for (Object part : content) { + parts.add(part instanceof Map map ? normalizePart(map) : part); + } + } + data.put("parts", parts); + if (message.get("metadata") != null) { + data.put("metadata", message.get("metadata")); + } + messages.add(Message.load(data, context)); + } + return messages; + } + + /** + * Rebuild the prompt a process vector implies. + * + *

Only {@code has_outputs} matters for these: declaring outputs is what makes a processor + * attempt to decode structured JSON rather than hand back text. + */ + public static Prompty buildProcessAgent( + Map input, String modelId, String provider) { + Map data = new LinkedHashMap<>(); + data.put("name", "test"); + data.put("kind", "prompt"); + data.put("instructions", "test"); + data.put("model", Map.of("id", modelId, "provider", provider)); + if (Boolean.TRUE.equals(input.get("has_outputs"))) { + data.put("outputs", List.of(Map.of("name", "result", "kind", "string"))); + } + return Prompty.load(data, new LoadContext()); + } + + private static Map normalizePart(Map part) { + Map normalized = new LinkedHashMap<>(); + boolean isText = "text".equals(part.get("kind")); + for (Map.Entry entry : part.entrySet()) { + String key = String.valueOf(entry.getKey()); + normalized.put("value".equals(key) && !isText ? "source" : key, entry.getValue()); + } + return normalized; + } +} diff --git a/runtime/java/settings.gradle.kts b/runtime/java/settings.gradle.kts new file mode 100644 index 000000000..6a20e9eeb --- /dev/null +++ b/runtime/java/settings.gradle.kts @@ -0,0 +1,12 @@ +rootProject.name = "prompty-java" + +include("prompty") +include("prompty-openai") +include("prompty-anthropic") +include("prompty-foundry") + +dependencyResolutionManagement { + repositories { + mavenCentral() + } +} diff --git a/schema/package-lock.json b/schema/package-lock.json index 719a4b18b..619224263 100644 --- a/schema/package-lock.json +++ b/schema/package-lock.json @@ -8,7 +8,7 @@ "dependencies": { "@typespec/compiler": "1.10.0", "@typespec/json-schema": "1.10.0", - "@typra/emitter": "0.4.2" + "@typra/emitter": "0.4.3" } }, "node_modules/@babel/code-frame": { @@ -476,9 +476,9 @@ } }, "node_modules/@typra/emitter": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@typra/emitter/-/emitter-0.4.2.tgz", - "integrity": "sha512-6eC3tOWiU00Qlt/LuOiISbDby76fv6lPrp9gB8wQ7q3ivUchIVWZAMzgnizqD4TIkfwQbDLMkf/oe08uG+2YZA==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@typra/emitter/-/emitter-0.4.3.tgz", + "integrity": "sha512-CxOocMoOP4oOiKiTzjvRfEt7aFH1X+e1Z8pnXusGWNcvsj3Ofrwv9gZgeNNoE70+JIkos+7LIarg7sGi9TyuAA==", "license": "MIT", "dependencies": { "xml-formatter": "^3.6.7", diff --git a/schema/package.json b/schema/package.json index a0d593116..887bc75b7 100644 --- a/schema/package.json +++ b/schema/package.json @@ -6,13 +6,14 @@ "format:tsp": "npx tsp format \"model/**/*.tsp\"", "format:tsp:check": "npx tsp format \"model/**/*.tsp\" --check", "format:rust": "cargo fmt --all --manifest-path ../runtime/rust/prompty/Cargo.toml", - "generate": "npx tsp compile model/main.tsp --config tspconfig.yaml && node scripts/normalize-typra-output.mjs", + "generate": "node scripts/clean-java-output.mjs && npx tsp compile model/main.tsp --config tspconfig.yaml && node scripts/normalize-typra-output.mjs", "verify:typra": "node scripts/verify-typra.mjs", + "test:scripts": "node --test \"scripts/*.test.mjs\"", "build": "npm run format:tsp && npm run generate && npm run format:rust" }, "dependencies": { "@typespec/compiler": "1.10.0", "@typespec/json-schema": "1.10.0", - "@typra/emitter": "0.4.2" + "@typra/emitter": "0.4.3" } } diff --git a/schema/scripts/clean-java-output.mjs b/schema/scripts/clean-java-output.mjs new file mode 100644 index 000000000..4f6c84bf7 --- /dev/null +++ b/schema/scripts/clean-java-output.mjs @@ -0,0 +1,115 @@ +// Removes the fully generated Java model and example trees before emission. +// +// The Java normalization shim (`normalize-java-output.mjs`) hoists enums into +// their own compilation units and emits small support classes, so the Java +// output directories contain files that the Typra manifest does not track. If a +// model type or enum is removed upstream, nothing else would delete the stale +// file. Clearing the directories first keeps the generated tree a faithful +// projection of the schema. +// +// Only files carrying the generated marker are removed, so a stray hand-written +// file is reported rather than silently deleted. +// +// Extension seams (`Methods.java`) are the exception. The emitter creates +// them once, only when missing, and never rewrites them, so they are the +// designated home for hand-written `@method` implementations. They deliberately +// carry a seam marker instead of the generated marker; deleting them would +// destroy hand-written code, so they are preserved. They are matched on that +// marker rather than on a file-name pattern, because the emitter's PascalCase +// conversion does not normalise underscores consistently (`foo_bar` -> `FooBar` +// but `foo_1` -> `Foo_1`), which a name pattern would miss. +// +// Several seam markers are accepted. The pinned emitter opens a seam with a +// prose sentence, while later emitter lines front the file with a stable +// machine-readable tag. Because this check is fail-closed, recognising only one +// spelling would abort the build the first time the marker changes -- and the +// failure would be a refusal to clean, not a clear diagnostic. +// +// Pre-accepting a marker the pinned emitter does not yet write does trade away +// some fail-closed strictness, so the exposure is bounded deliberately: +// * an unrecognised marker still aborts, so genuinely unknown files are never +// deleted and never silently kept; +// * generated files open with `MARKER` and are deleted, so the only files this +// list can preserve are ones already declaring themselves a Typra seam; +// * tag markers must match the opening line exactly, so a marker cannot be +// smuggled in as a prefix of unrelated content. +// The residual risk is that a seam-tagged file outlives its `@method`, which is +// the pre-existing orphan case noted below rather than a new one. +// +// Consequence: a seam whose `@method` is later removed from the schema is not +// cleaned up automatically. The Typra manifest does not yet record seam files, +// so orphan detection is left to review. + +import { readdirSync, rmSync, readFileSync, existsSync } from "node:fs"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; + +export const MARKER = "// "; +export const SEAM_MARKER = "// Typra extension seam."; + +/** + * Every accepted opening line for a hand-editable extension seam. + * + * `exact` markers must be the whole opening line. The pinned emitter's prose + * marker is the one exception: its documented form continues with a trailing + * sentence, so it is necessarily matched as a prefix. + */ +export const SEAM_MARKERS = [ + { marker: SEAM_MARKER, exact: false }, + { marker: "// ", exact: true }, + { marker: "// ", exact: true }, +]; + +/** True when `text` opens with an accepted seam marker. */ +export function hasSeamMarker(text) { + const firstLine = text.split("\n", 1)[0].trimEnd(); + return SEAM_MARKERS.some(({ marker, exact }) => + exact ? firstLine === marker : firstLine.startsWith(marker), + ); +} + +export const ROOTS = [ + join("..", "runtime", "java", "prompty", "src", "main", "java", "com", "microsoft", "prompty", "model"), + join("..", "runtime", "java", "prompty", "src", "test", "java", "com", "microsoft", "prompty", "model"), +]; + +/** + * Deletes every generated file under `roots`, preserves extension seams, and + * throws on anything else. Returns the paths that were removed and preserved. + */ +export function cleanJavaOutput(roots = ROOTS) { + const removed = []; + const preserved = []; + + for (const root of roots) { + if (!existsSync(root)) { + continue; + } + for (const name of readdirSync(root)) { + const path = join(root, name); + if (!name.endsWith(".java")) { + throw new Error(`Unexpected non-Java file in generated output directory: ${path}`); + } + const text = readFileSync(path, "utf8"); + if (text.startsWith(MARKER)) { + rmSync(path); + removed.push(path); + continue; + } + if (hasSeamMarker(text)) { + preserved.push(path); + continue; + } + throw new Error( + `Refusing to clean ${path}: it is missing the generated marker. ` + + "Generated output directories must not contain hand-written files.", + ); + } + } + + return { removed, preserved }; +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) { + cleanJavaOutput(); +} diff --git a/schema/scripts/clean-java-output.test.mjs b/schema/scripts/clean-java-output.test.mjs new file mode 100644 index 000000000..18472d882 --- /dev/null +++ b/schema/scripts/clean-java-output.test.mjs @@ -0,0 +1,134 @@ +// Guards the generated-output cleaner. +// +// The cleaner runs before every emission, so a defect here either destroys the +// hand-written `@method` implementations kept in extension seams or silently +// leaves stale generated files behind. Both failure modes are quiet, which is +// why they are asserted rather than left to review. + +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { mkdtempSync, mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { cleanJavaOutput, MARKER, SEAM_MARKER, SEAM_MARKERS } from "./clean-java-output.mjs"; + +function withRoot(files, run) { + const dir = mkdtempSync(join(tmpdir(), "clean-java-")); + const root = join(dir, "model"); + mkdirSync(root); + for (const [name, text] of Object.entries(files)) { + writeFileSync(join(root, name), text, "utf8"); + } + try { + return run(root); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +} + +const generated = `${MARKER}\n// Code generated by Typra emitter; DO NOT EDIT.\npackage p;\n`; +const seam = `${SEAM_MARKER}\npackage p;\nclass MessageMethods { static String text() { return "hand written"; } }\n`; + +test("removes generated files", () => { + withRoot({ "Message.java": generated }, (root) => { + const result = cleanJavaOutput([root]); + assert.equal(result.removed.length, 1); + assert.ok(!existsSync(join(root, "Message.java"))); + }); +}); + +test("preserves extension seams and their hand-written bodies", () => { + withRoot({ "Message.java": generated, "MessageMethods.java": seam }, (root) => { + const result = cleanJavaOutput([root]); + assert.deepEqual(result.preserved, [join(root, "MessageMethods.java")]); + assert.ok(existsSync(join(root, "MessageMethods.java"))); + assert.ok(!existsSync(join(root, "Message.java"))); + }); +}); + +test("preserves a seam whose name defeats a file-name pattern", () => { + // Typra's PascalCase conversion leaves the underscore before a digit + // (`foo_1` -> `Foo_1`), so seams are matched on their marker, not their name. + withRoot({ "Foo_1Methods.java": seam }, (root) => { + const result = cleanJavaOutput([root]); + assert.equal(result.preserved.length, 1); + assert.ok(existsSync(join(root, "Foo_1Methods.java"))); + }); +}); + +test("preserves a seam opened by any accepted marker", () => { + // The pinned emitter opens a seam with prose; later lines front the file with + // a stable machine-readable tag. The cleaner is fail-closed, so an unrecognised + // spelling would abort the build instead of preserving hand-written code. + SEAM_MARKERS.forEach(({ marker }, index) => { + withRoot({ [`Message${index}Methods.java`]: `${marker}\npackage p;\n` }, (root) => { + const result = cleanJavaOutput([root]); + assert.equal(result.preserved.length, 1, `marker not accepted: ${marker}`); + assert.equal(result.removed.length, 0); + }); + }); +}); + +test("a tag marker must be the whole opening line", () => { + // Prefix matching would let a marker be smuggled in ahead of unrelated + // content, so tag markers are compared against the complete opening line. + // The pinned prose marker is the documented exception: its real form carries + // a trailing sentence, so it stays a prefix match. + for (const { marker, exact } of SEAM_MARKERS) { + const smuggled = { "Stray.java": `${marker}-not-a-seam\npackage p;\n` }; + if (exact) { + withRoot(smuggled, (root) => { + assert.throws(() => cleanJavaOutput([root]), /missing the generated marker/u, marker); + }); + } else { + withRoot(smuggled, (root) => { + assert.equal(cleanJavaOutput([root]).preserved.length, 1, marker); + }); + } + } +}); + +test("no seam marker can shadow the generated marker", () => { + // A seam marker that prefixed the generated marker would strand every + // generated file, so the two sets must stay disjoint. + for (const { marker } of SEAM_MARKERS) { + assert.ok(!MARKER.startsWith(marker), `seam marker shadows generated files: ${marker}`); + assert.ok(!marker.startsWith(MARKER), `generated marker shadows a seam: ${marker}`); + } +}); + +test("refuses to clean an unmarked hand-written file", () => { + withRoot({ "Stray.java": "package p;\nclass Stray { }\n" }, (root) => { + assert.throws(() => cleanJavaOutput([root]), /missing the generated marker/u); + assert.ok(existsSync(join(root, "Stray.java"))); + }); +}); + +test("requires the marker at the start of the file", () => { + withRoot({ "Late.java": `package p;\n${MARKER}\n` }, (root) => { + assert.throws(() => cleanJavaOutput([root]), /missing the generated marker/u); + }); +}); + +test("rejects non-Java files in the generated directory", () => { + withRoot({ "notes.txt": "scratch" }, (root) => { + assert.throws(() => cleanJavaOutput([root]), /Unexpected non-Java file/u); + }); +}); + +test("is idempotent across repeated runs", () => { + // Regression guard: the cleaner previously threw on the second regeneration + // because the seam it had preserved was then treated as a stray file. + withRoot({ "Message.java": generated, "MessageMethods.java": seam }, (root) => { + cleanJavaOutput([root]); + const second = cleanJavaOutput([root]); + assert.deepEqual(second.removed, []); + assert.equal(second.preserved.length, 1); + }); +}); + +test("skips roots that do not exist", () => { + const result = cleanJavaOutput([join(tmpdir(), "clean-java-does-not-exist")]); + assert.deepEqual(result, { removed: [], preserved: [] }); +}); diff --git a/schema/scripts/normalize-java-output.mjs b/schema/scripts/normalize-java-output.mjs new file mode 100644 index 000000000..aaf48325e --- /dev/null +++ b/schema/scripts/normalize-java-output.mjs @@ -0,0 +1,932 @@ +// Deterministic post-emit normalization for the Typra Java target. +// +// TEMPORARY SHIM — remove once @typra/emitter's Java backend is fixed. +// +// The Java language backend of @typra/emitter emits source that diverges from +// the C#/Rust/Go/Python backends. This module applies a fixed, deterministic set +// of rewrites so the emitted model remains the single canonical model layer for +// the Java runtime (no hand-written duplicate model code, no manual edits to +// generated files). +// +// Pinned emitter: @typra/emitter@0.4.3. +// +// Residual defects still present in 0.4.3 (reported upstream): +// +// J9 Named collections are not normalized between their dictionary and list +// forms on load, and `collectionFormat` is not honoured on save. Without +// this, `Tool.bindings` saves as an array and the shared vector +// `spec/vectors/agent/agent_vectors.json` fails (`missing field 'unit'`). +// J11 Generated tests double-escape expected string literals, so the +// expectation never matches the value the model actually loads. +// J12 Generated tests dereference object-valued fields as if they were maps +// (`instance1.bindings.input` against a `List`), which does not +// compile. Reported upstream as J20; it is the same defect class. +// J13 The `float` and `integer` scalar shorthand branches are both guarded by +// a bare `data instanceof Number`, so the integer branch is unreachable +// and every numeric shorthand loads as a float. +// J14 A derived `save()` runs the `postSave` hook at every level of the +// inheritance chain, post-processing an incomplete dictionary and then the +// complete one again. C# runs it exactly once, in the base class. +// J15 `SaveContext` lacks the `collectionFormat` and `useShorthand` knobs the +// other backends expose, so callers cannot select the object wire form. +// J16 Optional properties that declare a TypeSpec default are materialized +// eagerly, so `save()` emits keys the C#, Rust and Go runtimes omit. +// J17 Required enum-typed properties are initialized to `null` and saved +// behind a null check, so a required enum can vanish from the wire data. +// C# and Rust seed them with the first declared constant and always emit +// them. +// J21 Generated tests compare enum-typed fields against their raw wire string +// (`"always"`) but stringify the Java constant name (`ALWAYS`), so every +// enum-valued assertion fails. +// +// Fixed upstream in 0.4.3 and removed from this shim: J1 (`default` reserved +// word — now emitted as `defaultValue`), J2 (abstract-base instantiation and +// the missing `*` wildcard subtype — `CustomTool` dispatch is now emitted), +// J3 (raw `String` assigned to enum fields — factories now use `fromValue`), +// J4 (`int` literals for boxed `Long`/`Double`/`Float`), J6/J7 (lowerCamelCase +// and package-private enums — now PascalCase standalone public files), +// J8 (derived `load()` not populating base properties — `loadBaseInto` is now +// emitted natively), J10 (tests dotting into `List<>` fields), +// J18 (deep polymorphic downcasts) and +// J19 (`.value` appended to discriminator fields). The generated-test runner is +// likewise now emitted upstream as `TypraGeneratedTests`, so this shim no longer +// synthesizes a registry; `GeneratedExamplesTest` drives that output directly. +// +// Every rewrite below is structural and idempotent: running the emitter and +// this shim again from a clean tree produces byte-identical output. +// +// Retirement. This shim is retired as a unit, not by deleting this file alone: +// pin a published @typra/emitter release, drop the `normalizeJavaOutput` and +// `normalizeJavaTests` calls from `normalize-typra-output.mjs`, remove this +// file and the references to it in `clean-java-output.mjs`, +// `runtime/java/README.md`, `ModelNormalizationTest`, and the +// `prompty-java-check` workflow path filter, regenerate from TypeSpec, and pass +// the complete Java suite with no generated-file drift. The pin must move to a +// published release rather than a branch build so the version resolves +// reproducibly for every consumer. +// +// Two emitter defects are known to block that evaluation. Both were confirmed +// against emitter commit 7595113, which is otherwise the closest candidate seen +// — it compiles and clears J21 along with the generated-test escaping and +// dotted-identifier defects. +// +// * J13, above. Numeric shorthand must resolve integral and floating values +// to distinct kinds. The families must be mutually exclusive and jointly +// cover the boxed numeric types SnakeYAML actually produces — it yields +// `Long` for large integers and `Double` for `3.14` — so narrowing one +// branch to `Integer`/`Float` alone trades one failing case for another. +// * J16, above, in its collection half. The optional-property rule reached +// the scalars but not the collections: `description`, `required` and +// `nullable` now default to `null`, while `enumValues` still defaults to an +// empty list and is therefore emitted by an otherwise correct `!= null` +// save guard. Only the field default is wrong. Suppressing empty lists on +// save instead would be incorrect, because it would stop an explicitly +// supplied `[]` from round-tripping. `Prompty.tools` is +// required-with-default and must keep materializing to `[]` and emitting +// unconditionally; the distinction is optional versus required-with-default, +// not scalar versus collection. +// +// J5 — `@method` stubs — is deliberately NOT addressed here. The emitter now +// emits a `${TypeName}Methods` extension seam, created only when missing and +// never overwritten, which is where the hand-written implementations live. + +import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; + +export function normalizeJavaOutput(root, specRoot) { + if (!existsSync(root)) { + return; + } + + const files = readdirSync(root).filter((name) => name.endsWith(".java")); + const units = new Map(); + for (const name of files) { + const path = join(root, name); + units.set(name.replace(/\.java$/u, ""), { path, text: readFileSync(path, "utf8") }); + } + + const optional = readOptionalProperties(specRoot); + const namedCollections = readNamedCollectionFields(specRoot); + const enumDefaults = collectEnumDefaults(units); + + const classes = indexClasses(units); + const described = describeClasses(classes); + const stats = { + namedDictionary: 0, + namedDictionarySave: 0, + derivedSave: 0, + scalarShorthand: 0, + optionalDefaults: 0, + requiredEnums: 0, + problems: [], + }; + + for (const [name, unit] of units) { + let text = unit.text; + text = clearOptionalDefaults(text, optional.get(name), stats); + text = fixRequiredEnumDefaults(text, optional.get(name), enumDefaults, stats); + text = fixScalarShorthandDispatch(text, stats); + const named = new Map(); + text = fixNamedDictionaryLoads(text, units, stats, named); + text = fixNamedDictionarySaves(text, named, namedCollections.get(name), name, stats); + text = fixDerivedSaveMethods(text, classes.get(name), name, stats); + unit.text = text; + } + + extendSaveContext(units, stats); + writeSupportClasses(root, units); + + assertRewritesApplied(stats, classes); + + for (const unit of units.values()) { + writeFileSync(unit.path, unit.text); + } + + return { classes: described }; +} + +/** + * Reads the TypeSpec sources and reports, per model, which list-valued + * properties may be saved in the name-keyed object form. + * + *

A collection is eligible when its element type carries a `name` property in + * the TypeSpec — either because the element is a named-collection alias + * (`alias X = Record | Named[]`, whose `Named<>` wrapper always + * declares one) or because the element model declares `name` itself. + * + *

The Java class shape is deliberately *not* consulted. `UnionProperty.anyOf` + * is declared `Property[]`, and the emitters graft a `name` field onto the + * generated `Property` class even though the TypeSpec model does not declare + * one — so testing the emitted class would wrongly make `anyOf` eligible. The + * generated C# agrees with the rule implemented here: it emits exactly eleven + * "Object format: use name as key" save sites, matching the eleven fields this + * resolves, and stamps `anyOf`/`oneOf` with "This collection type does not have + * a 'name' property, only array format is supported". + */ +function readNamedCollectionFields(specRoot) { + const fields = new Map(); + if (!specRoot || !existsSync(specRoot)) { + return fields; + } + const sources = collectFiles(specRoot, ".tsp").map((file) => readFileSync(file, "utf8")); + + const aliases = new Set(); + for (const source of sources) { + for (const alias of source.matchAll(/^alias\s+(\w+)\s*=\s*Record<[^>]*>\s*\|\s*Named]*>)?\s+extends\s+(\w+)/u.exec(whole); + if (base) { + bases.set(model, base[1]); + } + } + } + const declaresName = (model) => { + for (let current = model, hops = 0; current && hops < 16; current = bases.get(current), hops += 1) { + if (declared.has(current)) { + return true; + } + } + return false; + }; + + for (const source of sources) { + for (const block of source.matchAll(MODEL_BLOCK_RE)) { + const [, model, body] = block; + const names = fields.get(model) ?? new Set(); + for (const property of body.matchAll(/^\s+(\w+)\??\s*:\s*(\w+)(\[\])?\s*[=;]/gmu)) { + const [, field, type, isArray] = property; + if (aliases.has(type) || (isArray && declaresName(type))) { + names.add(field); + } + } + fields.set(model, names); + } + } + return fields; +} + +/** + * Reads the TypeSpec sources and reports, per model, which properties are + * declared optional (`name?: type`). + * + *

TypeSpec lets an optional property carry a default (`description?: string = + * ""`). The C#, Rust and Go backends treat that default as a fallback for + * readers and leave the field unset when the wire data omits it; the Java + * backend materializes it, so every saved dictionary carries keys the other + * runtimes omit. J16 restores the shared behaviour. + */ +function readOptionalProperties(specRoot) { + const optional = new Map(); + if (!specRoot || !existsSync(specRoot)) { + return optional; + } + for (const file of collectFiles(specRoot, ".tsp")) { + const source = readFileSync(file, "utf8"); + for (const block of source.matchAll(MODEL_BLOCK_RE)) { + const [, model, body] = block; + const names = optional.get(model) ?? new Set(); + for (const property of body.matchAll(/^\s+(\w+)\?\s*:/gmu)) { + names.add(property[1]); + } + optional.set(model, names); + } + } + return optional; +} + +const MODEL_BLOCK_RE = /^model\s+(\w+)[^{]*\{([\s\S]*?)^\}/gmu; + +function collectFiles(root, extension) { + const found = []; + for (const entry of readdirSync(root, { withFileTypes: true })) { + const path = join(root, entry.name); + if (entry.isDirectory()) { + found.push(...collectFiles(path, extension)); + } else if (entry.name.endsWith(extension)) { + found.push(path); + } + } + return found; +} + +/** J16 — an unset optional property must stay unset until a reader supplies it. */ +function clearOptionalDefaults(text, optionalNames, stats) { + if (!optionalNames || optionalNames.size === 0) { + return text; + } + return text.replace( + /^( {2}public (?!static)([\w.]+)((?:<[^>]*>)?(?:\[\])?) )(\w+) = (?!null;)(.+);$/gmu, + (whole, prefix, type, suffix, field) => { + // A primitive cannot hold null, and `default` is renamed by J1. + if (suffix === "" && PRIMITIVES.has(type)) { + return whole; + } + const declared = field === "defaultValue" ? "default" : field; + if (!optionalNames.has(declared) && !optionalNames.has(field)) { + return whole; + } + stats.optionalDefaults += 1; + return `${prefix}${field} = null;`; + }, + ); +} + +const PRIMITIVES = new Set(["boolean", "byte", "char", "short", "int", "long", "float", "double"]); + +/** Field name -> declared type per class, including inherited fields. */ +function describeClasses(classes) { + const described = new Map(); + for (const [name, info] of classes) { + const fields = new Map(); + for (let cursor = info; cursor; cursor = cursor.base ? classes.get(cursor.base) : null) { + for (const [field, { type }] of cursor.fields) { + if (!fields.has(field)) { + fields.set(field, type); + } + } + } + described.set(name, fields); + } + return described; +} + + +/** + * Maps each generated enum to its first declared constant, which is the value + * the other backends use as the default for a required enum-typed property. + */ +function collectEnumDefaults(units) { + const defaults = new Map(); + for (const [name, unit] of units) { + const first = /^public enum \w+ \{\n\s*(\w+)\("/mu.exec(unit.text); + if (first) { + defaults.set(name, first[1]); + } + } + return defaults; +} + +// --------------------------------------------------------------------------- +// J17 — required enum-typed properties default to null +// --------------------------------------------------------------------------- + +/** + * The emitter initializes every enum-typed field to {@code null} and guards its + * save with a null check, so a required enum silently disappears from the wire + * dictionary when it was never assigned. C# and Rust instead seed a required + * enum with the first declared constant and always emit it — see + * {@code EngineEvent.Kind = EngineEventKind.TurnStarted} in + * {@code runtime/csharp/Prompty.Core/Model/pipeline/EngineEvent.cs:88,245}. + * + *

Optional enums keep the null initializer and the conditional save, which is + * exactly what the other backends do for a nullable enum. + */ +function fixRequiredEnumDefaults(text, optional, enumDefaults, stats) { + const seeded = new Set(); + const withDefaults = text.replace(/^ {2}public (\w+) (\w+) = null;$/gmu, (whole, type, field) => { + const constant = enumDefaults.get(type); + if (!constant || optional?.has(field) !== false) { + return whole; + } + stats.requiredEnums += 1; + seeded.add(field); + return ` public ${type} ${field} = ${type}.${constant};`; + }); + + if (seeded.size === 0) { + return withDefaults; + } + return withDefaults.replace( + /^( *)if \(obj\.(\w+) != null\) (result\.put\("\w+", obj\.\2\.value\);)$/gmu, + (whole, indent, field, statement) => (seeded.has(field) ? `${indent}${statement}` : whole), + ); +} + + +// --------------------------------------------------------------------------- +// J9 — named-dictionary properties are not normalized into lists +// --------------------------------------------------------------------------- + +const NAMED_DICT_RE = + / {4}if \(map\.containsKey\("(\w+)"\) && map\.get\("\1"\) != null\) \{\n {6}result\.(\w+) = new ArrayList<>\(\);\n {6}if \(map\.get\("\1"\) instanceof Iterable<\?> values\) \{\n {8}for \(Object item : values\) \{\n {10}result\.\2\.add\((\w+)\.load\(item, ctx\)\);\n {8}\}\n {6}\}\n {4}\}/gu; + +/** + * Model properties declared as named dictionaries (`inputs: { firstName: ... }`) + * must load as lists with the dictionary key injected as `name`, and scalar + * values must be widened through the element type's shorthand property. The + * Java backend only handles the already-list form, so a name-keyed dictionary + * silently loads as an empty list. + */ +function fixNamedDictionaryLoads(text, units, stats, named) { + return text.replace(NAMED_DICT_RE, (_whole, wireName, field, elementType) => { + stats.namedDictionary += 1; + const shorthand = declaresShorthand(units, elementType) ? `${elementType}.SHORTHAND_PROPERTY` : "null"; + named.set(field, { wireName, elementType, shorthand }); + return ( + ` if (map.containsKey("${wireName}") && map.get("${wireName}") != null) {\n` + + ` result.${field} = ModelCollections.loadList(\n` + + ` map.get("${wireName}"), "${wireName}", ${shorthand}, ${elementType}::load, ctx);\n` + + " }" + ); + }); +} + +/** + * The save side of J9. The emitter always writes these collections as arrays; + * the reference runtimes honour {@code SaveContext.collectionFormat} and default + * to the name-keyed object form — but only for element types that actually have + * a {@code name} property, exactly as Prompty.Core does. + */ +function fixNamedDictionarySaves(text, named, objectFormatFields, className, stats) { + if (!objectFormatFields || objectFormatFields.size === 0) { + return text; + } + const rewritten = new Set(); + const result = text.replace( + / {4}if \(obj\.(\w+) != null\) \{\n {6}List items = new ArrayList<>\(\);\n {6}for \(\w+ item : obj\.\1\) items\.add\(item\.save\(ctx\)\);\n {6}result\.put\("(\w+)", items\);\n {4}\}/gu, + (whole, field, wireName) => { + if (!objectFormatFields.has(field) || wireName !== field) { + return whole; + } + const entry = named.get(field); + stats.namedDictionarySave += 1; + rewritten.add(field); + return ( + ` if (obj.${field} != null) {\n` + + ` result.put("${wireName}", ModelCollections.saveList(\n` + + ` obj.${field}, ${entry ? entry.shorthand : "null"}, item -> item.save(ctx), ctx));\n` + + " }" + ); + }, + ); + + // The declared type is the authority here, so a field the TypeSpec marks as a + // named collection but whose save site did not match means the emitter's save + // shape drifted. + for (const field of objectFormatFields) { + if (!rewritten.has(field) && text.includes(`obj.${field}`)) { + stats.problems.push(`${className}.${field} is a named collection but its save site was not rewritten`); + } + } + return result; +} + +/** + * Only the root of an inheritance chain may run the {@code postSave} hook. The + * emitter calls it at every level, so a derived save post-processes an + * incomplete dictionary and then post-processes the complete one again. C# calls + * it exactly once, in the base class. + */ +function fixDerivedSaveMethods(text, info, name, stats) { + if (!info?.base || !text.includes("Map result = super.save(ctx);")) { + return text; + } + const updated = text.replace( + /(Map result = super\.save\(ctx\);[\s\S]*?\n {4}return )ctx\.processDict\(result\);/u, + "$1result;", + ); + if (updated === text) { + stats.problems.push(`${name}.save() still post-processes an inherited dictionary`); + return text; + } + stats.derivedSave += 1; + return updated; +} + +function declaresShorthand(units, className) { + const unit = units.get(className); + return Boolean(unit && unit.text.includes("public static final String SHORTHAND_PROPERTY")); +} + +// --------------------------------------------------------------------------- +// J13 — scalar shorthand dispatch has an unreachable integer branch +// --------------------------------------------------------------------------- + +/** + * The emitter guards both the `float` and the `integer` shorthand branch with a + * bare `data instanceof Number`, so every numeric shorthand loads as a float and + * the integer branch is dead. Narrow the float branch to floating-point types so + * integral values reach the integer branch, matching the Rust and C# runtimes. + */ +function fixScalarShorthandDispatch(text, stats) { + return text.replace( + /( {4})if \(data instanceof Number\) \{\n( {6}\w+ result = new \w+\(\);\n {6}result\.kind = "float";)/gu, + (whole, indent, tail) => { + stats.scalarShorthand += 1; + return `${indent}if (data instanceof Double || data instanceof Float || data instanceof java.math.BigDecimal) {\n${tail}`; + }, + ); +} + +// --------------------------------------------------------------------------- +// Support classes emitted alongside the model +// --------------------------------------------------------------------------- + +const MARKER = "// \n// Code generated by Typra emitter; DO NOT EDIT.\n"; + +/** + * The reference runtimes let callers choose how named collections serialize. + * The Java backend omits both knobs, so add them with the same names and + * defaults as {@code Prompty.Core}'s {@code SaveContext}. + */ +function extendSaveContext(units, stats) { + const unit = units.get("SaveContext"); + if (!unit || unit.text.includes("collectionFormat")) { + return; + } + // Anchor above any annotations on processObject so they stay attached to it. + const updated = unit.text.replace( + /(\n(?: {2}@\w+(?:\([^)]*\))?\n)* {2}public T processObject)/u, + ` + /** Output format for collections: "object" (name as key) or "array" (list of dicts). */ + public String collectionFormat = "object"; + + /** Use the shorthand scalar representation when possible. */ + public boolean useShorthand = true; +$1`, + ); + if (updated === unit.text) { + stats.problems.push("SaveContext no longer declares processObject; collection format not added"); + return; + } + unit.text = updated; +} + +function writeSupportClasses(root, units) { + const source = + MARKER + + `package com.microsoft.prompty.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Function; + +/** Shared collection loading and saving helpers used by the generated model. */ +final class ModelCollections { + private ModelCollections() { } + + /** + * Loads a model list from either a flat list or a name-keyed dictionary. + * + *

Dictionary keys are injected as the element's {@code name}; scalar values + * are widened through the element type's shorthand property. + */ + static List loadList( + Object raw, String property, String shorthand, BiFunction loader, LoadContext ctx) { + List result = new ArrayList<>(); + if (raw instanceof Map dict) { + for (Map.Entry entry : dict.entrySet()) { + String key = String.valueOf(entry.getKey()); + Object value = entry.getValue(); + if (isSequence(value)) { + // Rust silently skips an array-valued entry here, which turns a malformed document + // into an empty list and hides the mistake until the tool is called and its arguments + // are missing. C# rejects it; this follows C#, because a schema that was written wrong + // is worth surfacing at load time. + throw new IllegalArgumentException( + "Invalid '" + property + "' format: key '" + key + "' has an array value. '" + property + + "' must be a flat list of objects or a name-keyed dict - not a nested {" + key + + ": [...]} structure."); + } + Map item = new LinkedHashMap<>(); + if (value instanceof Map nested && !nested.isEmpty()) { + for (Map.Entry field : nested.entrySet()) { + item.put(String.valueOf(field.getKey()), field.getValue()); + } + item.put("name", key); + } else { + item.put("name", key); + if (shorthand != null && value != null) { + item.put(shorthand, value); + } + } + result.add(loader.apply(item, ctx)); + } + } else if (raw instanceof Iterable values) { + for (Object item : values) { + Object widened = widen(item, shorthand); + if (widened != null) { + result.add(loader.apply(widened, ctx)); + } + } + } else if (raw instanceof Object[] values) { + for (Object item : values) { + Object widened = widen(item, shorthand); + if (widened != null) { + result.add(loader.apply(widened, ctx)); + } + } + } + return result; + } + + /** + * Saves a model list as either a name-keyed dictionary (the default) or a flat + * array, honouring {@link SaveContext#collectionFormat} and + * {@link SaveContext#useShorthand}. + * + *

The object form is only usable when every item carries a name, so a list + * with an unnamed item falls back to the array form rather than collapsing + * entries onto a shared key. + * + *

This is a deliberate, documented divergence: the C# runtime throws on an + * unnamed item and the Rust runtime silently drops it. Both lose data for a + * document that the load side accepts. Falling back to the array form is + * lossless and reloads identically, and every well-formed document — where + * each entry has a name — serializes the same way in all three runtimes. + */ + static Object saveList(List items, String shorthand, Function> saver, SaveContext ctx) { + List> saved = new ArrayList<>(); + for (T item : items) { + saved.add(saver.apply(item)); + } + if (!"object".equals(ctx.collectionFormat) || !allNamed(saved)) { + return new ArrayList(saved); + } + Map result = new LinkedHashMap<>(); + for (Map item : saved) { + String key = String.valueOf(item.remove("name")); + if (ctx.useShorthand && shorthand != null && item.size() == 1 && item.containsKey(shorthand)) { + result.put(key, item.get(shorthand)); + } else { + result.put(key, item); + } + } + return result; + } + + private static boolean allNamed(List> items) { + for (Map item : items) { + if (!(item.get("name") instanceof String name) || name.isEmpty()) { + return false; + } + } + return true; + } + + private static boolean isSequence(Object value) { + return value instanceof Iterable || value instanceof Object[]; + } + + /** + * Widens a list element into a loadable map. Scalars go through the shorthand + * property; empty and absent values are dropped, matching Prompty.Core's + * {@code GetDictionary} + {@code Count > 0} guard. + */ + private static Object widen(Object item, String shorthand) { + if (item instanceof Map map) { + return map.isEmpty() ? null : map; + } + if (item == null || shorthand == null) { + return null; + } + Map wrapped = new LinkedHashMap<>(); + wrapped.put(shorthand, item); + return wrapped; + } +} +`; + units.set("ModelCollections", { path: join(root, "ModelCollections.java"), text: source }); +} + +// --------------------------------------------------------------------------- +// Guards — fail loudly when a rewrite stops matching emitter output +// --------------------------------------------------------------------------- + +/** + * Every rewrite here targets a specific emitter code shape. If the emitter + * changes, a silent no-op would produce Java that still compiles but no longer + * matches the reference runtimes. Assert the expected shapes were found. + */ +function assertRewritesApplied(stats, classes) { + const problems = []; + problems.push(...stats.problems); + for (const [key, minimum] of Object.entries(EXPECTED_MINIMUMS)) { + if (stats[key] < minimum) { + problems.push(`rewrite '${key}' matched ${stats[key]} sites, expected at least ${minimum}`); + } + } + if (problems.length > 0) { + throw new Error( + "Java normalization no longer matches emitter output:\n - " + + problems.join("\n - ") + + "\nThe @typra/emitter Java backend changed; review schema/scripts/normalize-java-output.mjs.", + ); + } +} + +// Floors that guard against a pass silently matching nothing. The exact counts +// for the save-side rewrites are self-checked against the model shape above, so +// these only need to be low enough to survive ordinary schema additions. +const EXPECTED_MINIMUMS = { + namedDictionary: 40, + // Eleven fields carry a named element type: Prompty.inputs/.outputs/.tools, + // ObjectProperty.properties, Tool.bindings, FunctionTool.parameters, + // EngineCheckpoint.pendingToolRequests/.completedToolResults, + // ModelInvocationResponse.toolRequests, TurnEngineResult.toolResults and + // AnthropicMessagesRequest.tools — matching the eleven "Object format: use + // name as key" save sites in the generated C#. Each is also checked + // individually against the TypeSpec, so this floor only catches a field + // disappearing entirely. + namedDictionarySave: 11, + derivedSave: 10, + scalarShorthand: 1, + optionalDefaults: 12, + // Eleven required enum-typed properties: EngineEvent.kind, + // InvocationContextDecision.disposition, McpApprovalMode.kind, + // ModelToolResult.outcome, RedactedField.mode, ReplayJournalRecord.kind, + // ReplayVerificationResult.status, RunTurnResult.status, SessionEvent.type, + // TurnCommit.status and TurnEvent.type. + requiredEnums: 11, +}; + +function indexClasses(units) { + const classes = new Map(); + for (const [name, unit] of units) { + const decl = /^public (abstract )?class (\w+)(?: extends (\w+))? \{$/mu.exec(unit.text); + if (!decl) { + continue; + } + const enumFields = new Map(); + const fields = new Map(); + for (const match of unit.text.matchAll(/^ {2}public ([\w<>,\[\]. ]+?) (\w+) = (.+);$/gmu)) { + const [, type, field, initializer] = match; + fields.set(field, { type, initializer }); + if (/^[A-Z]\w*$/u.test(type)) { + enumFields.set(field, type); + } + } + classes.set(name, { + name, + isAbstract: Boolean(decl[1]), + base: decl[3] ?? null, + enumFields, + fields, + }); + } + return classes; +} + + +// --------------------------------------------------------------------------- +// Generated tests +// --------------------------------------------------------------------------- + +/** + * The Java backend emits example-driven test classes that expose a + * package-private `run()` entry point rather than JUnit methods. Apply the same + * structural renames used on the model, drop classes whose `run()` body is + * empty, and emit a deterministic registry so a hand-written JUnit test can + * execute every generated example as its own test case. + */ +export function normalizeJavaTests(root, model) { + if (!existsSync(root)) { + return; + } + const classes = model?.classes ?? new Map(); + const stats = { doubleEscaped: 0, enumAssertions: 0 }; + + for (const name of readdirSync(root)) { + if (!name.endsWith("GeneratedTest.java")) { + continue; + } + const path = join(root, name); + let text = readFileSync(path, "utf8"); + + text = relaxEnumAssertions(text, stats); + text = fixDoubleEscapedExpectations(text, stats); + text = dropStructuredAssertions(text, classes, name.replace(/GeneratedTest\.java$/u, "")); + + writeFileSync(path, text); + } + + // If the emitter ever stops double-escaping, this pass would start decoding + // legitimate literals. Fail loudly rather than silently corrupting them. + if (stats.doubleEscaped < MINIMUM_DOUBLE_ESCAPED) { + throw new Error( + `Java test normalization no longer matches emitter output:\n` + + ` - collapsed ${stats.doubleEscaped} double-escaped expectations, expected at least ${MINIMUM_DOUBLE_ESCAPED}\n` + + ` The @typra/emitter Java backend changed; review schema/scripts/normalize-java-output.mjs.`, + ); + } + if (stats.enumAssertions < MINIMUM_ENUM_ASSERTIONS) { + throw new Error( + `Java test normalization no longer matches emitter output:\n` + + ` - relaxed ${stats.enumAssertions} enum assertion helpers, expected at least ${MINIMUM_ENUM_ASSERTIONS}\n` + + ` The @typra/emitter Java backend changed; review schema/scripts/normalize-java-output.mjs.`, + ); + } +} + +const MINIMUM_DOUBLE_ESCAPED = 5; + +// One helper per generated example class that declares the assertEquals shim. +const MINIMUM_ENUM_ASSERTIONS = 100; + +/** + * Expected values passed to `assertEquals` are escaped twice: once for the wire + * representation and once for the Java literal. Collapse the extra level so the + * expectation matches the value the model actually loads. + * + *

Only literals that still contain a valid Java escape sequence *after* one + * decode carry the double-escape signature, so an ordinary literal is left + * untouched. + * + *

LIMITATION: a literal whose intended value genuinely contains a backslash + * followed by an escape character (`"\\n"` meaning the two characters `\` and + * `n`) is indistinguishable from a double-escaped newline. No such value exists + * in the current examples, and the caller asserts the collapse count stays + * above a floor, so if the emitter ever stops double-escaping this pass fails + * loudly instead of silently corrupting expectations. + */ +function fixDoubleEscapedExpectations(text, stats) { + return text.replace(/(\bassertEquals\()("(?:[^"\\\n]|\\.)*")/gu, (whole, prefix, literal) => { + const once = javaUnescape(literal.slice(1, -1)); + if (!/\\(["'\\ntrbfs0]|u[0-9a-fA-F]{4})/u.test(once)) { + return whole; + } + if (stats) { + stats.doubleEscaped += 1; + } + return `${prefix}"${javaEscape(javaUnescape(once))}"`; + }); +} + + +function javaUnescape(value) { + return value.replace(/\\(u[0-9a-fA-F]{4}|.)/gu, (whole, escape) => { + switch (escape[0]) { + case "n": + return "\n"; + case "t": + return "\t"; + case "r": + return "\r"; + case "b": + return "\b"; + case "f": + return "\f"; + case "s": + return " "; + case "0": + return "\0"; + case "\\": + case '"': + case "'": + return escape; + case "u": + return String.fromCharCode(Number.parseInt(escape.slice(1), 16)); + default: + return whole; + } + }); +} + +function javaEscape(value) { + return value + .replace(/\\/gu, "\\\\") + .replace(/"/gu, '\\"') + .replace(/\n/gu, "\\n") + .replace(/\r/gu, "\\r") + .replace(/\t/gu, "\\t"); +} + +/** + * Named-dictionary properties are modelled as `List` in Java and nested + * models as object references, but the emitter still generates assertions that + * compare them against scalar wire values. Resolve each accessor chain against + * the model and drop only the assertions whose target is not a scalar. + * + * Unlike the other rewrites this carries no count floor, because the Java + * compiler is the guard: leaving a structured assertion in place fails to + * compile, and dropping a scalar one would fail the assertion it replaced. + */ +function dropStructuredAssertions(text, classes, rootClass) { + const fields = classes.get(rootClass); + if (!fields) { + return text; + } + return text + .split("\n") + .filter((line) => { + if (!line.trimStart().startsWith("assert")) { + return true; + } + const accessor = /\bassert\w*\([^,]*,\s*\w+\d*((?:\.\w+)+),/u.exec(line); + if (!accessor) { + return true; + } + return isScalarPath(classes, rootClass, accessor[1].slice(1).split(".")); + }) + .join("\n"); +} + +function isScalarPath(classes, className, path) { + let fields = classes.get(className); + for (let index = 0; index < path.length; index += 1) { + if (!fields) { + // Unknown owner: keep the assertion rather than silently dropping coverage. + return true; + } + const type = fields.get(path[index]); + if (type === undefined) { + return true; + } + const last = index === path.length - 1; + if (type.startsWith("List<") || type.startsWith("Map<")) { + // Collections are never addressable with a dotted accessor, and comparing + // one against a scalar wire value is always wrong. + return false; + } + if (classes.has(type)) { + if (last) { + return false; + } + fields = classes.get(type); + continue; + } + return last; + } + return true; +} + +/** + * Generated examples compare enum-typed fields against their raw wire strings. + * Teach the emitted `assertEquals` helper to unwrap enum constants first. + */ +function relaxEnumAssertions(text, stats) { + const anchor = /( {2}private static void assertEquals\(Object expected, Object actual, String message\) \{\n)/u; + if (!anchor.test(text)) { + return text; + } + stats.enumAssertions += 1; + return text.replace( + anchor, + "$1 expected = unwrapEnum(expected);\n actual = unwrapEnum(actual);\n", + ).replace( + anchor, + " private static Object unwrapEnum(Object value) {\n" + + " if (value == null || !value.getClass().isEnum()) return value;\n" + + " try {\n" + + " return value.getClass().getField(\"value\").get(value);\n" + + " } catch (ReflectiveOperationException ignored) {\n" + + " return value;\n" + + " }\n" + + " }\n\n$1", + ); +} diff --git a/schema/scripts/normalize-typra-output.mjs b/schema/scripts/normalize-typra-output.mjs index 98544872d..8da653eb5 100644 --- a/schema/scripts/normalize-typra-output.mjs +++ b/schema/scripts/normalize-typra-output.mjs @@ -1,6 +1,8 @@ import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs"; import { join } from "node:path"; +import { normalizeJavaOutput, normalizeJavaTests } from "./normalize-java-output.mjs"; + const metadataRoot = join("tsp-output", ".typra-generated"); const manifestPath = join(metadataRoot, "manifest.json"); @@ -13,6 +15,35 @@ if (existsSync(manifestPath)) { trimEmptyPythonGeneratedTests(join("..", "runtime", "python", "prompty", "tests", "model")); trimTrailingWhitespace(join("..", "runtime", "go", "prompty", "model")); +const javaModelRoot = join( + "..", + "runtime", + "java", + "prompty", + "src", + "main", + "java", + "com", + "microsoft", + "prompty", + "model", +); +const javaTestRoot = join( + "..", + "runtime", + "java", + "prompty", + "src", + "test", + "java", + "com", + "microsoft", + "prompty", + "model", +); +const typeSpecRoot = "model"; +normalizeJavaTests(javaTestRoot, normalizeJavaOutput(javaModelRoot, typeSpecRoot)); + function trimEmptyPythonGeneratedTests(root) { if (!existsSync(root)) { return; diff --git a/schema/tsp-output/.typra-generated/export-surfaces.json b/schema/tsp-output/.typra-generated/export-surfaces.json index 45c400eb4..ea1a99d5a 100644 --- a/schema/tsp-output/.typra-generated/export-surfaces.json +++ b/schema/tsp-output/.typra-generated/export-surfaces.json @@ -17,8 +17,8 @@ }, { "name": "@typra/emitter", - "version": "0.4.2", - "supportedRange": "0.4.2", + "version": "0.4.3", + "supportedRange": "0.4.3", "supported": true } ] @@ -4127,6 +4127,2055 @@ "validation_result.go" ] }, + { + "target": "java", + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "packageName": "com.microsoft.prompty.model", + "rootExports": [ + "AiResourceInfo", + "AnonymousConnection", + "AnthropicImageBlock", + "AnthropicImageSource", + "AnthropicMessagesRequest", + "AnthropicMessagesResponse", + "AnthropicTextBlock", + "AnthropicToolDefinition", + "AnthropicToolResultBlock", + "AnthropicToolUseBlock", + "AnthropicUsage", + "AnthropicWireMessage", + "ApiKeyConnection", + "ArrayProperty", + "AudioPart", + "AuthorizationCodeFlow", + "Binding", + "Checkpoint", + "CheckpointStore", + "CompactionCompletePayload", + "CompactionConfig", + "CompactionFailedPayload", + "CompactionStartPayload", + "Connection", + "ContentPart", + "ContextCandidate", + "ContextRequest", + "CustomTool", + "DelegatedStateReference", + "DeviceAuthorization", + "DoneEventPayload", + "EngineCheckpoint", + "EngineEvent", + "EnginePermissionDecision", + "ErrorChunk", + "ErrorEventPayload", + "EventJournalWriter", + "EventSink", + "Executor", + "FileNotFoundError", + "FilePart", + "FinalOutputPolicyRequest", + "FinalOutputPolicyResult", + "FormatConfig", + "FoundryConnection", + "FunctionTool", + "GuardrailResult", + "HarnessContext", + "HookEndPayload", + "HookStartPayload", + "HostPolicyRequest", + "HostPolicyResult", + "HostToolExecutor", + "HostToolRequest", + "HostToolResult", + "ImagePart", + "InvocationContextDecision", + "InvocationContextState", + "InvocationUsage", + "InvokerError", + "LlmCompletePayload", + "LlmStartPayload", + "McpApprovalMode", + "McpTool", + "MemoryEntry", + "MemoryStore", + "Message", + "MessagesUpdatedPayload", + "Model", + "ModelInfo", + "ModelInvocationContextSnapshot", + "ModelInvocationRequest", + "ModelInvocationResponse", + "ModelLister", + "ModelOptions", + "ModelReconciliationState", + "ModelToolRequest", + "ModelToolResult", + "OAuthConnection", + "OAuthToken", + "ObjectProperty", + "OpenApiTool", + "Parser", + "ParserConfig", + "PermissionCompletedPayload", + "PermissionDecision", + "PermissionRequest", + "PermissionRequestedPayload", + "PermissionResolver", + "Processor", + "ProjectInfo", + "Prompty", + "PromptyTool", + "Property", + "RedactedField", + "RedactionMetadata", + "ReferenceConnection", + "RemoteConnection", + "Renderer", + "ReplayJournalRecord", + "ReplayMismatch", + "ReplayVerificationRequest", + "ReplayVerificationResult", + "ResumeContext", + "RetryPayload", + "RetryPolicyRequest", + "RunTurnRequest", + "RunTurnResult", + "SessionEndPayload", + "SessionEvent", + "SessionFileRef", + "SessionRef", + "SessionStartPayload", + "SessionSummary", + "SessionTrace", + "SessionWarningPayload", + "StatusEventPayload", + "StreamChunk", + "StreamOptions", + "SubscriptionInfo", + "Template", + "TextChunk", + "TextPart", + "ThinkingChunk", + "ThinkingEventPayload", + "ThreadMarker", + "TokenEventPayload", + "TokenUsage", + "Tool", + "ToolCall", + "ToolCallCompletePayload", + "ToolCallStartPayload", + "ToolChunk", + "ToolContext", + "ToolDispatchResult", + "ToolExecutionCompletePayload", + "ToolExecutionStartPayload", + "ToolResult", + "ToolResultPayload", + "TraceFile", + "TraceSpan", + "TraceTime", + "TrajectoryEvent", + "TurnCommit", + "TurnEndPayload", + "TurnEngineResult", + "TurnEvent", + "TurnModelRequest", + "TurnModelResponse", + "TurnOptions", + "TurnStartPayload", + "TurnSummary", + "TurnTrace", + "UnionProperty", + "UsageChunk", + "ValidationError", + "ValidationResult" + ], + "exports": [ + { + "name": "GuardrailResult", + "kind": "value", + "group": "agent", + "source": "GuardrailResult.java", + "protocol": false + }, + { + "name": "Prompty", + "kind": "value", + "group": "agent", + "source": "Prompty.java", + "protocol": false + }, + { + "name": "AnonymousConnection", + "kind": "value", + "group": "connection", + "source": "Connection.java", + "protocol": false + }, + { + "name": "ApiKeyConnection", + "kind": "value", + "group": "connection", + "source": "Connection.java", + "protocol": false + }, + { + "name": "AuthorizationCodeFlow", + "kind": "value", + "group": "connection", + "source": "AuthorizationCodeFlow.java", + "protocol": false + }, + { + "name": "Connection", + "kind": "value", + "group": "connection", + "source": "Connection.java", + "protocol": false + }, + { + "name": "DeviceAuthorization", + "kind": "value", + "group": "connection", + "source": "DeviceAuthorization.java", + "protocol": false + }, + { + "name": "FoundryConnection", + "kind": "value", + "group": "connection", + "source": "Connection.java", + "protocol": false + }, + { + "name": "OAuthConnection", + "kind": "value", + "group": "connection", + "source": "Connection.java", + "protocol": false + }, + { + "name": "OAuthToken", + "kind": "value", + "group": "connection", + "source": "OAuthToken.java", + "protocol": false + }, + { + "name": "ReferenceConnection", + "kind": "value", + "group": "connection", + "source": "Connection.java", + "protocol": false + }, + { + "name": "RemoteConnection", + "kind": "value", + "group": "connection", + "source": "Connection.java", + "protocol": false + }, + { + "name": "AudioPart", + "kind": "value", + "group": "conversation", + "source": "ContentPart.java", + "protocol": false + }, + { + "name": "ContentPart", + "kind": "value", + "group": "conversation", + "source": "ContentPart.java", + "protocol": false + }, + { + "name": "FilePart", + "kind": "value", + "group": "conversation", + "source": "ContentPart.java", + "protocol": false + }, + { + "name": "ImagePart", + "kind": "value", + "group": "conversation", + "source": "ContentPart.java", + "protocol": false + }, + { + "name": "Message", + "kind": "value", + "group": "conversation", + "source": "Message.java", + "protocol": false + }, + { + "name": "TextPart", + "kind": "value", + "group": "conversation", + "source": "ContentPart.java", + "protocol": false + }, + { + "name": "ThreadMarker", + "kind": "value", + "group": "conversation", + "source": "ThreadMarker.java", + "protocol": false + }, + { + "name": "ToolCall", + "kind": "value", + "group": "conversation", + "source": "ToolCall.java", + "protocol": false + }, + { + "name": "ToolResult", + "kind": "value", + "group": "conversation", + "source": "ToolResult.java", + "protocol": false + }, + { + "name": "ArrayProperty", + "kind": "value", + "group": "core", + "source": "Property.java", + "protocol": false + }, + { + "name": "FileNotFoundError", + "kind": "value", + "group": "core", + "source": "FileNotFoundError.java", + "protocol": false + }, + { + "name": "InvokerError", + "kind": "value", + "group": "core", + "source": "InvokerError.java", + "protocol": false + }, + { + "name": "ObjectProperty", + "kind": "value", + "group": "core", + "source": "Property.java", + "protocol": false + }, + { + "name": "Property", + "kind": "value", + "group": "core", + "source": "Property.java", + "protocol": false + }, + { + "name": "UnionProperty", + "kind": "value", + "group": "core", + "source": "Property.java", + "protocol": false + }, + { + "name": "ValidationError", + "kind": "value", + "group": "core", + "source": "ValidationError.java", + "protocol": false + }, + { + "name": "ValidationResult", + "kind": "value", + "group": "core", + "source": "ValidationResult.java", + "protocol": false + }, + { + "name": "Checkpoint", + "kind": "value", + "group": "events", + "source": "Checkpoint.java", + "protocol": false + }, + { + "name": "CompactionCompletePayload", + "kind": "value", + "group": "events", + "source": "CompactionCompletePayload.java", + "protocol": false + }, + { + "name": "CompactionFailedPayload", + "kind": "value", + "group": "events", + "source": "CompactionFailedPayload.java", + "protocol": false + }, + { + "name": "CompactionStartPayload", + "kind": "value", + "group": "events", + "source": "CompactionStartPayload.java", + "protocol": false + }, + { + "name": "DoneEventPayload", + "kind": "value", + "group": "events", + "source": "DoneEventPayload.java", + "protocol": false + }, + { + "name": "ErrorChunk", + "kind": "value", + "group": "events", + "source": "StreamChunk.java", + "protocol": false + }, + { + "name": "ErrorEventPayload", + "kind": "value", + "group": "events", + "source": "ErrorEventPayload.java", + "protocol": false + }, + { + "name": "HarnessContext", + "kind": "value", + "group": "events", + "source": "HarnessContext.java", + "protocol": false + }, + { + "name": "HookEndPayload", + "kind": "value", + "group": "events", + "source": "HookEndPayload.java", + "protocol": false + }, + { + "name": "HookStartPayload", + "kind": "value", + "group": "events", + "source": "HookStartPayload.java", + "protocol": false + }, + { + "name": "HostToolRequest", + "kind": "value", + "group": "events", + "source": "HostToolRequest.java", + "protocol": false + }, + { + "name": "HostToolResult", + "kind": "value", + "group": "events", + "source": "HostToolResult.java", + "protocol": false + }, + { + "name": "LlmCompletePayload", + "kind": "value", + "group": "events", + "source": "LlmCompletePayload.java", + "protocol": false + }, + { + "name": "LlmStartPayload", + "kind": "value", + "group": "events", + "source": "LlmStartPayload.java", + "protocol": false + }, + { + "name": "MessagesUpdatedPayload", + "kind": "value", + "group": "events", + "source": "MessagesUpdatedPayload.java", + "protocol": false + }, + { + "name": "PermissionCompletedPayload", + "kind": "value", + "group": "events", + "source": "PermissionCompletedPayload.java", + "protocol": false + }, + { + "name": "PermissionDecision", + "kind": "value", + "group": "events", + "source": "PermissionDecision.java", + "protocol": false + }, + { + "name": "PermissionRequest", + "kind": "value", + "group": "events", + "source": "PermissionRequest.java", + "protocol": false + }, + { + "name": "PermissionRequestedPayload", + "kind": "value", + "group": "events", + "source": "PermissionRequestedPayload.java", + "protocol": false + }, + { + "name": "RedactedField", + "kind": "value", + "group": "events", + "source": "RedactedField.java", + "protocol": false + }, + { + "name": "RedactionMetadata", + "kind": "value", + "group": "events", + "source": "RedactionMetadata.java", + "protocol": false + }, + { + "name": "RetryPayload", + "kind": "value", + "group": "events", + "source": "RetryPayload.java", + "protocol": false + }, + { + "name": "SessionEndPayload", + "kind": "value", + "group": "events", + "source": "SessionEndPayload.java", + "protocol": false + }, + { + "name": "SessionEvent", + "kind": "value", + "group": "events", + "source": "SessionEvent.java", + "protocol": false + }, + { + "name": "SessionFileRef", + "kind": "value", + "group": "events", + "source": "SessionFileRef.java", + "protocol": false + }, + { + "name": "SessionRef", + "kind": "value", + "group": "events", + "source": "SessionRef.java", + "protocol": false + }, + { + "name": "SessionStartPayload", + "kind": "value", + "group": "events", + "source": "SessionStartPayload.java", + "protocol": false + }, + { + "name": "SessionSummary", + "kind": "value", + "group": "events", + "source": "SessionSummary.java", + "protocol": false + }, + { + "name": "SessionTrace", + "kind": "value", + "group": "events", + "source": "SessionTrace.java", + "protocol": false + }, + { + "name": "SessionWarningPayload", + "kind": "value", + "group": "events", + "source": "SessionWarningPayload.java", + "protocol": false + }, + { + "name": "StatusEventPayload", + "kind": "value", + "group": "events", + "source": "StatusEventPayload.java", + "protocol": false + }, + { + "name": "StreamChunk", + "kind": "value", + "group": "events", + "source": "StreamChunk.java", + "protocol": false + }, + { + "name": "TextChunk", + "kind": "value", + "group": "events", + "source": "StreamChunk.java", + "protocol": false + }, + { + "name": "ThinkingChunk", + "kind": "value", + "group": "events", + "source": "StreamChunk.java", + "protocol": false + }, + { + "name": "ThinkingEventPayload", + "kind": "value", + "group": "events", + "source": "ThinkingEventPayload.java", + "protocol": false + }, + { + "name": "TokenEventPayload", + "kind": "value", + "group": "events", + "source": "TokenEventPayload.java", + "protocol": false + }, + { + "name": "ToolCallCompletePayload", + "kind": "value", + "group": "events", + "source": "ToolCallCompletePayload.java", + "protocol": false + }, + { + "name": "ToolCallStartPayload", + "kind": "value", + "group": "events", + "source": "ToolCallStartPayload.java", + "protocol": false + }, + { + "name": "ToolChunk", + "kind": "value", + "group": "events", + "source": "StreamChunk.java", + "protocol": false + }, + { + "name": "ToolExecutionCompletePayload", + "kind": "value", + "group": "events", + "source": "ToolExecutionCompletePayload.java", + "protocol": false + }, + { + "name": "ToolExecutionStartPayload", + "kind": "value", + "group": "events", + "source": "ToolExecutionStartPayload.java", + "protocol": false + }, + { + "name": "ToolResultPayload", + "kind": "value", + "group": "events", + "source": "ToolResultPayload.java", + "protocol": false + }, + { + "name": "TrajectoryEvent", + "kind": "value", + "group": "events", + "source": "TrajectoryEvent.java", + "protocol": false + }, + { + "name": "TurnEndPayload", + "kind": "value", + "group": "events", + "source": "TurnEndPayload.java", + "protocol": false + }, + { + "name": "TurnEvent", + "kind": "value", + "group": "events", + "source": "TurnEvent.java", + "protocol": false + }, + { + "name": "TurnStartPayload", + "kind": "value", + "group": "events", + "source": "TurnStartPayload.java", + "protocol": false + }, + { + "name": "TurnSummary", + "kind": "value", + "group": "events", + "source": "TurnSummary.java", + "protocol": false + }, + { + "name": "TurnTrace", + "kind": "value", + "group": "events", + "source": "TurnTrace.java", + "protocol": false + }, + { + "name": "UsageChunk", + "kind": "value", + "group": "events", + "source": "StreamChunk.java", + "protocol": false + }, + { + "name": "MemoryEntry", + "kind": "value", + "group": "memory", + "source": "MemoryEntry.java", + "protocol": false + }, + { + "name": "MemoryStore", + "kind": "value", + "group": "memory", + "source": "MemoryStore.java", + "protocol": false + }, + { + "name": "AiResourceInfo", + "kind": "value", + "group": "model", + "source": "AiResourceInfo.java", + "protocol": false + }, + { + "name": "InvocationUsage", + "kind": "value", + "group": "model", + "source": "InvocationUsage.java", + "protocol": false + }, + { + "name": "Model", + "kind": "value", + "group": "model", + "source": "Model.java", + "protocol": false + }, + { + "name": "ModelInfo", + "kind": "value", + "group": "model", + "source": "ModelInfo.java", + "protocol": false + }, + { + "name": "ModelLister", + "kind": "type", + "group": "model", + "source": "ModelLister.java", + "protocol": true + }, + { + "name": "ModelOptions", + "kind": "value", + "group": "model", + "source": "ModelOptions.java", + "protocol": false + }, + { + "name": "ProjectInfo", + "kind": "value", + "group": "model", + "source": "ProjectInfo.java", + "protocol": false + }, + { + "name": "SubscriptionInfo", + "kind": "value", + "group": "model", + "source": "SubscriptionInfo.java", + "protocol": false + }, + { + "name": "TokenUsage", + "kind": "value", + "group": "model", + "source": "TokenUsage.java", + "protocol": false + }, + { + "name": "CheckpointStore", + "kind": "type", + "group": "pipeline", + "source": "CheckpointStore.java", + "protocol": true + }, + { + "name": "CompactionConfig", + "kind": "value", + "group": "pipeline", + "source": "CompactionConfig.java", + "protocol": false + }, + { + "name": "ContextCandidate", + "kind": "value", + "group": "pipeline", + "source": "ContextCandidate.java", + "protocol": false + }, + { + "name": "ContextRequest", + "kind": "value", + "group": "pipeline", + "source": "ContextRequest.java", + "protocol": false + }, + { + "name": "DelegatedStateReference", + "kind": "value", + "group": "pipeline", + "source": "DelegatedStateReference.java", + "protocol": false + }, + { + "name": "EngineCheckpoint", + "kind": "value", + "group": "pipeline", + "source": "EngineCheckpoint.java", + "protocol": false + }, + { + "name": "EngineEvent", + "kind": "value", + "group": "pipeline", + "source": "EngineEvent.java", + "protocol": false + }, + { + "name": "EnginePermissionDecision", + "kind": "value", + "group": "pipeline", + "source": "EnginePermissionDecision.java", + "protocol": false + }, + { + "name": "EventJournalWriter", + "kind": "type", + "group": "pipeline", + "source": "EventJournalWriter.java", + "protocol": true + }, + { + "name": "EventSink", + "kind": "type", + "group": "pipeline", + "source": "EventSink.java", + "protocol": true + }, + { + "name": "Executor", + "kind": "type", + "group": "pipeline", + "source": "Executor.java", + "protocol": true + }, + { + "name": "FinalOutputPolicyRequest", + "kind": "value", + "group": "pipeline", + "source": "FinalOutputPolicyRequest.java", + "protocol": false + }, + { + "name": "FinalOutputPolicyResult", + "kind": "value", + "group": "pipeline", + "source": "FinalOutputPolicyResult.java", + "protocol": false + }, + { + "name": "HostPolicyRequest", + "kind": "value", + "group": "pipeline", + "source": "HostPolicyRequest.java", + "protocol": false + }, + { + "name": "HostPolicyResult", + "kind": "value", + "group": "pipeline", + "source": "HostPolicyResult.java", + "protocol": false + }, + { + "name": "HostToolExecutor", + "kind": "type", + "group": "pipeline", + "source": "HostToolExecutor.java", + "protocol": true + }, + { + "name": "InvocationContextDecision", + "kind": "value", + "group": "pipeline", + "source": "InvocationContextDecision.java", + "protocol": false + }, + { + "name": "InvocationContextState", + "kind": "value", + "group": "pipeline", + "source": "InvocationContextState.java", + "protocol": false + }, + { + "name": "ModelInvocationContextSnapshot", + "kind": "value", + "group": "pipeline", + "source": "ModelInvocationContextSnapshot.java", + "protocol": false + }, + { + "name": "ModelInvocationRequest", + "kind": "value", + "group": "pipeline", + "source": "ModelInvocationRequest.java", + "protocol": false + }, + { + "name": "ModelInvocationResponse", + "kind": "value", + "group": "pipeline", + "source": "ModelInvocationResponse.java", + "protocol": false + }, + { + "name": "ModelReconciliationState", + "kind": "value", + "group": "pipeline", + "source": "ModelReconciliationState.java", + "protocol": false + }, + { + "name": "ModelToolRequest", + "kind": "value", + "group": "pipeline", + "source": "ModelToolRequest.java", + "protocol": false + }, + { + "name": "ModelToolResult", + "kind": "value", + "group": "pipeline", + "source": "ModelToolResult.java", + "protocol": false + }, + { + "name": "Parser", + "kind": "type", + "group": "pipeline", + "source": "Parser.java", + "protocol": true + }, + { + "name": "PermissionResolver", + "kind": "type", + "group": "pipeline", + "source": "PermissionResolver.java", + "protocol": true + }, + { + "name": "Processor", + "kind": "type", + "group": "pipeline", + "source": "Processor.java", + "protocol": true + }, + { + "name": "Renderer", + "kind": "type", + "group": "pipeline", + "source": "Renderer.java", + "protocol": true + }, + { + "name": "ReplayJournalRecord", + "kind": "value", + "group": "pipeline", + "source": "ReplayJournalRecord.java", + "protocol": false + }, + { + "name": "ReplayMismatch", + "kind": "value", + "group": "pipeline", + "source": "ReplayMismatch.java", + "protocol": false + }, + { + "name": "ReplayVerificationRequest", + "kind": "value", + "group": "pipeline", + "source": "ReplayVerificationRequest.java", + "protocol": false + }, + { + "name": "ReplayVerificationResult", + "kind": "value", + "group": "pipeline", + "source": "ReplayVerificationResult.java", + "protocol": false + }, + { + "name": "ResumeContext", + "kind": "value", + "group": "pipeline", + "source": "ResumeContext.java", + "protocol": false + }, + { + "name": "RetryPolicyRequest", + "kind": "value", + "group": "pipeline", + "source": "RetryPolicyRequest.java", + "protocol": false + }, + { + "name": "RunTurnRequest", + "kind": "value", + "group": "pipeline", + "source": "RunTurnRequest.java", + "protocol": false + }, + { + "name": "RunTurnResult", + "kind": "value", + "group": "pipeline", + "source": "RunTurnResult.java", + "protocol": false + }, + { + "name": "TurnCommit", + "kind": "value", + "group": "pipeline", + "source": "TurnCommit.java", + "protocol": false + }, + { + "name": "TurnEngineResult", + "kind": "value", + "group": "pipeline", + "source": "TurnEngineResult.java", + "protocol": false + }, + { + "name": "TurnModelRequest", + "kind": "value", + "group": "pipeline", + "source": "TurnModelRequest.java", + "protocol": false + }, + { + "name": "TurnModelResponse", + "kind": "value", + "group": "pipeline", + "source": "TurnModelResponse.java", + "protocol": false + }, + { + "name": "TurnOptions", + "kind": "value", + "group": "pipeline", + "source": "TurnOptions.java", + "protocol": false + }, + { + "name": "StreamOptions", + "kind": "value", + "group": "streaming", + "source": "StreamOptions.java", + "protocol": false + }, + { + "name": "FormatConfig", + "kind": "value", + "group": "template", + "source": "FormatConfig.java", + "protocol": false + }, + { + "name": "ParserConfig", + "kind": "value", + "group": "template", + "source": "ParserConfig.java", + "protocol": false + }, + { + "name": "Template", + "kind": "value", + "group": "template", + "source": "Template.java", + "protocol": false + }, + { + "name": "Binding", + "kind": "value", + "group": "tools", + "source": "Binding.java", + "protocol": false + }, + { + "name": "CustomTool", + "kind": "value", + "group": "tools", + "source": "Tool.java", + "protocol": false + }, + { + "name": "FunctionTool", + "kind": "value", + "group": "tools", + "source": "Tool.java", + "protocol": false + }, + { + "name": "McpApprovalMode", + "kind": "value", + "group": "tools", + "source": "McpApprovalMode.java", + "protocol": false + }, + { + "name": "McpTool", + "kind": "value", + "group": "tools", + "source": "Tool.java", + "protocol": false + }, + { + "name": "OpenApiTool", + "kind": "value", + "group": "tools", + "source": "Tool.java", + "protocol": false + }, + { + "name": "PromptyTool", + "kind": "value", + "group": "tools", + "source": "Tool.java", + "protocol": false + }, + { + "name": "Tool", + "kind": "value", + "group": "tools", + "source": "Tool.java", + "protocol": false + }, + { + "name": "ToolContext", + "kind": "value", + "group": "tools", + "source": "ToolContext.java", + "protocol": false + }, + { + "name": "ToolDispatchResult", + "kind": "value", + "group": "tools", + "source": "ToolDispatchResult.java", + "protocol": false + }, + { + "name": "TraceFile", + "kind": "value", + "group": "tracing", + "source": "TraceFile.java", + "protocol": false + }, + { + "name": "TraceSpan", + "kind": "value", + "group": "tracing", + "source": "TraceSpan.java", + "protocol": false + }, + { + "name": "TraceTime", + "kind": "value", + "group": "tracing", + "source": "TraceTime.java", + "protocol": false + }, + { + "name": "AnthropicImageBlock", + "kind": "value", + "group": "wire", + "source": "AnthropicImageBlock.java", + "protocol": false + }, + { + "name": "AnthropicImageSource", + "kind": "value", + "group": "wire", + "source": "AnthropicImageSource.java", + "protocol": false + }, + { + "name": "AnthropicMessagesRequest", + "kind": "value", + "group": "wire", + "source": "AnthropicMessagesRequest.java", + "protocol": false + }, + { + "name": "AnthropicMessagesResponse", + "kind": "value", + "group": "wire", + "source": "AnthropicMessagesResponse.java", + "protocol": false + }, + { + "name": "AnthropicTextBlock", + "kind": "value", + "group": "wire", + "source": "AnthropicTextBlock.java", + "protocol": false + }, + { + "name": "AnthropicToolDefinition", + "kind": "value", + "group": "wire", + "source": "AnthropicToolDefinition.java", + "protocol": false + }, + { + "name": "AnthropicToolResultBlock", + "kind": "value", + "group": "wire", + "source": "AnthropicToolResultBlock.java", + "protocol": false + }, + { + "name": "AnthropicToolUseBlock", + "kind": "value", + "group": "wire", + "source": "AnthropicToolUseBlock.java", + "protocol": false + }, + { + "name": "AnthropicUsage", + "kind": "value", + "group": "wire", + "source": "AnthropicUsage.java", + "protocol": false + }, + { + "name": "AnthropicWireMessage", + "kind": "value", + "group": "wire", + "source": "AnthropicWireMessage.java", + "protocol": false + } + ], + "groups": [ + { + "name": "agent", + "exports": [ + "GuardrailResult", + "Prompty" + ], + "modules": [ + "GuardrailResult.java", + "Prompty.java" + ] + }, + { + "name": "connection", + "exports": [ + "AnonymousConnection", + "ApiKeyConnection", + "AuthorizationCodeFlow", + "Connection", + "DeviceAuthorization", + "FoundryConnection", + "OAuthConnection", + "OAuthToken", + "ReferenceConnection", + "RemoteConnection" + ], + "modules": [ + "AuthorizationCodeFlow.java", + "Connection.java", + "DeviceAuthorization.java", + "OAuthToken.java" + ] + }, + { + "name": "conversation", + "exports": [ + "AudioPart", + "ContentPart", + "FilePart", + "ImagePart", + "Message", + "TextPart", + "ThreadMarker", + "ToolCall", + "ToolResult" + ], + "modules": [ + "ContentPart.java", + "Message.java", + "ThreadMarker.java", + "ToolCall.java", + "ToolResult.java" + ] + }, + { + "name": "core", + "exports": [ + "ArrayProperty", + "FileNotFoundError", + "InvokerError", + "ObjectProperty", + "Property", + "UnionProperty", + "ValidationError", + "ValidationResult" + ], + "modules": [ + "FileNotFoundError.java", + "InvokerError.java", + "Property.java", + "ValidationError.java", + "ValidationResult.java" + ] + }, + { + "name": "events", + "exports": [ + "Checkpoint", + "CompactionCompletePayload", + "CompactionFailedPayload", + "CompactionStartPayload", + "DoneEventPayload", + "ErrorChunk", + "ErrorEventPayload", + "HarnessContext", + "HookEndPayload", + "HookStartPayload", + "HostToolRequest", + "HostToolResult", + "LlmCompletePayload", + "LlmStartPayload", + "MessagesUpdatedPayload", + "PermissionCompletedPayload", + "PermissionDecision", + "PermissionRequest", + "PermissionRequestedPayload", + "RedactedField", + "RedactionMetadata", + "RetryPayload", + "SessionEndPayload", + "SessionEvent", + "SessionFileRef", + "SessionRef", + "SessionStartPayload", + "SessionSummary", + "SessionTrace", + "SessionWarningPayload", + "StatusEventPayload", + "StreamChunk", + "TextChunk", + "ThinkingChunk", + "ThinkingEventPayload", + "TokenEventPayload", + "ToolCallCompletePayload", + "ToolCallStartPayload", + "ToolChunk", + "ToolExecutionCompletePayload", + "ToolExecutionStartPayload", + "ToolResultPayload", + "TrajectoryEvent", + "TurnEndPayload", + "TurnEvent", + "TurnStartPayload", + "TurnSummary", + "TurnTrace", + "UsageChunk" + ], + "modules": [ + "Checkpoint.java", + "CompactionCompletePayload.java", + "CompactionFailedPayload.java", + "CompactionStartPayload.java", + "DoneEventPayload.java", + "ErrorEventPayload.java", + "HarnessContext.java", + "HookEndPayload.java", + "HookStartPayload.java", + "HostToolRequest.java", + "HostToolResult.java", + "LlmCompletePayload.java", + "LlmStartPayload.java", + "MessagesUpdatedPayload.java", + "PermissionCompletedPayload.java", + "PermissionDecision.java", + "PermissionRequest.java", + "PermissionRequestedPayload.java", + "RedactedField.java", + "RedactionMetadata.java", + "RetryPayload.java", + "SessionEndPayload.java", + "SessionEvent.java", + "SessionFileRef.java", + "SessionRef.java", + "SessionStartPayload.java", + "SessionSummary.java", + "SessionTrace.java", + "SessionWarningPayload.java", + "StatusEventPayload.java", + "StreamChunk.java", + "ThinkingEventPayload.java", + "TokenEventPayload.java", + "ToolCallCompletePayload.java", + "ToolCallStartPayload.java", + "ToolExecutionCompletePayload.java", + "ToolExecutionStartPayload.java", + "ToolResultPayload.java", + "TrajectoryEvent.java", + "TurnEndPayload.java", + "TurnEvent.java", + "TurnStartPayload.java", + "TurnSummary.java", + "TurnTrace.java" + ] + }, + { + "name": "memory", + "exports": [ + "MemoryEntry", + "MemoryStore" + ], + "modules": [ + "MemoryEntry.java", + "MemoryStore.java" + ] + }, + { + "name": "model", + "exports": [ + "AiResourceInfo", + "InvocationUsage", + "Model", + "ModelInfo", + "ModelLister", + "ModelOptions", + "ProjectInfo", + "SubscriptionInfo", + "TokenUsage" + ], + "modules": [ + "AiResourceInfo.java", + "InvocationUsage.java", + "Model.java", + "ModelInfo.java", + "ModelLister.java", + "ModelOptions.java", + "ProjectInfo.java", + "SubscriptionInfo.java", + "TokenUsage.java" + ] + }, + { + "name": "pipeline", + "exports": [ + "CheckpointStore", + "CompactionConfig", + "ContextCandidate", + "ContextRequest", + "DelegatedStateReference", + "EngineCheckpoint", + "EngineEvent", + "EnginePermissionDecision", + "EventJournalWriter", + "EventSink", + "Executor", + "FinalOutputPolicyRequest", + "FinalOutputPolicyResult", + "HostPolicyRequest", + "HostPolicyResult", + "HostToolExecutor", + "InvocationContextDecision", + "InvocationContextState", + "ModelInvocationContextSnapshot", + "ModelInvocationRequest", + "ModelInvocationResponse", + "ModelReconciliationState", + "ModelToolRequest", + "ModelToolResult", + "Parser", + "PermissionResolver", + "Processor", + "Renderer", + "ReplayJournalRecord", + "ReplayMismatch", + "ReplayVerificationRequest", + "ReplayVerificationResult", + "ResumeContext", + "RetryPolicyRequest", + "RunTurnRequest", + "RunTurnResult", + "TurnCommit", + "TurnEngineResult", + "TurnModelRequest", + "TurnModelResponse", + "TurnOptions" + ], + "modules": [ + "CheckpointStore.java", + "CompactionConfig.java", + "ContextCandidate.java", + "ContextRequest.java", + "DelegatedStateReference.java", + "EngineCheckpoint.java", + "EngineEvent.java", + "EnginePermissionDecision.java", + "EventJournalWriter.java", + "EventSink.java", + "Executor.java", + "FinalOutputPolicyRequest.java", + "FinalOutputPolicyResult.java", + "HostPolicyRequest.java", + "HostPolicyResult.java", + "HostToolExecutor.java", + "InvocationContextDecision.java", + "InvocationContextState.java", + "ModelInvocationContextSnapshot.java", + "ModelInvocationRequest.java", + "ModelInvocationResponse.java", + "ModelReconciliationState.java", + "ModelToolRequest.java", + "ModelToolResult.java", + "Parser.java", + "PermissionResolver.java", + "Processor.java", + "Renderer.java", + "ReplayJournalRecord.java", + "ReplayMismatch.java", + "ReplayVerificationRequest.java", + "ReplayVerificationResult.java", + "ResumeContext.java", + "RetryPolicyRequest.java", + "RunTurnRequest.java", + "RunTurnResult.java", + "TurnCommit.java", + "TurnEngineResult.java", + "TurnModelRequest.java", + "TurnModelResponse.java", + "TurnOptions.java" + ] + }, + { + "name": "streaming", + "exports": [ + "StreamOptions" + ], + "modules": [ + "StreamOptions.java" + ] + }, + { + "name": "template", + "exports": [ + "FormatConfig", + "ParserConfig", + "Template" + ], + "modules": [ + "FormatConfig.java", + "ParserConfig.java", + "Template.java" + ] + }, + { + "name": "tools", + "exports": [ + "Binding", + "CustomTool", + "FunctionTool", + "McpApprovalMode", + "McpTool", + "OpenApiTool", + "PromptyTool", + "Tool", + "ToolContext", + "ToolDispatchResult" + ], + "modules": [ + "Binding.java", + "McpApprovalMode.java", + "Tool.java", + "ToolContext.java", + "ToolDispatchResult.java" + ] + }, + { + "name": "tracing", + "exports": [ + "TraceFile", + "TraceSpan", + "TraceTime" + ], + "modules": [ + "TraceFile.java", + "TraceSpan.java", + "TraceTime.java" + ] + }, + { + "name": "wire", + "exports": [ + "AnthropicImageBlock", + "AnthropicImageSource", + "AnthropicMessagesRequest", + "AnthropicMessagesResponse", + "AnthropicTextBlock", + "AnthropicToolDefinition", + "AnthropicToolResultBlock", + "AnthropicToolUseBlock", + "AnthropicUsage", + "AnthropicWireMessage" + ], + "modules": [ + "AnthropicImageBlock.java", + "AnthropicImageSource.java", + "AnthropicMessagesRequest.java", + "AnthropicMessagesResponse.java", + "AnthropicTextBlock.java", + "AnthropicToolDefinition.java", + "AnthropicToolResultBlock.java", + "AnthropicToolUseBlock.java", + "AnthropicUsage.java", + "AnthropicWireMessage.java" + ] + } + ], + "protocols": [ + { + "name": "ModelLister", + "group": "model", + "symbol": "ModelLister", + "source": "ModelLister.java", + "methods": [ + { + "name": "listModels", + "returns": "ModelInfo[]", + "params": { + "connection": "unknown" + }, + "optional": false, + "sync": false + } + ] + }, + { + "name": "CheckpointStore", + "group": "pipeline", + "symbol": "CheckpointStore", + "source": "CheckpointStore.java", + "methods": [ + { + "name": "listCheckpoints", + "returns": "Checkpoint[]", + "params": { + "sessionId": "string" + }, + "optional": false, + "sync": false + }, + { + "name": "load", + "returns": "Checkpoint?", + "params": { + "checkpointId": "string", + "sessionId": "string" + }, + "optional": false, + "sync": false + }, + { + "name": "save", + "returns": "Checkpoint", + "params": { + "checkpoint": "Checkpoint" + }, + "optional": false, + "sync": false + } + ] + }, + { + "name": "EventJournalWriter", + "group": "pipeline", + "symbol": "EventJournalWriter", + "source": "EventJournalWriter.java", + "methods": [ + { + "name": "appendSession", + "returns": "boolean", + "params": { + "sessionEvent": "SessionEvent" + }, + "optional": false, + "sync": true + }, + { + "name": "appendTurn", + "returns": "boolean", + "params": { + "turnEvent": "TurnEvent" + }, + "optional": false, + "sync": true + }, + { + "name": "close", + "returns": "boolean", + "params": { + "summary": "SessionSummary?" + }, + "optional": false, + "sync": true + } + ] + }, + { + "name": "EventSink", + "group": "pipeline", + "symbol": "EventSink", + "source": "EventSink.java", + "methods": [ + { + "name": "emitSession", + "returns": "boolean", + "params": { + "sessionEvent": "SessionEvent" + }, + "optional": false, + "sync": true + }, + { + "name": "emitTurn", + "returns": "boolean", + "params": { + "turnEvent": "TurnEvent" + }, + "optional": false, + "sync": true + } + ] + }, + { + "name": "Executor", + "group": "pipeline", + "symbol": "Executor", + "source": "Executor.java", + "methods": [ + { + "name": "execute", + "returns": "unknown", + "params": { + "agent": "Prompty", + "messages": "Message[]" + }, + "optional": false, + "sync": false + }, + { + "name": "executeStream", + "returns": "unknown", + "params": { + "agent": "Prompty", + "messages": "Message[]" + }, + "optional": true, + "sync": false + }, + { + "name": "formatToolMessages", + "returns": "Message[]", + "params": { + "rawResponse": "unknown", + "textContent": "string?", + "toolCalls": "ToolCall[]", + "toolResults": "string[]" + }, + "optional": false, + "sync": true + } + ] + }, + { + "name": "HostToolExecutor", + "group": "pipeline", + "symbol": "HostToolExecutor", + "source": "HostToolExecutor.java", + "methods": [ + { + "name": "execute", + "returns": "HostToolResult", + "params": { + "request": "HostToolRequest" + }, + "optional": false, + "sync": false + } + ] + }, + { + "name": "Parser", + "group": "pipeline", + "symbol": "Parser", + "source": "Parser.java", + "methods": [ + { + "name": "parse", + "returns": "Message[]", + "params": { + "agent": "Prompty", + "context": "Record?", + "rendered": "string" + }, + "optional": false, + "sync": false + }, + { + "name": "preRender", + "returns": "unknown?", + "params": { + "template": "string" + }, + "optional": true, + "sync": true + } + ] + }, + { + "name": "PermissionResolver", + "group": "pipeline", + "symbol": "PermissionResolver", + "source": "PermissionResolver.java", + "methods": [ + { + "name": "request", + "returns": "PermissionDecision", + "params": { + "request": "PermissionRequest" + }, + "optional": false, + "sync": false + } + ] + }, + { + "name": "Processor", + "group": "pipeline", + "symbol": "Processor", + "source": "Processor.java", + "methods": [ + { + "name": "process", + "returns": "unknown", + "params": { + "agent": "Prompty", + "response": "unknown" + }, + "optional": false, + "sync": false + }, + { + "name": "processStream", + "returns": "unknown", + "params": { + "stream": "unknown" + }, + "optional": true, + "sync": false + } + ] + }, + { + "name": "Renderer", + "group": "pipeline", + "symbol": "Renderer", + "source": "Renderer.java", + "methods": [ + { + "name": "render", + "returns": "string", + "params": { + "agent": "Prompty", + "inputs": "Record", + "template": "string" + }, + "optional": false, + "sync": false + } + ] + } + ], + "modules": [ + "AiResourceInfo.java", + "AnthropicImageBlock.java", + "AnthropicImageSource.java", + "AnthropicMessagesRequest.java", + "AnthropicMessagesResponse.java", + "AnthropicTextBlock.java", + "AnthropicToolDefinition.java", + "AnthropicToolResultBlock.java", + "AnthropicToolUseBlock.java", + "AnthropicUsage.java", + "AnthropicWireMessage.java", + "AuthorizationCodeFlow.java", + "Binding.java", + "Checkpoint.java", + "CheckpointStore.java", + "CompactionCompletePayload.java", + "CompactionConfig.java", + "CompactionFailedPayload.java", + "CompactionStartPayload.java", + "Connection.java", + "ContentPart.java", + "ContextCandidate.java", + "ContextRequest.java", + "DelegatedStateReference.java", + "DeviceAuthorization.java", + "DoneEventPayload.java", + "EngineCheckpoint.java", + "EngineEvent.java", + "EnginePermissionDecision.java", + "ErrorEventPayload.java", + "EventJournalWriter.java", + "EventSink.java", + "Executor.java", + "FileNotFoundError.java", + "FinalOutputPolicyRequest.java", + "FinalOutputPolicyResult.java", + "FormatConfig.java", + "GuardrailResult.java", + "HarnessContext.java", + "HookEndPayload.java", + "HookStartPayload.java", + "HostPolicyRequest.java", + "HostPolicyResult.java", + "HostToolExecutor.java", + "HostToolRequest.java", + "HostToolResult.java", + "InvocationContextDecision.java", + "InvocationContextState.java", + "InvocationUsage.java", + "InvokerError.java", + "LlmCompletePayload.java", + "LlmStartPayload.java", + "McpApprovalMode.java", + "MemoryEntry.java", + "MemoryStore.java", + "Message.java", + "MessagesUpdatedPayload.java", + "Model.java", + "ModelInfo.java", + "ModelInvocationContextSnapshot.java", + "ModelInvocationRequest.java", + "ModelInvocationResponse.java", + "ModelLister.java", + "ModelOptions.java", + "ModelReconciliationState.java", + "ModelToolRequest.java", + "ModelToolResult.java", + "OAuthToken.java", + "Parser.java", + "ParserConfig.java", + "PermissionCompletedPayload.java", + "PermissionDecision.java", + "PermissionRequest.java", + "PermissionRequestedPayload.java", + "PermissionResolver.java", + "Processor.java", + "ProjectInfo.java", + "Prompty.java", + "Property.java", + "RedactedField.java", + "RedactionMetadata.java", + "Renderer.java", + "ReplayJournalRecord.java", + "ReplayMismatch.java", + "ReplayVerificationRequest.java", + "ReplayVerificationResult.java", + "ResumeContext.java", + "RetryPayload.java", + "RetryPolicyRequest.java", + "RunTurnRequest.java", + "RunTurnResult.java", + "SessionEndPayload.java", + "SessionEvent.java", + "SessionFileRef.java", + "SessionRef.java", + "SessionStartPayload.java", + "SessionSummary.java", + "SessionTrace.java", + "SessionWarningPayload.java", + "StatusEventPayload.java", + "StreamChunk.java", + "StreamOptions.java", + "SubscriptionInfo.java", + "Template.java", + "ThinkingEventPayload.java", + "ThreadMarker.java", + "TokenEventPayload.java", + "TokenUsage.java", + "Tool.java", + "ToolCall.java", + "ToolCallCompletePayload.java", + "ToolCallStartPayload.java", + "ToolContext.java", + "ToolDispatchResult.java", + "ToolExecutionCompletePayload.java", + "ToolExecutionStartPayload.java", + "ToolResult.java", + "ToolResultPayload.java", + "TraceFile.java", + "TraceSpan.java", + "TraceTime.java", + "TrajectoryEvent.java", + "TurnCommit.java", + "TurnEndPayload.java", + "TurnEngineResult.java", + "TurnEvent.java", + "TurnModelRequest.java", + "TurnModelResponse.java", + "TurnOptions.java", + "TurnStartPayload.java", + "TurnSummary.java", + "TurnTrace.java", + "ValidationError.java", + "ValidationResult.java" + ] + }, { "target": "markdown", "outputRoot": "../web/src/content/docs/reference", diff --git a/schema/tsp-output/.typra-generated/hydration-seams.json b/schema/tsp-output/.typra-generated/hydration-seams.json index aee225062..64b12c9c6 100644 --- a/schema/tsp-output/.typra-generated/hydration-seams.json +++ b/schema/tsp-output/.typra-generated/hydration-seams.json @@ -164,6 +164,86 @@ "generatedSource": "renderer.go", "seamKind": "protocol-adapter" }, + { + "contract": "ModelLister", + "target": "java", + "group": "model", + "symbol": "ModelLister", + "generatedSource": "ModelLister.java", + "seamKind": "protocol-adapter" + }, + { + "contract": "CheckpointStore", + "target": "java", + "group": "pipeline", + "symbol": "CheckpointStore", + "generatedSource": "CheckpointStore.java", + "seamKind": "protocol-adapter" + }, + { + "contract": "EventJournalWriter", + "target": "java", + "group": "pipeline", + "symbol": "EventJournalWriter", + "generatedSource": "EventJournalWriter.java", + "seamKind": "protocol-adapter" + }, + { + "contract": "EventSink", + "target": "java", + "group": "pipeline", + "symbol": "EventSink", + "generatedSource": "EventSink.java", + "seamKind": "protocol-adapter" + }, + { + "contract": "Executor", + "target": "java", + "group": "pipeline", + "symbol": "Executor", + "generatedSource": "Executor.java", + "seamKind": "protocol-adapter" + }, + { + "contract": "HostToolExecutor", + "target": "java", + "group": "pipeline", + "symbol": "HostToolExecutor", + "generatedSource": "HostToolExecutor.java", + "seamKind": "protocol-adapter" + }, + { + "contract": "Parser", + "target": "java", + "group": "pipeline", + "symbol": "Parser", + "generatedSource": "Parser.java", + "seamKind": "protocol-adapter" + }, + { + "contract": "PermissionResolver", + "target": "java", + "group": "pipeline", + "symbol": "PermissionResolver", + "generatedSource": "PermissionResolver.java", + "seamKind": "protocol-adapter" + }, + { + "contract": "Processor", + "target": "java", + "group": "pipeline", + "symbol": "Processor", + "generatedSource": "Processor.java", + "seamKind": "protocol-adapter" + }, + { + "contract": "Renderer", + "target": "java", + "group": "pipeline", + "symbol": "Renderer", + "generatedSource": "Renderer.java", + "seamKind": "protocol-adapter" + }, { "contract": "ModelLister", "target": "markdown", diff --git a/schema/tsp-output/.typra-generated/manifest.json b/schema/tsp-output/.typra-generated/manifest.json index 4581f6480..aca97fdf5 100644 --- a/schema/tsp-output/.typra-generated/manifest.json +++ b/schema/tsp-output/.typra-generated/manifest.json @@ -3053,6 +3053,1666 @@ "path": "../runtime/go/prompty/model/validation_result.go", "marker": true }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AiResourceInfo.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnonymousConnection.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnthropicImageBlock.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnthropicImageSource.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnthropicMessagesRequest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnthropicMessagesResponse.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnthropicTextBlock.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnthropicToolDefinition.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnthropicToolResultBlock.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnthropicToolUseBlock.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnthropicUsage.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnthropicWireMessage.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ApiKeyConnection.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ArrayProperty.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AudioPart.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AuthenticationMode.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AuthorizationCodeFlow.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Binding.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Checkpoint.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/CheckpointStore.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/CompactionCompletePayload.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/CompactionConfig.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/CompactionFailedPayload.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/CompactionStartPayload.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Connection.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ContentPart.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ContextCandidate.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ContextRequest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/CustomTool.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/DelegatedStateReference.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/DeviceAuthorization.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/DoneEventPayload.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/EngineCheckpoint.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/EngineEvent.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/EngineEventKind.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/EnginePermissionDecision.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/EngineTurnStatus.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ErrorChunk.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ErrorEventPayload.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/EventJournalWriter.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/EventSink.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Executor.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/FileNotFoundError.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/FilePart.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/FinalOutputPolicyRequest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/FinalOutputPolicyResult.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/FormatConfig.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/FoundryConnection.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/FunctionTool.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/GuardrailResult.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/HarnessContext.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/HookEndPayload.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/HookEndScope.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/HookStartPayload.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/HookStartScope.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/HostPolicyRequest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/HostPolicyResult.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/HostToolExecutor.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/HostToolRequest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/HostToolResult.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ImagePart.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/InvocationContextDecision.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/InvocationContextDisposition.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/InvocationContextPortability.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/InvocationContextState.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/InvocationUsage.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/InvokerError.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/LlmCompletePayload.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/LlmStartPayload.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/LoadContext.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/McpApprovalMode.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/McpApprovalModeKind.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/McpTool.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/MemoryCategory.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/MemoryEntry.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/MemoryStore.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Message.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/MessagesUpdatedPayload.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Model.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelInfo.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelInvocationContextSnapshot.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelInvocationRequest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelInvocationResponse.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelLister.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelOptions.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelReconciliationState.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelToolOutcome.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelToolRequest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelToolResult.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/OAuthConnection.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/OAuthToken.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ObjectProperty.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/OpenApiTool.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Parser.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ParserConfig.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/PermissionCompletedPayload.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/PermissionDecision.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/PermissionRequest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/PermissionRequestedPayload.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/PermissionResolver.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Processor.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ProjectInfo.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Prompty.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/PromptyTool.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Property.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/RedactedField.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/RedactionMetadata.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/RedactionMode.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ReferenceConnection.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/RemoteConnection.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Renderer.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ReplayJournalRecord.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ReplayMismatch.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ReplayRecordKind.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ReplayRecordStatus.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ReplayVerificationRequest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ReplayVerificationResult.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ReplayVerificationStatus.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ResumeContext.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/RetryPayload.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/RetryPolicyRequest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Role.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/RunTurnRequest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/RunTurnResult.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/RunTurnStatus.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SaveContext.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionEndPayload.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionEndStatus.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionEvent.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionEventType.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionFileRef.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionRef.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionStartPayload.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionSummary.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionSummaryStatus.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionTrace.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionWarningPayload.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/StatusEventPayload.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/StreamChunk.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/StreamOptions.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SubscriptionInfo.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Template.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TextChunk.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TextPart.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ThinkingChunk.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ThinkingEventPayload.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ThreadMarker.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TokenEventPayload.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TokenUsage.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Tool.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolCall.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolCallCompletePayload.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolCallStartPayload.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolChunk.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolContext.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolDispatchResult.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolExecutionCompletePayload.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolExecutionStartPayload.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolResult.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolResultPayload.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolResultStatus.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TraceFile.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TraceSpan.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TraceTime.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TrajectoryEvent.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnCommit.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnEndPayload.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnEngineResult.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnEvent.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnEventType.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnModelRequest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnModelResponse.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnOptions.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnStartPayload.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnStatus.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnSummary.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnTrace.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TypraJson.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TypraMaps.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TypraYaml.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/UnionProperty.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/UsageChunk.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ValidationError.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ValidationResult.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AiResourceInfoGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnonymousConnectionGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnthropicImageBlockGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnthropicImageSourceGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnthropicMessagesRequestGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnthropicMessagesResponseGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnthropicTextBlockGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnthropicToolDefinitionGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnthropicToolResultBlockGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnthropicToolUseBlockGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnthropicUsageGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnthropicWireMessageGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ApiKeyConnectionGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ArrayPropertyGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AudioPartGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AuthorizationCodeFlowGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/BindingGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/CheckpointGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/CompactionCompletePayloadGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/CompactionConfigGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/CompactionFailedPayloadGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/CompactionStartPayloadGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ConnectionGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ContentPartGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ContextCandidateGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ContextRequestGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/CustomToolGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/DelegatedStateReferenceGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/DeviceAuthorizationGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/DoneEventPayloadGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/EngineCheckpointGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/EngineEventGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/EnginePermissionDecisionGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ErrorChunkGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ErrorEventPayloadGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/FileNotFoundErrorGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/FilePartGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/FinalOutputPolicyRequestGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/FinalOutputPolicyResultGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/FormatConfigGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/FoundryConnectionGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/FunctionToolGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/GuardrailResultGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/HarnessContextGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/HookEndPayloadGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/HookStartPayloadGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/HostPolicyRequestGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/HostPolicyResultGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/HostToolRequestGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/HostToolResultGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ImagePartGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/InvocationContextDecisionGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/InvocationContextStateGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/InvocationUsageGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/InvokerErrorGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/LlmCompletePayloadGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/LlmStartPayloadGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/McpApprovalModeGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/McpToolGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/MemoryEntryGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/MemoryStoreGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/MessageGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/MessagesUpdatedPayloadGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ModelGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ModelInfoGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ModelInvocationContextSnapshotGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ModelInvocationRequestGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ModelInvocationResponseGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ModelOptionsGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ModelReconciliationStateGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ModelToolRequestGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ModelToolResultGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/OAuthConnectionGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/OAuthTokenGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ObjectPropertyGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/OpenApiToolGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ParserConfigGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/PermissionCompletedPayloadGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/PermissionDecisionGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/PermissionRequestedPayloadGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/PermissionRequestGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ProjectInfoGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/PromptyGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/PromptyToolGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/PropertyGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/RedactedFieldGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/RedactionMetadataGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ReferenceConnectionGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/RemoteConnectionGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ReplayJournalRecordGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ReplayMismatchGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ReplayVerificationRequestGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ReplayVerificationResultGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ResumeContextGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/RetryPayloadGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/RetryPolicyRequestGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/RunTurnRequestGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/RunTurnResultGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/SessionEndPayloadGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/SessionEventGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/SessionFileRefGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/SessionRefGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/SessionStartPayloadGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/SessionSummaryGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/SessionTraceGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/SessionWarningPayloadGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/StatusEventPayloadGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/StreamChunkGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/StreamOptionsGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/SubscriptionInfoGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TemplateGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TextChunkGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TextPartGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ThinkingChunkGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ThinkingEventPayloadGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ThreadMarkerGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TokenEventPayloadGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TokenUsageGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolCallCompletePayloadGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolCallGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolCallStartPayloadGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolChunkGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolContextGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolDispatchResultGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolExecutionCompletePayloadGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolExecutionStartPayloadGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolResultGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolResultPayloadGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TraceFileGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TraceSpanGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TraceTimeGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TrajectoryEventGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TurnCommitGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TurnEndPayloadGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TurnEngineResultGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TurnEventGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TurnModelRequestGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TurnModelResponseGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TurnOptionsGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TurnStartPayloadGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TurnSummaryGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TurnTraceGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TypraGeneratedTests.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/UnionPropertyGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/UsageChunkGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ValidationErrorGeneratedTest.java", + "marker": true + }, + { + "outputRoot": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model", + "path": "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ValidationResultGeneratedTest.java", + "marker": true + }, { "outputRoot": "../runtime/python/prompty/prompty/model", "path": "../runtime/python/prompty/prompty/model/__init__.py", diff --git a/schema/tspconfig.yaml b/schema/tspconfig.yaml index a4be8b639..35aa91202 100644 --- a/schema/tspconfig.yaml +++ b/schema/tspconfig.yaml @@ -32,5 +32,9 @@ options: output-dir: "../runtime/rust/prompty/src/model" test-dir: "../runtime/rust/prompty/tests/model" import-path: "prompty::model" + - type: Java + output-dir: "../runtime/java/prompty/src/main/java/com/microsoft/prompty/model" + test-dir: "../runtime/java/prompty/src/test/java/com/microsoft/prompty/model" + package-name: "com.microsoft.prompty.model" - type: markdown output-dir: "../web/src/content/docs/reference" diff --git a/spec/vectors/agent/agent_vectors.json b/spec/vectors/agent/agent_vectors.json index 4401d7ff0..35e1b5ea1 100644 --- a/spec/vectors/agent/agent_vectors.json +++ b/spec/vectors/agent/agent_vectors.json @@ -18,15 +18,13 @@ "name": "get_weather", "kind": "function", "description": "Get the current weather for a city", - "parameters": { - "properties": [ - { - "name": "city", - "kind": "string", - "required": true - } - ] - } + "parameters": [ + { + "name": "city", + "kind": "string", + "required": true + } + ] } ], "tool_functions": { @@ -79,15 +77,13 @@ "name": "get_weather", "kind": "function", "description": "Get the current weather for a city", - "parameters": { - "properties": [ - { - "name": "city", - "kind": "string", - "required": true - } - ] - } + "parameters": [ + { + "name": "city", + "kind": "string", + "required": true + } + ] } ], "tool_functions": { @@ -219,15 +215,13 @@ "name": "get_weather", "kind": "function", "description": "Get the current weather for a city", - "parameters": { - "properties": [ - { - "name": "city", - "kind": "string", - "required": true - } - ] - } + "parameters": [ + { + "name": "city", + "kind": "string", + "required": true + } + ] } ], "tool_functions": { @@ -393,39 +387,35 @@ "name": "get_weather", "kind": "function", "description": "Get the current weather for a city (returns Fahrenheit)", - "parameters": { - "properties": [ - { - "name": "city", - "kind": "string", - "required": true - } - ] - } + "parameters": [ + { + "name": "city", + "kind": "string", + "required": true + } + ] }, { "name": "convert_temperature", "kind": "function", "description": "Convert a temperature between Fahrenheit and Celsius", - "parameters": { - "properties": [ - { - "name": "value", - "kind": "float", - "required": true - }, - { - "name": "from_unit", - "kind": "string", - "required": true - }, - { - "name": "to_unit", - "kind": "string", - "required": true - } - ] - } + "parameters": [ + { + "name": "value", + "kind": "float", + "required": true + }, + { + "name": "from_unit", + "kind": "string", + "required": true + }, + { + "name": "to_unit", + "kind": "string", + "required": true + } + ] } ], "tool_functions": { @@ -565,15 +555,13 @@ "name": "get_weather", "kind": "function", "description": "Get the current weather for a city", - "parameters": { - "properties": [ - { - "name": "city", - "kind": "string", - "required": true - } - ] - } + "parameters": [ + { + "name": "city", + "kind": "string", + "required": true + } + ] } ], "tool_functions": { @@ -1071,20 +1059,18 @@ "name": "get_weather", "kind": "function", "description": "Get the current weather for a city", - "parameters": { - "properties": [ - { - "name": "city", - "kind": "string", - "required": true - }, - { - "name": "unit", - "kind": "string", - "required": false - } - ] - }, + "parameters": [ + { + "name": "city", + "kind": "string", + "required": true + }, + { + "name": "unit", + "kind": "string", + "required": false + } + ], "bindings": { "unit": { "input": "preferred_unit" @@ -1191,15 +1177,13 @@ "name": "get_weather", "kind": "function", "description": "Get the current weather for a city", - "parameters": { - "properties": [ - { - "name": "city", - "kind": "string", - "required": true - } - ] - } + "parameters": [ + { + "name": "city", + "kind": "string", + "required": true + } + ] } ], "tool_functions": { @@ -1268,15 +1252,13 @@ "name": "get_weather", "kind": "function", "description": "Get the current weather for a city", - "parameters": { - "properties": [ - { - "name": "city", - "kind": "string", - "required": true - } - ] - } + "parameters": [ + { + "name": "city", + "kind": "string", + "required": true + } + ] } ], "tool_functions": { @@ -1382,15 +1364,13 @@ "name": "get_weather", "kind": "function", "description": "Get the current weather for a city", - "parameters": { - "properties": [ - { - "name": "city", - "kind": "string", - "required": true - } - ] - } + "parameters": [ + { + "name": "city", + "kind": "string", + "required": true + } + ] } ], "tool_functions": { @@ -1527,9 +1507,7 @@ "name": "clear_cache", "kind": "function", "description": "Clear the application cache, returns empty on success", - "parameters": { - "properties": [] - } + "parameters": [] } ], "tool_functions": { @@ -1635,15 +1613,13 @@ "name": "lookup", "kind": "function", "description": "Look up data", - "parameters": { - "properties": [ - { - "name": "query", - "kind": "string", - "required": true - } - ] - } + "parameters": [ + { + "name": "query", + "kind": "string", + "required": true + } + ] } ], "tool_functions": { @@ -1738,15 +1714,13 @@ "name": "get_weather", "kind": "function", "description": "Get the current weather for a city", - "parameters": { - "properties": [ - { - "name": "city", - "kind": "string", - "required": true - } - ] - } + "parameters": [ + { + "name": "city", + "kind": "string", + "required": true + } + ] } ], "tool_functions": { @@ -1877,15 +1851,13 @@ "name": "get_weather", "kind": "function", "description": "Get the current weather for a city", - "parameters": { - "properties": [ - { - "name": "city", - "kind": "string", - "required": true - } - ] - } + "parameters": [ + { + "name": "city", + "kind": "string", + "required": true + } + ] } ], "tool_functions": { @@ -1948,15 +1920,13 @@ "name": "get_weather", "kind": "function", "description": "Get the current weather for a city", - "parameters": { - "properties": [ - { - "name": "city", - "kind": "string", - "required": true - } - ] - } + "parameters": [ + { + "name": "city", + "kind": "string", + "required": true + } + ] } ], "tool_functions": { @@ -2067,15 +2037,13 @@ "name": "get_weather", "kind": "function", "description": "Get the current weather for a city", - "parameters": { - "properties": [ - { - "name": "city", - "kind": "string", - "required": true - } - ] - } + "parameters": [ + { + "name": "city", + "kind": "string", + "required": true + } + ] } ], "tool_functions": { @@ -2121,15 +2089,13 @@ "name": "get_weather", "kind": "function", "description": "Get the current weather for a city", - "parameters": { - "properties": [ - { - "name": "city", - "kind": "string", - "required": true - } - ] - } + "parameters": [ + { + "name": "city", + "kind": "string", + "required": true + } + ] } ], "tool_functions": { @@ -2253,15 +2219,13 @@ "name": "get_weather", "kind": "function", "description": "Get the current weather for a city", - "parameters": { - "properties": [ - { - "name": "city", - "kind": "string", - "required": true - } - ] - } + "parameters": [ + { + "name": "city", + "kind": "string", + "required": true + } + ] } ], "tool_functions": { @@ -2408,15 +2372,13 @@ "name": "get_weather", "kind": "function", "description": "Get the current weather for a city", - "parameters": { - "properties": [ - { - "name": "city", - "kind": "string", - "required": true - } - ] - } + "parameters": [ + { + "name": "city", + "kind": "string", + "required": true + } + ] } ], "tool_functions": { @@ -2527,15 +2489,13 @@ "name": "get_weather", "kind": "function", "description": "Get the current weather for a city", - "parameters": { - "properties": [ - { - "name": "city", - "kind": "string", - "required": true - } - ] - } + "parameters": [ + { + "name": "city", + "kind": "string", + "required": true + } + ] } ], "tool_functions": { @@ -2611,15 +2571,13 @@ "name": "get_weather", "kind": "function", "description": "Get the current weather for a city", - "parameters": { - "properties": [ - { - "name": "city", - "kind": "string", - "required": true - } - ] - } + "parameters": [ + { + "name": "city", + "kind": "string", + "required": true + } + ] } ], "tool_functions": { @@ -2691,15 +2649,13 @@ "name": "get_weather", "kind": "function", "description": "Get the current weather for a city", - "parameters": { - "properties": [ - { - "name": "city", - "kind": "string", - "required": true - } - ] - } + "parameters": [ + { + "name": "city", + "kind": "string", + "required": true + } + ] } ], "tool_functions": { @@ -2740,15 +2696,13 @@ "name": "get_weather", "kind": "function", "description": "Get the current weather for a city", - "parameters": { - "properties": [ - { - "name": "city", - "kind": "string", - "required": true - } - ] - } + "parameters": [ + { + "name": "city", + "kind": "string", + "required": true + } + ] } ], "tool_functions": { @@ -2811,29 +2765,25 @@ "name": "get_weather", "kind": "function", "description": "Get the current weather for a city", - "parameters": { - "properties": [ - { - "name": "city", - "kind": "string", - "required": true - } - ] - } + "parameters": [ + { + "name": "city", + "kind": "string", + "required": true + } + ] }, { "name": "dangerous_tool", "kind": "function", "description": "A dangerous operation that should be guarded", - "parameters": { - "properties": [ - { - "name": "target", - "kind": "string", - "required": true - } - ] - } + "parameters": [ + { + "name": "target", + "kind": "string", + "required": true + } + ] } ], "tool_functions": { @@ -2960,15 +2910,13 @@ "name": "get_weather", "kind": "function", "description": "Get the current weather for a city", - "parameters": { - "properties": [ - { - "name": "city", - "kind": "string", - "required": true - } - ] - } + "parameters": [ + { + "name": "city", + "kind": "string", + "required": true + } + ] } ], "tool_functions": { @@ -3076,15 +3024,13 @@ "name": "get_weather", "kind": "function", "description": "Get the current weather for a city", - "parameters": { - "properties": [ - { - "name": "city", - "kind": "string", - "required": true - } - ] - } + "parameters": [ + { + "name": "city", + "kind": "string", + "required": true + } + ] } ], "tool_functions": { @@ -3277,15 +3223,13 @@ "name": "get_weather", "kind": "function", "description": "Get the current weather for a city", - "parameters": { - "properties": [ - { - "name": "city", - "kind": "string", - "required": true - } - ] - } + "parameters": [ + { + "name": "city", + "kind": "string", + "required": true + } + ] } ], "tool_functions": { @@ -3440,37 +3384,31 @@ "name": "get_weather", "kind": "function", "description": "Get the current weather for a city", - "parameters": { - "properties": [ - { - "name": "city", - "kind": "string", - "required": true - } - ] - } + "parameters": [ + { + "name": "city", + "kind": "string", + "required": true + } + ] }, { "name": "get_time", "kind": "function", "description": "Get the current time in a city", - "parameters": { - "properties": [ - { - "name": "city", - "kind": "string", - "required": true - } - ] - } + "parameters": [ + { + "name": "city", + "kind": "string", + "required": true + } + ] }, { "name": "get_news", "kind": "function", "description": "Get the latest news headlines", - "parameters": { - "properties": [] - } + "parameters": [] } ], "tool_functions": { @@ -3611,43 +3549,37 @@ "name": "get_weather", "kind": "function", "description": "Get the current weather for a city", - "parameters": { - "properties": [ - { - "name": "city", - "kind": "string", - "required": true - } - ] - } + "parameters": [ + { + "name": "city", + "kind": "string", + "required": true + } + ] }, { "name": "get_time", "kind": "function", "description": "Get the current time in a city", - "parameters": { - "properties": [ - { - "name": "city", - "kind": "string", - "required": true - } - ] - } + "parameters": [ + { + "name": "city", + "kind": "string", + "required": true + } + ] }, { "name": "dangerous_tool", "kind": "function", "description": "A dangerous operation", - "parameters": { - "properties": [ - { - "name": "target", - "kind": "string", - "required": true - } - ] - } + "parameters": [ + { + "name": "target", + "kind": "string", + "required": true + } + ] } ], "tool_functions": {