From 6ca27d767da3f36b19f43b86e9ca7c609e7a0b14 Mon Sep 17 00:00:00 2001 From: Seth Juarez Date: Tue, 4 Aug 2026 00:36:11 -0700 Subject: [PATCH 01/29] Add Java target to the model generation pipeline Introduces `runtime/java/` and wires a Java emit target into the shared TypeSpec pipeline so the Java runtime is built on the same canonical Typra-emitted model layer as the C#, Go, Python, Rust and TypeScript runtimes. No hand-written duplicate model code is introduced. The @typra/emitter@0.4.2 Java backend is immature: its raw output does not compile and diverges from the other backends. `normalize-java-output.mjs` applies a fixed, deterministic set of structural rewrites from the sanctioned post-emit seam, covering seventeen catalogued emitter defects (J1-J4, J6-J17) - reserved-word field names, abstract-model instantiation, enum typing and visibility, base-property population in derived loaders, named-collection normalization, `collectionFormat` support, double-run save hooks, eagerly materialized optional defaults, and null-initialized required enums. Behaviour is matched against the generated C# and Rust runtimes rather than guessed. Object-format collection eligibility is resolved from the TypeSpec declared type - a named-collection alias, or an element model that declares `name` - which reproduces exactly the eleven object-format save sites the C# backend emits, and correctly keeps `UnionProperty.anyOf`/`oneOf` array-only. The shim is temporary and self-policing: each rewrite asserts it matched, and generation is idempotent, so an upstream emitter fix surfaces as a loud failure rather than silent drift. Verified: 334 generated files, clean build with zero warnings, 174 tests passing (152 generated example round-trips plus 22 normalization regression tests), and byte-identical output across repeated generation. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- runtime/java/.env.example | 21 + runtime/java/.gitignore | 11 + runtime/java/README.md | 77 + runtime/java/build.gradle.kts | 48 + .../java/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 48462 bytes .../gradle/wrapper/gradle-wrapper.properties | 9 + runtime/java/gradlew | 248 ++ runtime/java/gradlew.bat | 82 + .../java/prompty-anthropic/build.gradle.kts | 11 + runtime/java/prompty-foundry/build.gradle.kts | 11 + runtime/java/prompty-openai/build.gradle.kts | 11 + runtime/java/prompty/build.gradle.kts | 19 + .../prompty/model/AiResourceInfo.java | 147 ++ .../prompty/model/AnonymousConnection.java | 86 + .../prompty/model/AnthropicImageBlock.java | 85 + .../prompty/model/AnthropicImageSource.java | 90 + .../model/AnthropicMessagesRequest.java | 134 ++ .../model/AnthropicMessagesResponse.java | 115 + .../prompty/model/AnthropicTextBlock.java | 85 + .../model/AnthropicToolDefinition.java | 92 + .../model/AnthropicToolResultBlock.java | 90 + .../prompty/model/AnthropicToolUseBlock.java | 97 + .../prompty/model/AnthropicUsage.java | 85 + .../prompty/model/AnthropicWireMessage.java | 90 + .../prompty/model/ApiKeyConnection.java | 91 + .../prompty/model/ArrayProperty.java | 86 + .../microsoft/prompty/model/AudioPart.java | 91 + .../prompty/model/AuthenticationMode.java | 18 + .../prompty/model/AuthorizationCodeFlow.java | 103 + .../com/microsoft/prompty/model/Binding.java | 90 + .../microsoft/prompty/model/Checkpoint.java | 134 ++ .../prompty/model/CheckpointStore.java | 14 + .../model/CompactionCompletePayload.java | 90 + .../prompty/model/CompactionConfig.java | 92 + .../model/CompactionFailedPayload.java | 80 + .../prompty/model/CompactionStartPayload.java | 80 + .../microsoft/prompty/model/Connection.java | 106 + .../microsoft/prompty/model/ContentPart.java | 92 + .../prompty/model/ContextCandidate.java | 102 + .../prompty/model/ContextRequest.java | 120 + .../microsoft/prompty/model/CustomTool.java | 93 + .../model/DelegatedStateReference.java | 97 + .../prompty/model/DeviceAuthorization.java | 147 ++ .../prompty/model/DoneEventPayload.java | 90 + .../prompty/model/EngineCheckpoint.java | 210 ++ .../microsoft/prompty/model/EngineEvent.java | 135 ++ .../prompty/model/EngineEventKind.java | 39 + .../model/EnginePermissionDecision.java | 92 + .../prompty/model/EngineTurnStatus.java | 20 + .../microsoft/prompty/model/ErrorChunk.java | 86 + .../prompty/model/ErrorEventPayload.java | 90 + .../prompty/model/EventJournalWriter.java | 14 + .../microsoft/prompty/model/EventSink.java | 13 + .../com/microsoft/prompty/model/Executor.java | 14 + .../prompty/model/FileNotFoundError.java | 85 + .../com/microsoft/prompty/model/FilePart.java | 91 + .../model/FinalOutputPolicyRequest.java | 110 + .../model/FinalOutputPolicyResult.java | 87 + .../microsoft/prompty/model/FormatConfig.java | 97 + .../prompty/model/FoundryConnection.java | 96 + .../microsoft/prompty/model/FunctionTool.java | 95 + .../prompty/model/GuardrailResult.java | 102 + .../prompty/model/HarnessContext.java | 92 + .../prompty/model/HookEndPayload.java | 117 + .../microsoft/prompty/model/HookEndScope.java | 18 + .../prompty/model/HookStartPayload.java | 102 + .../prompty/model/HookStartScope.java | 18 + .../prompty/model/HostPolicyRequest.java | 110 + .../prompty/model/HostPolicyResult.java | 97 + .../prompty/model/HostToolExecutor.java | 12 + .../prompty/model/HostToolRequest.java | 102 + .../prompty/model/HostToolResult.java | 122 + .../microsoft/prompty/model/ImagePart.java | 96 + .../model/InvocationContextDecision.java | 107 + .../model/InvocationContextDisposition.java | 18 + .../model/InvocationContextPortability.java | 19 + .../prompty/model/InvocationContextState.java | 90 + .../prompty/model/InvocationUsage.java | 116 + .../microsoft/prompty/model/InvokerError.java | 90 + .../prompty/model/LlmCompletePayload.java | 95 + .../prompty/model/LlmStartPayload.java | 95 + .../microsoft/prompty/model/LoadContext.java | 29 + .../prompty/model/McpApprovalMode.java | 105 + .../prompty/model/McpApprovalModeKind.java | 19 + .../com/microsoft/prompty/model/McpTool.java | 111 + .../prompty/model/MemoryCategory.java | 19 + .../microsoft/prompty/model/MemoryEntry.java | 100 + .../microsoft/prompty/model/MemoryStore.java | 85 + .../com/microsoft/prompty/model/Message.java | 117 + .../prompty/model/MessagesUpdatedPayload.java | 105 + .../com/microsoft/prompty/model/Model.java | 105 + .../prompty/model/ModelCollections.java | 131 ++ .../microsoft/prompty/model/ModelInfo.java | 165 ++ .../model/ModelInvocationContextSnapshot.java | 137 ++ .../prompty/model/ModelInvocationRequest.java | 80 + .../model/ModelInvocationResponse.java | 116 + .../microsoft/prompty/model/ModelLister.java | 12 + .../microsoft/prompty/model/ModelOptions.java | 200 ++ .../model/ModelReconciliationState.java | 102 + .../prompty/model/ModelToolOutcome.java | 19 + .../prompty/model/ModelToolRequest.java | 97 + .../prompty/model/ModelToolResult.java | 107 + .../prompty/model/OAuthConnection.java | 111 + .../microsoft/prompty/model/OAuthToken.java | 136 ++ .../prompty/model/ObjectProperty.java | 90 + .../microsoft/prompty/model/OpenApiTool.java | 91 + .../com/microsoft/prompty/model/Parser.java | 13 + .../microsoft/prompty/model/ParserConfig.java | 92 + .../model/PermissionCompletedPayload.java | 112 + .../prompty/model/PermissionDecision.java | 107 + .../prompty/model/PermissionRequest.java | 114 + .../model/PermissionRequestedPayload.java | 119 + .../prompty/model/PermissionResolver.java | 12 + .../microsoft/prompty/model/Processor.java | 13 + .../microsoft/prompty/model/ProjectInfo.java | 114 + .../com/microsoft/prompty/model/Prompty.java | 139 ++ .../microsoft/prompty/model/PromptyTool.java | 91 + .../com/microsoft/prompty/model/Property.java | 159 ++ .../prompty/model/RedactedField.java | 90 + .../prompty/model/RedactionMetadata.java | 95 + .../prompty/model/RedactionMode.java | 21 + .../prompty/model/ReferenceConnection.java | 91 + .../prompty/model/RemoteConnection.java | 91 + .../com/microsoft/prompty/model/Renderer.java | 12 + .../prompty/model/ReplayJournalRecord.java | 135 ++ .../prompty/model/ReplayMismatch.java | 95 + .../prompty/model/ReplayRecordKind.java | 19 + .../prompty/model/ReplayRecordStatus.java | 19 + .../model/ReplayVerificationRequest.java | 95 + .../model/ReplayVerificationResult.java | 100 + .../model/ReplayVerificationStatus.java | 18 + .../prompty/model/ResumeContext.java | 102 + .../microsoft/prompty/model/RetryPayload.java | 100 + .../prompty/model/RetryPolicyRequest.java | 95 + .../com/microsoft/prompty/model/Role.java | 21 + .../prompty/model/RunTurnRequest.java | 97 + .../prompty/model/RunTurnResult.java | 120 + .../prompty/model/RunTurnStatus.java | 19 + .../microsoft/prompty/model/SaveContext.java | 35 + .../prompty/model/SessionEndPayload.java | 95 + .../prompty/model/SessionEndStatus.java | 20 + .../microsoft/prompty/model/SessionEvent.java | 122 + .../prompty/model/SessionEventType.java | 23 + .../prompty/model/SessionFileRef.java | 100 + .../microsoft/prompty/model/SessionRef.java | 100 + .../prompty/model/SessionStartPayload.java | 120 + .../prompty/model/SessionSummary.java | 105 + .../prompty/model/SessionSummaryStatus.java | 20 + .../microsoft/prompty/model/SessionTrace.java | 160 ++ .../prompty/model/SessionWarningPayload.java | 92 + .../prompty/model/StatusEventPayload.java | 80 + .../microsoft/prompty/model/StreamChunk.java | 94 + .../prompty/model/StreamOptions.java | 80 + .../prompty/model/SubscriptionInfo.java | 114 + .../com/microsoft/prompty/model/Template.java | 85 + .../microsoft/prompty/model/TextChunk.java | 86 + .../com/microsoft/prompty/model/TextPart.java | 86 + .../prompty/model/ThinkingChunk.java | 86 + .../prompty/model/ThinkingEventPayload.java | 80 + .../microsoft/prompty/model/ThreadMarker.java | 85 + .../prompty/model/TokenEventPayload.java | 80 + .../microsoft/prompty/model/TokenUsage.java | 116 + .../com/microsoft/prompty/model/Tool.java | 111 + .../com/microsoft/prompty/model/ToolCall.java | 90 + .../model/ToolCallCompletePayload.java | 105 + .../prompty/model/ToolCallStartPayload.java | 90 + .../microsoft/prompty/model/ToolChunk.java | 86 + .../microsoft/prompty/model/ToolContext.java | 92 + .../prompty/model/ToolDispatchResult.java | 90 + .../model/ToolExecutionCompletePayload.java | 127 + .../model/ToolExecutionStartPayload.java | 107 + .../microsoft/prompty/model/ToolResult.java | 113 + .../prompty/model/ToolResultPayload.java | 85 + .../prompty/model/ToolResultStatus.java | 20 + .../microsoft/prompty/model/TraceFile.java | 90 + .../microsoft/prompty/model/TraceSpan.java | 129 ++ .../microsoft/prompty/model/TraceTime.java | 90 + .../prompty/model/TrajectoryEvent.java | 122 + .../microsoft/prompty/model/TurnCommit.java | 125 + .../prompty/model/TurnEndPayload.java | 95 + .../prompty/model/TurnEngineResult.java | 104 + .../microsoft/prompty/model/TurnEvent.java | 117 + .../prompty/model/TurnEventType.java | 40 + .../prompty/model/TurnModelRequest.java | 112 + .../prompty/model/TurnModelResponse.java | 102 + .../microsoft/prompty/model/TurnOptions.java | 110 + .../prompty/model/TurnStartPayload.java | 92 + .../microsoft/prompty/model/TurnStatus.java | 19 + .../microsoft/prompty/model/TurnSummary.java | 115 + .../microsoft/prompty/model/TurnTrace.java | 105 + .../microsoft/prompty/model/TypraJson.java | 202 ++ .../microsoft/prompty/model/TypraMaps.java | 18 + .../microsoft/prompty/model/TypraYaml.java | 341 +++ .../prompty/model/UnionProperty.java | 101 + .../microsoft/prompty/model/UsageChunk.java | 86 + .../prompty/model/ValidationError.java | 90 + .../prompty/model/ValidationResult.java | 90 + .../prompty/GeneratedModelExamplesTest.java | 33 + .../prompty/ModelNormalizationTest.java | 458 ++++ .../model/AiResourceInfoGeneratedTest.java | 43 + .../AnonymousConnectionGeneratedTest.java | 65 + .../AnthropicImageBlockGeneratedTest.java | 43 + .../AnthropicImageSourceGeneratedTest.java | 65 + ...AnthropicMessagesRequestGeneratedTest.java | 90 + ...nthropicMessagesResponseGeneratedTest.java | 69 + .../AnthropicTextBlockGeneratedTest.java | 61 + .../AnthropicToolDefinitionGeneratedTest.java | 65 + ...AnthropicToolResultBlockGeneratedTest.java | 65 + .../AnthropicToolUseBlockGeneratedTest.java | 71 + .../model/AnthropicUsageGeneratedTest.java | 65 + .../AnthropicWireMessageGeneratedTest.java | 61 + .../model/ApiKeyConnectionGeneratedTest.java | 69 + .../model/ArrayPropertyGeneratedTest.java | 60 + .../prompty/model/AudioPartGeneratedTest.java | 65 + .../AuthorizationCodeFlowGeneratedTest.java | 43 + .../prompty/model/BindingGeneratedTest.java | 65 + .../model/CheckpointGeneratedTest.java | 81 + ...ompactionCompletePayloadGeneratedTest.java | 69 + .../model/CompactionConfigGeneratedTest.java | 68 + .../CompactionFailedPayloadGeneratedTest.java | 61 + .../CompactionStartPayloadGeneratedTest.java | 61 + .../model/ConnectionGeneratedTest.java | 69 + .../model/ContentPartGeneratedTest.java | 43 + .../model/ContextCandidateGeneratedTest.java | 65 + .../model/ContextRequestGeneratedTest.java | 43 + .../model/CustomToolGeneratedTest.java | 70 + .../DelegatedStateReferenceGeneratedTest.java | 69 + .../DeviceAuthorizationGeneratedTest.java | 43 + .../model/DoneEventPayloadGeneratedTest.java | 43 + .../model/EngineCheckpointGeneratedTest.java | 73 + .../model/EngineEventGeneratedTest.java | 77 + ...EnginePermissionDecisionGeneratedTest.java | 43 + .../model/ErrorChunkGeneratedTest.java | 61 + .../model/ErrorEventPayloadGeneratedTest.java | 69 + .../model/FileNotFoundErrorGeneratedTest.java | 65 + .../prompty/model/FilePartGeneratedTest.java | 65 + ...FinalOutputPolicyRequestGeneratedTest.java | 43 + .../FinalOutputPolicyResultGeneratedTest.java | 43 + .../model/FormatConfigGeneratedTest.java | 71 + .../model/FoundryConnectionGeneratedTest.java | 73 + .../model/FunctionToolGeneratedTest.java | 117 + .../prompty/model/GeneratedModelExamples.java | 162 ++ .../model/GuardrailResultGeneratedTest.java | 65 + .../model/HarnessContextGeneratedTest.java | 65 + .../model/HookEndPayloadGeneratedTest.java | 77 + .../model/HookStartPayloadGeneratedTest.java | 65 + .../model/HostPolicyRequestGeneratedTest.java | 43 + .../model/HostPolicyResultGeneratedTest.java | 43 + .../model/HostToolRequestGeneratedTest.java | 73 + .../model/HostToolResultGeneratedTest.java | 85 + .../prompty/model/ImagePartGeneratedTest.java | 69 + ...nvocationContextDecisionGeneratedTest.java | 65 + .../InvocationContextStateGeneratedTest.java | 43 + .../model/InvocationUsageGeneratedTest.java | 83 + .../model/InvokerErrorGeneratedTest.java | 69 + .../LlmCompletePayloadGeneratedTest.java | 69 + .../model/LlmStartPayloadGeneratedTest.java | 73 + .../model/McpApprovalModeGeneratedTest.java | 79 + .../prompty/model/McpToolGeneratedTest.java | 91 + .../model/MemoryEntryGeneratedTest.java | 82 + .../model/MemoryStoreGeneratedTest.java | 61 + .../prompty/model/MessageGeneratedTest.java | 88 + .../MessagesUpdatedPayloadGeneratedTest.java | 65 + .../prompty/model/ModelGeneratedTest.java | 97 + .../prompty/model/ModelInfoGeneratedTest.java | 114 + ...nvocationContextSnapshotGeneratedTest.java | 73 + .../ModelInvocationRequestGeneratedTest.java | 43 + .../ModelInvocationResponseGeneratedTest.java | 43 + .../model/ModelOptionsGeneratedTest.java | 147 ++ ...ModelReconciliationStateGeneratedTest.java | 65 + .../model/ModelToolRequestGeneratedTest.java | 65 + .../model/ModelToolResultGeneratedTest.java | 65 + .../model/OAuthConnectionGeneratedTest.java | 86 + .../model/OAuthTokenGeneratedTest.java | 43 + .../model/ObjectPropertyGeneratedTest.java | 65 + .../model/OpenApiToolGeneratedTest.java | 68 + .../model/ParserConfigGeneratedTest.java | 67 + ...rmissionCompletedPayloadGeneratedTest.java | 77 + .../PermissionDecisionGeneratedTest.java | 77 + .../model/PermissionRequestGeneratedTest.java | 77 + ...rmissionRequestedPayloadGeneratedTest.java | 77 + .../model/ProjectInfoGeneratedTest.java | 43 + .../prompty/model/PromptyGeneratedTest.java | 979 ++++++++ .../model/PromptyToolGeneratedTest.java | 69 + .../prompty/model/PropertyGeneratedTest.java | 102 + .../model/RedactedFieldGeneratedTest.java | 69 + .../model/RedactionMetadataGeneratedTest.java | 65 + .../ReferenceConnectionGeneratedTest.java | 69 + .../model/RemoteConnectionGeneratedTest.java | 69 + .../ReplayJournalRecordGeneratedTest.java | 43 + .../model/ReplayMismatchGeneratedTest.java | 43 + ...eplayVerificationRequestGeneratedTest.java | 43 + ...ReplayVerificationResultGeneratedTest.java | 43 + .../model/ResumeContextGeneratedTest.java | 61 + .../model/RetryPayloadGeneratedTest.java | 77 + .../RetryPolicyRequestGeneratedTest.java | 43 + .../model/RunTurnRequestGeneratedTest.java | 65 + .../model/RunTurnResultGeneratedTest.java | 69 + .../model/SessionEndPayloadGeneratedTest.java | 73 + .../model/SessionEventGeneratedTest.java | 81 + .../model/SessionFileRefGeneratedTest.java | 77 + .../model/SessionRefGeneratedTest.java | 77 + .../SessionStartPayloadGeneratedTest.java | 89 + .../model/SessionSummaryGeneratedTest.java | 77 + .../model/SessionTraceGeneratedTest.java | 73 + .../SessionWarningPayloadGeneratedTest.java | 65 + .../StatusEventPayloadGeneratedTest.java | 61 + .../model/StreamChunkGeneratedTest.java | 43 + .../model/StreamOptionsGeneratedTest.java | 61 + .../model/SubscriptionInfoGeneratedTest.java | 43 + .../prompty/model/TemplateGeneratedTest.java | 69 + .../prompty/model/TextChunkGeneratedTest.java | 61 + .../prompty/model/TextPartGeneratedTest.java | 61 + .../model/ThinkingChunkGeneratedTest.java | 61 + .../ThinkingEventPayloadGeneratedTest.java | 61 + .../model/ThreadMarkerGeneratedTest.java | 65 + .../model/TokenEventPayloadGeneratedTest.java | 61 + .../model/TokenUsageGeneratedTest.java | 83 + .../ToolCallCompletePayloadGeneratedTest.java | 77 + .../prompty/model/ToolCallGeneratedTest.java | 69 + .../ToolCallStartPayloadGeneratedTest.java | 69 + .../prompty/model/ToolChunkGeneratedTest.java | 71 + .../model/ToolContextGeneratedTest.java | 63 + .../ToolDispatchResultGeneratedTest.java | 73 + ...ExecutionCompletePayloadGeneratedTest.java | 85 + ...oolExecutionStartPayloadGeneratedTest.java | 73 + .../prompty/model/ToolGeneratedTest.java | 72 + .../model/ToolResultGeneratedTest.java | 90 + .../model/ToolResultPayloadGeneratedTest.java | 69 + .../prompty/model/TraceFileGeneratedTest.java | 65 + .../prompty/model/TraceSpanGeneratedTest.java | 69 + .../prompty/model/TraceTimeGeneratedTest.java | 69 + .../model/TrajectoryEventGeneratedTest.java | 85 + .../model/TurnCommitGeneratedTest.java | 65 + .../model/TurnEndPayloadGeneratedTest.java | 65 + .../model/TurnEngineResultGeneratedTest.java | 43 + .../prompty/model/TurnEventGeneratedTest.java | 81 + .../model/TurnModelRequestGeneratedTest.java | 69 + .../model/TurnModelResponseGeneratedTest.java | 43 + .../model/TurnOptionsGeneratedTest.java | 87 + .../model/TurnStartPayloadGeneratedTest.java | 65 + .../model/TurnSummaryGeneratedTest.java | 85 + .../prompty/model/TurnTraceGeneratedTest.java | 69 + .../prompty/model/TypraGeneratedTests.java | 157 ++ .../model/UnionPropertyGeneratedTest.java | 74 + .../model/UsageChunkGeneratedTest.java | 43 + .../model/ValidationErrorGeneratedTest.java | 69 + .../model/ValidationResultGeneratedTest.java | 65 + runtime/java/settings.gradle.kts | 12 + schema/package.json | 2 +- schema/scripts/clean-java-output.mjs | 40 + schema/scripts/normalize-java-output.mjs | 1166 ++++++++++ schema/scripts/normalize-typra-output.mjs | 31 + .../.typra-generated/export-surfaces.json | 2049 +++++++++++++++++ .../.typra-generated/hydration-seams.json | 80 + .../tsp-output/.typra-generated/manifest.json | 1550 +++++++++++++ schema/tspconfig.yaml | 4 + 357 files changed, 33647 insertions(+), 1 deletion(-) create mode 100644 runtime/java/.env.example create mode 100644 runtime/java/.gitignore create mode 100644 runtime/java/README.md create mode 100644 runtime/java/build.gradle.kts create mode 100644 runtime/java/gradle/wrapper/gradle-wrapper.jar create mode 100644 runtime/java/gradle/wrapper/gradle-wrapper.properties create mode 100644 runtime/java/gradlew create mode 100644 runtime/java/gradlew.bat create mode 100644 runtime/java/prompty-anthropic/build.gradle.kts create mode 100644 runtime/java/prompty-foundry/build.gradle.kts create mode 100644 runtime/java/prompty-openai/build.gradle.kts create mode 100644 runtime/java/prompty/build.gradle.kts create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AiResourceInfo.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnonymousConnection.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnthropicImageBlock.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnthropicImageSource.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnthropicMessagesRequest.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnthropicMessagesResponse.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnthropicTextBlock.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnthropicToolDefinition.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnthropicToolResultBlock.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnthropicToolUseBlock.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnthropicUsage.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnthropicWireMessage.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ApiKeyConnection.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ArrayProperty.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AudioPart.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AuthenticationMode.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AuthorizationCodeFlow.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Binding.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Checkpoint.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/CheckpointStore.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/CompactionCompletePayload.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/CompactionConfig.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/CompactionFailedPayload.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/CompactionStartPayload.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Connection.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ContentPart.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ContextCandidate.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ContextRequest.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/CustomTool.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/DelegatedStateReference.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/DeviceAuthorization.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/DoneEventPayload.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/EngineCheckpoint.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/EngineEvent.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/EngineEventKind.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/EnginePermissionDecision.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/EngineTurnStatus.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ErrorChunk.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ErrorEventPayload.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/EventJournalWriter.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/EventSink.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Executor.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/FileNotFoundError.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/FilePart.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/FinalOutputPolicyRequest.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/FinalOutputPolicyResult.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/FormatConfig.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/FoundryConnection.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/FunctionTool.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/GuardrailResult.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/HarnessContext.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/HookEndPayload.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/HookEndScope.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/HookStartPayload.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/HookStartScope.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/HostPolicyRequest.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/HostPolicyResult.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/HostToolExecutor.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/HostToolRequest.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/HostToolResult.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ImagePart.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/InvocationContextDecision.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/InvocationContextDisposition.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/InvocationContextPortability.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/InvocationContextState.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/InvocationUsage.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/InvokerError.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/LlmCompletePayload.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/LlmStartPayload.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/LoadContext.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/McpApprovalMode.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/McpApprovalModeKind.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/McpTool.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/MemoryCategory.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/MemoryEntry.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/MemoryStore.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Message.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/MessagesUpdatedPayload.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Model.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelCollections.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelInfo.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelInvocationContextSnapshot.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelInvocationRequest.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelInvocationResponse.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelLister.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelOptions.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelReconciliationState.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelToolOutcome.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelToolRequest.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelToolResult.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/OAuthConnection.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/OAuthToken.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ObjectProperty.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/OpenApiTool.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Parser.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ParserConfig.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/PermissionCompletedPayload.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/PermissionDecision.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/PermissionRequest.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/PermissionRequestedPayload.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/PermissionResolver.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Processor.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ProjectInfo.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Prompty.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/PromptyTool.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Property.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/RedactedField.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/RedactionMetadata.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/RedactionMode.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ReferenceConnection.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/RemoteConnection.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Renderer.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ReplayJournalRecord.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ReplayMismatch.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ReplayRecordKind.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ReplayRecordStatus.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ReplayVerificationRequest.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ReplayVerificationResult.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ReplayVerificationStatus.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ResumeContext.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/RetryPayload.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/RetryPolicyRequest.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Role.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/RunTurnRequest.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/RunTurnResult.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/RunTurnStatus.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SaveContext.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionEndPayload.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionEndStatus.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionEvent.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionEventType.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionFileRef.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionRef.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionStartPayload.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionSummary.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionSummaryStatus.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionTrace.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SessionWarningPayload.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/StatusEventPayload.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/StreamChunk.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/StreamOptions.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/SubscriptionInfo.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Template.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TextChunk.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TextPart.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ThinkingChunk.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ThinkingEventPayload.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ThreadMarker.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TokenEventPayload.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TokenUsage.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/Tool.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolCall.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolCallCompletePayload.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolCallStartPayload.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolChunk.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolContext.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolDispatchResult.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolExecutionCompletePayload.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolExecutionStartPayload.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolResult.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolResultPayload.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolResultStatus.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TraceFile.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TraceSpan.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TraceTime.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TrajectoryEvent.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnCommit.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnEndPayload.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnEngineResult.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnEvent.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnEventType.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnModelRequest.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnModelResponse.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnOptions.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnStartPayload.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnStatus.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnSummary.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TurnTrace.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TypraJson.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TypraMaps.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TypraYaml.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/UnionProperty.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/UsageChunk.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ValidationError.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ValidationResult.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/GeneratedModelExamplesTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/ModelNormalizationTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AiResourceInfoGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnonymousConnectionGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnthropicImageBlockGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnthropicImageSourceGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnthropicMessagesRequestGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnthropicMessagesResponseGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnthropicTextBlockGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnthropicToolDefinitionGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnthropicToolResultBlockGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnthropicToolUseBlockGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnthropicUsageGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AnthropicWireMessageGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ApiKeyConnectionGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ArrayPropertyGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AudioPartGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/AuthorizationCodeFlowGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/BindingGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/CheckpointGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/CompactionCompletePayloadGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/CompactionConfigGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/CompactionFailedPayloadGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/CompactionStartPayloadGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ConnectionGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ContentPartGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ContextCandidateGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ContextRequestGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/CustomToolGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/DelegatedStateReferenceGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/DeviceAuthorizationGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/DoneEventPayloadGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/EngineCheckpointGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/EngineEventGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/EnginePermissionDecisionGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ErrorChunkGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ErrorEventPayloadGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/FileNotFoundErrorGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/FilePartGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/FinalOutputPolicyRequestGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/FinalOutputPolicyResultGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/FormatConfigGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/FoundryConnectionGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/FunctionToolGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/GeneratedModelExamples.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/GuardrailResultGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/HarnessContextGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/HookEndPayloadGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/HookStartPayloadGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/HostPolicyRequestGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/HostPolicyResultGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/HostToolRequestGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/HostToolResultGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ImagePartGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/InvocationContextDecisionGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/InvocationContextStateGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/InvocationUsageGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/InvokerErrorGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/LlmCompletePayloadGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/LlmStartPayloadGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/McpApprovalModeGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/McpToolGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/MemoryEntryGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/MemoryStoreGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/MessageGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/MessagesUpdatedPayloadGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ModelGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ModelInfoGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ModelInvocationContextSnapshotGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ModelInvocationRequestGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ModelInvocationResponseGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ModelOptionsGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ModelReconciliationStateGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ModelToolRequestGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ModelToolResultGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/OAuthConnectionGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/OAuthTokenGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ObjectPropertyGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/OpenApiToolGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ParserConfigGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/PermissionCompletedPayloadGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/PermissionDecisionGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/PermissionRequestGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/PermissionRequestedPayloadGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ProjectInfoGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/PromptyGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/PromptyToolGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/PropertyGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/RedactedFieldGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/RedactionMetadataGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ReferenceConnectionGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/RemoteConnectionGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ReplayJournalRecordGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ReplayMismatchGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ReplayVerificationRequestGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ReplayVerificationResultGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ResumeContextGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/RetryPayloadGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/RetryPolicyRequestGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/RunTurnRequestGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/RunTurnResultGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/SessionEndPayloadGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/SessionEventGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/SessionFileRefGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/SessionRefGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/SessionStartPayloadGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/SessionSummaryGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/SessionTraceGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/SessionWarningPayloadGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/StatusEventPayloadGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/StreamChunkGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/StreamOptionsGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/SubscriptionInfoGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TemplateGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TextChunkGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TextPartGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ThinkingChunkGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ThinkingEventPayloadGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ThreadMarkerGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TokenEventPayloadGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TokenUsageGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolCallCompletePayloadGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolCallGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolCallStartPayloadGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolChunkGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolContextGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolDispatchResultGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolExecutionCompletePayloadGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolExecutionStartPayloadGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolResultGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ToolResultPayloadGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TraceFileGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TraceSpanGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TraceTimeGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TrajectoryEventGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TurnCommitGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TurnEndPayloadGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TurnEngineResultGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TurnEventGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TurnModelRequestGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TurnModelResponseGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TurnOptionsGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TurnStartPayloadGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TurnSummaryGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TurnTraceGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/TypraGeneratedTests.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/UnionPropertyGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/UsageChunkGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ValidationErrorGeneratedTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/ValidationResultGeneratedTest.java create mode 100644 runtime/java/settings.gradle.kts create mode 100644 schema/scripts/clean-java-output.mjs create mode 100644 schema/scripts/normalize-java-output.mjs 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..c9cd19cf9 --- /dev/null +++ b/runtime/java/README.md @@ -0,0 +1,77 @@ +# 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 +surfaced to JUnit through the generated `GeneratedModelExamples` registry and +executed by `GeneratedModelExamplesTest`. `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 0000000000000000000000000000000000000000..b1b8ef56b44f16b14dc800fa8103a6d89abb526f GIT binary patch literal 48462 zcma&NV{|3jwk;gnwr$(CRk3Z`Sy9Ed?Nn^ruGlsztklcC=e7I2x9>aqJFB(1eyu-q z%|3b`eLzVT6buar3JMAc2#EOW{C^)LAZQ?YaW!FjX$1*JIcZUG1yyl%HEd!6f#E+}*Jo*NafvM<-FbE0;-_L#rp}qdn%JEoAVNlEB#J^Oq`mU_#*ev4HLmc> zjXz_hFft^><#omb;Zer-%wm4hxo!wjuX3hBldg(^-RiOleKin`>KHfL3P*{k?(rji(#j2Cc0K509#>qu=-T&B!-5EBi(+ zIuTD-qfcAYgS@`Fb2^-p)4#o6A3z0&fp?~cV=CRsAeCmO4ZQ5kKgC%0el=Q&Rhd#k zaGmAbUW8uKC}-C0s~2);d{;mpsNBx9rn__66W{AhaSvJEK+c0b6ARO+l(CI7E|S5x zhaYP--@F<|99X&)9`q^2(^-Zu^Tzfm)v|gkTJHQ!G*zIg5hzoygeXZoYUEJ;iFkE# zq^r$*c|>Hmn3GapzcDYnjgSFiO^NFyTR5AH#mh%zRToMpEi(r)1$5)h455DuV}0al z!*psWuL@Ke-2gvftfMEGf9YEi^<{B@qru zINgo+YsE&LN?)1qItJoNhISp-fZ86`XR#*6xcvM~_7=JHUX;K9*=Gu5X~ zix|O2d=&C#u_w{=B$eCpJ4L*6i7={j+{Og~`Emz@&98}6s<-p^)`0fXE4cJBP{>)Ltb>JwcqI>yz z0-r-SEhC@p)XOoh|1|XgjFaREHfsu4dAGVz*k#m+V<4 zHqvlud6=;#QWHUoTR_a8Y8+heN?M%n1@0YLiaN@GuOPNd26tik7eKulTx?mM-R!1H znB6+H{^krFXg_b{y=QeCT~qR3T4}l+b!Oz9;~|3*6F<3?#|DYYW&1RtFE)ILZ!`85 zVmvrZkLTzf31unH7Cc5E0iFShqlBE9hgEnRJH1juII*vyp&xd!g`q}X_6WT6E$hhQ`Vdp9k^<)VS?lj!cTh z7FQcQAVA@jL^cXod8cnhKG2TS9+;QU6Kq>}UOY3&TL9gXbl{Fv8@WsF=z7>X0To@$ zY@Oi1uc|MdJ$>Kn{@!g_e`-I&Tpwfg9cr>(iakDX1qciCG_1y!Di#4_)lE!bWJbrp z5aUonb6m-?tiQyR_`P#~SOu+tb_ev6JO>EbEhHK@KbeT0_FDo>dl9bMg)>xmCNB*g zG5NC8ABavuTEZVGW6jP*nAqRt3W?7Iigc-EE~zpNJXRAE z>`~RO9$892j&I1kV;9U)xT8^}IeV`n{}QDtj2o-RBt`DGZUOO;O*lFCb_vpyGh*;95PfeGu!dyrmZ9VJ3Z*upg z6R-3Lr%_55$Hw1^{+KWx0#z`T7O6sXo1h;m?B_ur`X2bFz-SzDrL zpk^@B<+I6imc@7vip za%1jMB7q@1j# zz{u?YojZMW{5j$@h=v4iu2mTu7IzI|)Sxn!74=*J>1a&?Xjt z2%JhSi#4huEcD9qdR9Lj4vwmfnL{%+vQ{f-KgYeqin(OPd8+(g*Uq#TLxQjD4 zLCL%ul(V&PAPlAx8D`@K8Rc`{GPecQ<)d=KWel0ejFeeXGQ6o7601B!!I@RY&eDriADD6wP6DcFKDLZ|lO#YwnrNCZ)zRJpdxX_nPZa4j#$j6v!h|6p!dH}MY6#B`@%6=) z-HigguDACKBULnon^FKzazF|Y1{t(U5rUGnEU|}djVsWT-F>@@mNx?_$kF51QF4C5 zStKR$^3(fw85(4HGs9{mUTtn1)3PwxTN?6}j;32&vJ^BiPHfndLkdU5sOemXKGyCZ z@<7j(k>DNeo~QXyJkFWk!7(y1SB%nA3{v~P2c8ooKa4auM!el!Q_=;lJ$c5ADqE+^ zX8*|A99v;jWPrm(8=h;2ZAj|(vVbx~wQ{N%v;eYLD_BB2LAEWCs@xauyBDl(_HIBvA(XJ7B1E;O zJYCJ8xFJh7f5sr;Y#Wp_`$4Z_H4e9bGiBp?Qu&2!@%Bl2dT5evfFO*^hLDiBu2%Jl z*WAlL5PaQ7skJa(qVysky}DQquZ8U?2@UyJ8zB#=U_E>MgE%XA$CtfL31m$rATJvC zs@!crc0=128PM=Zp zW_5Czv9))n_8Ru?{pxM2F8^r%*O41}RnONbSj*piG%`nyF>6ky=|;B&k8iot(J=kyoU3p<_zaAX(1ijzf*uXA zZ_5jeC{Lks+&QeFIlmzZi3+fsF4fNW^~kvC4Q*T-vrNP!x9xnen12lZQM=1_MdW76LKX(GuW`%T~dM^YX6+ras|Xy4Qhfcq=D+z-P-ea z`T;^gj3+grr3^hwqcNTJErl$z+k>{bYFm6QV%7Opth?9+>|Dn)O@`7F@=j-XSqGPW zjUAu%b3Er@;j1%RZxVDhI3sakg-gvTLOSV7;FV6ED=(5;UG??=WADZw^=$4AyFh#}VMe3afM^pF zFa}-nM8X=K?Jy02*o02@6k{ z%O!hBhjXlXKdhy3A{xGB<##e|j3^dFv~~%v2_H{t(mN7NVeS~51?D&Ozbxa`qwZ_4 z;C#Q#fL1sua%ggucgIEHZtcY=Ag&GgE|h7Q{77D!WUq`;SSGEE0pU;aoj<7-JCAvf zduN=(tx3Mb+EUXKoax|v;8b@#HJ&Q|!g4ryrl|R>WlAv?IH`bk)I24;eE4NIq@SLK31LD4+w~#3iN{=<`<1R!t^$@K5>U6%W=%8_ANuR5 zs(IDuI18ftirTDARnGmF%;iz+4{MlMihJw_l!0Y)NttXC_t+s)V<EY>=Xin*nGX79k6vQ?beRk zy_J>@YSC_gMIG$yjO-y&o>S6xtfT27aSs>e|`x(f2R1bM}*518~%x>1Yct=18b&Z>GiS*>VB$+i2876zL)1cT zN33g=g|>xWE2)dds5m2+8Vy)m-u@NHOlGYxxjam21r1;xWtT0TgqKZrl}*LSkqFt4 zNTI1=3o%C*!-i;iWnlca$stRdwITA1?#fD~5OIqIQAM18BwO_u>hqL&OAANiF|8rG z_IZ9mp?FA-{Gq9+Ky<#NgL1gWJixfO0ziP$4T4G>vsvqC-NQh+A64F4! z-(t<=AbPSG%`mTl6BJtH~3RmvPhQlE-EUkEoBIP(_WMN zK~Fe!siee{M*ns1hkp5(2}vX#%u+T!Abh=<_gEx_QW?h4V@B>uOCEetEe01tl)^`V z(=cOLmuOB;8&&m%_6pcyrt83UXkJ`f9I&0KxY09}RTTs!l^_7~8$tPA%Hm#&$k0;# zF;O0zCGo0IN)X~SyKDoY1DW{Ulce|V9w=ld;U`z$t$>8U!Gu8V?_LAJAudt3eI#*! z2i9~F=kP5m>!bmb%1e~b1!1gz01Py(Yw5gOsFN#o1a&d|=PpgN(#UVreY9^99I0iG zaYE@>(C^V7pnoB~#w$2C1_TIb1N5Je&iao?S2A*TF>@vpHg`31{uk<9{zf_}s&z%dL-Fo)C$yl$%pAdqU!HJgp zh_{m1imk{&{ScyeuziqZHu5cto0{S}^BlXu% z0~;>_yHGd#?Kt8ErxK)z6ojj5SacQobw)-8`c!$HOI*V6eyqou{1Upm%_p!BY^t(D zDtn(oQ!jff`ddGSD;P8Hes!v)OKW-*>mS&#i0ow87;h>(=Cu0>b4)|=EegbN5=Xkh z9Ge13=3z#sk+fT<)PuUUf_%Nx@l!P?t*mni^94p^Ax6b2SVL5U>9dHH!H4DL4}@?@ z?Gpq$C**OmWliYA{5s<|EZ@QI2{-K#brFxfA~AIqq&-WSALHWQ8}%mvaNFasrtnE{ zg=sB4-RF!?)nf{>Wo~kNFgYefoFHBcSr*;iF9B!R=5Np|jv>Uf+mcarG-XGy*kP{z zISVyoPcl_9cOg-@613Qx16OGF#sH&2NTHDa_}vyidmxS~pMfY#AeQvu?AXpWNzi7A z*6&7a7!C9HRU+N{>WYTh0GXoBnXw{lQby^XShgDOw@e8TP}9Y*oFV4MVF#@Ds2A+A zXBEt3a@-IIl)TOcXx;0P;|ihR%Tq@DXeG5p-O{!T7Sg$s1 z8OA4iOx-!>6eK^x{jU-0SvByimK|nZik5zKIvvWVGE)4=x^&5Nx%Qgje!k3VoizaB zip#?$u(R8u{wUFC>tVR8oA%7fs?xEu(gYn>y6BB%vwPR9&RoZE%%RK! zl#Qnkl^+Y*Y4L{Xk(YX&aGj|zSpqO_;C3CTepA!L#4EXO|(eA`Fi+2EQ3!C zo^SpVP?{chQ3uaxu7y>w213e22cdA#l-M2kStPE%sq6vE4M*?3At!S7tIp(tQg(Ml zECjeJw8)*#LYYk_+Txv3rxsH9jJZBRrHp29yJ(^;_PEdn%#U1q`r89}38;XeF{ee& zsZEsUbJ{LtwOjU{vjL(Wvs2!Bx;#^Mzld&TjS@oo3kk=0P36MC-Ie6eHNN&{8b^s z0@jcbdejrrj!>r#Wu=3H1dgjeOI}NkhmE}K+UK&M>%7b!n&{0Zixk%^)6#@=V~IZN zxG>9kl&STQth}qScidfg58d2dF|v_U<@+V^eE@$4x;7oS3)MvWusA?9+%rN>aY#eA_6 zic@S(@e9$9tQM-&-7>X8~#n{5G}nuOu=dSyN+b~jA;_SExZ1H9Q1A}}Rz;XtXUIOP0~ zZzS|~T+%de-nGI$s?wxaJoe+99vmo%xm8o8SNEsAqAE)4LNvHc-1AX24C4k4u3vZmov^_VcxgGxapV(8)_K(^8= z2d{xCrmk(x&514Ly?e{Mf6}h3=oeP7+ZE{%B^c-kK8g0W{tYw3q%zty_Rd@1nbnyHMwabNp-sSyzpV4v>QsnKcQjF67%g~n&3t^1MesVxCzfJ5b=SOI#YfPP^^JGQw=9L1RCMFbrU{8O0LWOUdBK#j&{`tzXX zpe2_{+-8$a+o#%8MUlL4$yK`*--z&3{@Y?jP!m{g5nM+Ht=bD3o}Ok~sBQ_!^!->! z?NDVtyLXzmGYCEmjSCDK*q?Aq1;8fz9l9|z@~l{)R6GfKELc^(nV+TjjI^n0M+S0i z@YOu*Tk>|M6a0_n$(E;#^1Zgif<-CpYiMvyT+Y*9Z?&~IKSwsLa5Q#p_?FqK3lKIw zlp6Hk%lio6)yq>m-`QT2Nj-q!aX7~Hlm^Xh6FNbw z$#ri(Kk*GUHXORu@`aYQU@ zB~S-oIO^~abRPocemkm!W73dbb!j^_xgo_@#W#6p12>w^{){VfeX?U71Xyn9&E zHa1#*!4c;?r}jv7dMN`g#&R_S215)dccDOJr=uz%LIz@zia+LIFjRakROr?P zQ|Xw0Pa8o7&W=fw17`+SqepsQ-Os5v3ncD5|N?N(AHH&`>hLY+CLOluJ z_ErpaT49zK(UcdNmQ%iA-`jS`A_1c|$W86{d_T_T2V-HH3xUqpX0QJSH%i>1i>#vK z&y{;5)^pMB=u;&_DEWakQU>j&+opIrBf~2GUh{`kG{|Z&2Z}5dwG}>Y{W_uQHaR$_ zYH%}$c`CGC-FGCetRdQ@RZ2-%ucC_|R?mHzYEnqC%u9zRBH8wx7po`=EVPMpq+hL2 zTdjVhQn$)++17^cn;<3=bxJy0Z$U;i3AqJMPJO&SuieU&0eVX?eLEEI7Av@#PV_ZQ zsa>I>B5HE996O$z6HyJfhEt^aC><@AnzeN`xs@lv>^pPFtcodrcGyqPSB?#C`Piu0 zh5=hAW|OtT9hs*G?7}@*mG_f7ae@-Nz4{qvne66kco^uD$(JbCo2ttqUm-SMy@kx% z!eDt?5>w5)M!E#C!b#Iu9GqyhUs|QoYWHtR{4espRS-LUt=viY2iygF=-j3kcU#uF z{ka2=zsOuLR}s;&PbbrB`zty&NfZpV*Y;~i*W$EH0JOGS&FMS%VK@)f*%OOrcU3P9 zq4zjhMpx}oc`PWtP!o5Bdlp=(A***TZwVwuZbuB1Pibv5uiHvW{PsE-k5IfCgUz~l z0nMeZU0R>(ajoQ0G%Il)z0BgRR*bsdz5NcqJ<)niF6|PUO0i}<4)q>6wx4K(5>Y_I z4$WMkbCOQFs(krBnl zx85i0*7%Zm(&nKNP?AQ}d~6@?D9dO%@}ouN2paSR;zyUqJuw)1SRy=g%o;g(BD|Bh ztnKV(4fcBgDJ~M@%}n-6ow3xOhnC>C^d?PbS(9=TnO)k5p+W;pu2F4eiG7ts zJVL4M(NiZPQDy*9`H>-P0GWY#=UTnh8feiNF}hCs`8^ZDKy;XIL^9K4Ps&y^#DQSE z-?J z@YOQ9NQi>ZP>^ix5K`R07kWj?`R(B?E*OyR1$Vd;8p%2Y2zEYt4CJM~gVX%MO(E1B zzXhsHn~R1ifq9~dtzuH!*3&W;r`D(Sjrc)m#EI%`Car;CMWcU0c+0r?O!)HpjEvyP zb^;pO-Bn6e-+>dS^o{q&8yEH9v}vuXX`W;NPRlwJdX|59`z?~z{pFE!^u{3k{KkJ55^ zD;F0ldy9W*`d5YP|0(E6|K%}9|D^SIq>wO)4^cJ+yCa&xl*3}hpvcQ1eP_k;@>tz= zOZnw)#fxHc81jPcTM#)jgy|0?n0(jd3IPu-lJ&Tm`#F1)o$GTwYp@dlqy-qiHFCHS zKgikMUx|%x=_%B)>n_y^+HvD2=nP`}-G_0A7)I$yc4`tXS-On8qOkNp>Q^$|Ew%Jm zYx34*(*Z3SF}xw$CA?nG9O3ZH7l)@Dp4EyH>8eXDb}AFz)k*T53iA~gRu&e15u@|% z9Rw?69nQOeJhv^^unjd-VGFwbDzf9K{i(U{xxHyM@-aI+0qP{TU0G~w+Fs>taL#Ik z4+92(Z7n%+okd478;__0GkE`&(C`k8h@?UNnM=F%A~2|TKo)q9F<5`s)KwxJRw~k; z4giS~|8AIVG;rde6I^W6m9fliR^7YT*>&x7wv^?xu(5p45n{|2F>x%?9Jq+~Tqo9# zChbeGm@9!(s;uIKae_4h@`~yIj`Tqct+-M>d>~2PCiQ?UmFUioyy&~h_DTBQ--W|q zqA^UaJMTz4tEggQ*_cQ_LA7j7bLyz8#cpGggy;YBVk!%oSdufoh5-FYAQ)v=d$Bi`G$^~ zm!O;En#M9uCykPzLZ5SHa%?hDHP5P;T4HN0L6J*r9DAvC1WWPOrd{*obfr3yJ?Kl3 z^_6dnXRoi4<$Tr!=4mhHg6ig~BatHR zv%ZMJr-`8w_JyFEzUSQdp0HT>|9QQG?IXj$7Rbx4E)%HauDyY!tedHP ztIbq;D)ckd-eirAHOG7icBH23*ApHA@nG*Jdh}~G?L5C^Xw^+nLWG+>hRi&(fnpY5 z?^hj4si6I{m1u^%i_yk$tco}28X8|}g5*tAEZYF37$f(+xT%XvO^`i^Ig}%cydrwF zlpL!xdO->&@q|8MiJrAxt;z2CP*a+EvV`_2& z<1=p{zjhmmYVkpx#RV=#zuy&7^2Trn=H$nT{OBVF*0z|QH!NxBF%gbqT!BEx zKB!SsSUwSo1Zr?kMM%N)@hG=&m`vRQ6QK6=oIvnUI+|C)dGKM@jNwqG2Xi8;YCUHYRh? zbl@DN-za)+0F9kw>Yv=ioL)01uFp7@AVEB0AH-nmB%j$RC_totFy4BKd;OPCMUMBb zu3oUUK`|{AvkM+@KPZD4Tn$(VlQi&aWV*Uf@DO|FQjLOoVw&C@z~Um*h%Ka-C=n4H z@(Lf&MDJXNS{3Hs@J)11(zo9tGp>wS^b9{Q1WN=Ktn>ZieRZS?k`gb7P4n?cl^7^* zG5-oARAG#i<*z`J0ski%;QCLD-T$AbOHq<{KxIb4=QJRn@MGj=ns0WhZX+uX z=oTjz`o-VviMt1mB0W1vA*7oq1ENz{<*-EU)U;r*ODfV!G-?hdnzhM@rRZ=|qaFTN zX*t~$gc-)M7GS{#34R-n`B)eAPfebN46~61R?j^(Pg3TXR1PyQrO7Mf@xf<3VL0`4 zh(i?-SktJu8Oj?KIy4p@%5ZH;P&p5LB8 z^}7P)9h}vUP+1Hd3nNzNcbR`%1>dSZbWhiXe-CcB+s9e)_w<{bypZ(@cQT`P@ch=d zSOPhExgI31MVFPsClEXe>$~qYQ+d}7(!BE*9y%AjQ47BMDt=#>`1ie)|ES{pFFdHa zI)CK`f3x>)DtZnm!f5=e@g;3iK^jf!RU6hpjYu^V#q0uWLuJ-6={Ua3gDi9#*P7;- z`rm*5)n{2QE{UZ01PVy@_9(amogzzOwYcVgp2>LsJ(}hKbX_!ayZ7=U{!p{BHussVj(W z2z3$zu7h$KK<%}P0YBJ+)0unV*xD&6GusXqs=M=Cl&fP@Ttzfq?>H9TW#qDId+C7? zhD;;HOxDJR4dc_xI7-b6N6nZ@bUWueDk<_9Rju2I*o(i)M0&~%C^ zc)a<25M<^NrsjAccydV2HJu_-1W>b;xrB~Mi@c7FrW-94$-GnKXvF7( zA68!d!gkIo8(URS{(u{zRtrF}B$9@*)KH9POqOW-B$za4Sg-A&PM*on$>$o#L7pH~ z&YW8oJX3T!!@2r4Rr6ac0ZDbtB1b5yc$5}7oZSDvGF0FWTpZ#r7@GfM^MmC-p{9Qj z_JmmlTxO(^(NHqBc$ECU$jQp^;)%xnyr$qvNTd`R@j$8JppDCGQAHQ7?fja9McCUZ^;``VW$1+G#=<;K{_OfH- z_$fp~S3K`;jPNNZnkB@=DFQy3{6+Bq9nOf3~dr4q8zD_t{P4-^%<4kj!U z0aj`=#@G*w?!4fpM? z8Pwb15(Ka*TtDN-2aWK>*hh{R_C}*e*vSTkHdM(ETM!JrJ=1h?(_WL}2p#QXjrKZ_ z0k_yu^;~)#*r>sQP7d_4VBRvWJCzw#TxA{*hktwQI3ST{8{>3$KHJIgMGK6I!d}Q zinmfq&RLRxX8P)_@@vVr0gPu7*)uU<%xS{|Eg;*w1}2=C&?7B zSX?OLt-gZO+<4@tLeF+K0~*|xwMD__KxWgGfsUpj)KyeCM3J-f*uxe|xk;Dlqq%1< zL(PaY@U(>Z#k!C!B45JlmE^~wHSH;r1c^kWTG9_VT~1LN6$a6Yg@kNF?&b0hs+5Dw=0j zR(wcEYmdfgojx+Hzu89*C}4$I7^?^vYKhF(`>=MC)VeeFR}}?j#XeLnp8OhW9%9ND zt6utD8DHnQj5@YJv+$USdN{8apQir2)Z{8_s!BABmG2O#pz5lSh|gf#CI8X4I|U4g zhQwk=VEV+j+-KNxuIk96Bi%^(Sf9}A7o$zHJ5mV~)qP))QQY&^>9}z9z9)PWpw>8T z7#NWNEtnUoUl{DP5(lmy<3;tpLJ3hG|;CGB`3**uH0tf9>;7w;Aq9SRVg1FDpI5y~rY#B|eCNpAXD z9692@_%$t2^nu&4lU~(~_iVf|Cs|mXs-xKlY$-~FZB$!oDK#)JgHZCG)ySDURM=@(i zCpd{Er89|l&)(&5>L6LuWY3yC6)`jPz(Po8pY=AYIBnx3y2Qx6*sT42mpR$zwx!!< zHHCc~tbF^-bje?bo#~Q59Dmw_-VcliCn^FfI*EV)U1NkNA`6Cm=^%j`%M?1Zxa=1U zn#DPNc32&XHHfUfmPx*J+3_GA&g-_pd#wO=Q^5bdhzmm)>s@yO0q|>ROV(hkhJWf@ zqWjI#+9Wx%C+!kp&kxX|XPS5m9CBC&3r>}SwdFd#YF_W78A*CN6mFC)qzOjM);Z&v z#MjdXXMw63v*tbvY+$tDmuHNFunOlRM#qe|eV&|$98!xy{n)-=N?lrkr0_}U^sz|x zs0y);(2Dooa;(9zHzRi=I{GSVcv!6jl%ck@)>JODfR? z%aI)0HvbhzY9K7eYsntq#JvWzj$WCuoyGoPY7;LSPfZlFiWU)X?(-p}s4FXQcpIp00;%Jv;k0t@2vBu4i;rh-?{z}cHTLL9Rz zT8r(1Ws*H~EyH+adP$cGv|7HkeS9p6eOEI*`idH3twkEJ*72|ey4JgISglGV0Vo@qe#)f-=|g%l$S&Onwl@mmdn|sjXXYaQ4MlfzjiK1* zY&hWQyc9?G2}2s1fYnQ}LXpq{!&Kr97d?=a?_xXAU0SXrZE?T+=9os2*v9%Csph*M zW{}m4+PIRmHEI;<=c5$PMrfg#MTs);4Tb_0**o}*cimSWRcxo(;G&&NV+-?W7v*%4ACG#t5J zQP=$g-(mN*;B6s)d9JNkF0#Zz_WA>J;{=2a!IJsiqCV!YLjJ(wUJ`3b$>qcZ!HjDT z2xm;fMSbtJ|3o~tc!jJ+U8a)vX@NcxU8y#u!Puq%R~{sps0msRFO2!GM4}786S7* zxgNmf{q@|Sdnf6_he>gEGX7Hn)uih5nL&&t4`O{?V;;bdl1U~9RAnjNmt~1UPC3mh zrR8ZtHzz1(yOYSK$OjKf;InJ+7mH$WfqI^OG3dhA+S!YmIgRv>2H78?<6A=~%E{ug^P+^b*+f=j32&Nv&Ypq?DcH&Busg^AUDE|p; z8(tQxZs1+0gUX<5~Ah zT0cGckI5%nM~d`uaMJ$o%2bt^##I0UdaQ2>-bpsP4P1Vk8r7EOSr+a!D*Z4shiKFL z35Lvs^i;#;G{%ksUUo8(Nj2DY?u5->J8kqS_#{B`HqS(UkzR|K5&6XI_#FH4?$ znMXeTb$nmr1`|{n*#5H1T%vtU4-H)vrtAchme!ZG#@c+Hrf4uxx$;VU(Dr~N-ich4 zMKpdwot^bPY#kBILFgi?i3W_kV%vn2J+%R5x}TL8I?B~o#VXlmr?i=y`yJi-><;X* zPCDrsU51x;mkr+t18lPs=6)r^gEh2$saaA!qv_< zKQP13J}ptHaUjT_(*x+P}wfV-}57aU3rp#3AB&~e3%y}0ju#22u5@mUIT!GA{* zd%-e2DTmr#$(P6^$&N0oCgR)F9IPR~!Q!x6YI*7dx6LR6n8tj(#1~!0rofeMtT#g* zW%-p@V09>&o>iz0j66K^soJWg(o9#T(8Xx-P3?;J|t~nIDSGPq(?-B zOoNnc5HZhsW(m6!J+yj~kjmjV6GKvhO>%^v5`O2I@4B$Z!~DgelYWdC4P>YfmI$TR zq`atDEhIt5ua)PS;Yz1`FX@3Na6j^uBx_rNKTmgboWGwE6O5;iQiN6Q8>ZX%ApVJS zTEf6oj=@?7klS(JaijG|(gO@dTgxB3#H)4&?+@VWkTc)dl;qK|uv;WRI*cG2`6PiF z4+svy+Bfn&Fs57Jz6i!C(w$w@VWPAbRGak~oN>3vUg|Mmk0NpfURt0*DSJ_e*Gi8I zqshW4F}L&aS8x~4*#{4vOc`gKW99cx*L^69fgPj#?++q9LidItd}<@&#E{ZGz7g|c zFX$uKJ;Qv^NpN*e&EL;l@1br8j8oxO3e`g<911L_jr~Xb0)t$x$A~dFay9(}gt4&L zyb=1<`|)_7(!^xJ14xLBGKXO3`R^_;F01 zG70TiF<5(=pRsJYj!^XjLl_vFJOQPhN#Pkr#G0-m#xG>q)GAHjE4WFhe7Zi83;gte zdDv6+)qrgh3F0}$gPmtb9-Ff1m|xDD$6jX)Dcd5Ms-(@nKM_3)2+hfh6@Cs@-=%Z_ zIinf|ck6rN{EOadGmJ-rzvxZnAL)(mf108HL2v&m)%=a*?3CnX2ZfOQY?ha_11m@UzRqlkhrVbQ@0M(tSSTerx}IH@Dn2={w$iGqU#`v}PuV7I&A9JYNP%sqMn z1bTq*Ok{V>SlVH8H*4X-lO?VzaDQzAaLvc1tTL+To)YOuj^V8mQ?)K-FT(s_!ds-O zeb$rKRR-~g^+_aiGtH6kbJ)!K^ie;ipJ8e;>iy2}73i(1RY-~!(tk2zPj;pwB4k1a zVa~7lF^EE`UH=#eb**88zBH%!WkO0S?_Zu0KpRtXN+XMsAwfT56IZI}&cs+R5N~p3 zlQH7o$(zsQQBPIRmD)i>TfdcgCSKbVVD;VCmO3l1VNbV&rWc9o>Pk>ex!)Nap%NtP z&kKIFMm@k9-HeXj2$((SmG+a-dXvl7q(7n=8)cELHf!@Le+X)=++(}pKC*dcns?>G zVa*fV{2FDIJNaK_jq)WE9MvxiTm6sI%YUn|S=oP0Z`vE#GMZa`4V5byxmv0@8@Zb~ zyBOJuTAG>Im^uIL@!ZrWJy6xL{%n;pEwY87Y^xYSfmmgRcgcEDfz4TJ#{;n|g>8(> zv$(RLnp4oD1Mj>H@ar|0RCy}E{GwvuKOf1FS}O&z-Q)MmCVEK{p~b2xFj@lTn}#s4xg7h+r;n$TZDlT2AXAv z7R^$J?R|*xL^>7HI}e>7{HszA#Y_e8=~8*3zy_J$ejuhByeI0I!w-&%MW7Q-FGMKU z8qPm&IdU3w#^#`d%Vcn&q^w;EEr|w2F@ax^`R;a@p>l`U-T%~f&^`#zG}qdSV)A<0 z^*U=#=#o&gd{o+*s#j$xf+2y^t1Wj9_h}(DNi^aK#jI}z)v1rk-H)gocbgc`wB*?$ zfg~22r!^VEN+n>U8|3{Ebe#!9k|dF8lV*9c&9H~&g|$Ymc-2O^j9w$Q^I)ldd}5zv zQkBFDS2TxDn`p}-{-`br?tUCgyfr0Wbf3QeATbp=9sN|e90U^eVOu0~VT$1A5))@C zPcwzUn7bP^Gd~hLA@8EwiklMmlc^(;uPE%tLecC-iZ$_~jNJnZYn1A%r}=VE(-LG; znh6Q+b;zKz_N7)0SH7t~u#)e>Pr194w7xp;V&CpmJw5j6zBO%yB zjVf*iveYaWlrE~+p8YYym=-QmTd_F!`)ATishn6(oD}hTE2AqnVPF_os`ca^ET@@Z zoo~4YJASOBn<;8#(#3G>n1E)&@JA^3LV7mK^kaJ$((~ASWup3G(%#8O%xFX8XSiN~ zUF0&gDyT`FzIjtA`<-+9RXEKbwu%RtcrG!#-aoN0aj)i z(G|=#b_!z{o1}cIyw#n=j~Ac|NnR@<-CW$c%JFBFTi5JW0BX#4k2o2w{L0EglSN7E zFUcmFVF&U6NBA7!t`Lut>faDk>pW>Lz9BSzsqWvnI<+L#wg=zw+aeL6=70S773#Rq zG@fVM9=1ZibB`>L>hKz>rHG}`pX;dZD>I!_x~u>jsx3;0d$`Q%t7d<8^lkl8w0WZ3 z(HGiok6h^#G2EzIH}G*;!U8FW>@|C+wE+z{@e{wwWEkzUEiT0aDJo2JwZR{zcX$Bz ze2pzE&vKCc6@vE*GIv1LZ=qSg~HR)Jf|ljt#^m2hZF4z|32*7{hd|u`C7{C zjG>}`{SC3Dnc~5%D4yBa!V@}xSBtQ$ZWY^qs3)9jTuIXYMgPF5E0*&A0B(=JEntcVgC%ZO4UKHyuzuSblKNHWJ}OzVpeS z?8|{P8FtkJ=~%YMf1h*@o-YsZkLVQU!43cY~nWEmBt#&Ar%7WClZK8 zSe-!M)B8((tj^wSIm3?e5oe&mQs6BAE#Y7K*^boU^Z#aITL%-H zul5Gx*FKM}n~RnE*Ko3}nXrk8nTw0Ok-d?{|KMda<$n9cFHzkfb4wa&Dp0x>XjayP zg-KZ^Ayey*gb`NecHls@$a-2|Z!Xe^@P`uYYo`Q*jKzDQGPFf^GDQ5rd(-X3n)&f|bD>?`-DktKL<0hWK!cPS>L^@|VH6## zG*0#NtGfzpZpt+e{yL@K$|Lg*JfO%I+hp&kR;NxOJ+y2H49xZA7=^RKObPZi6 zL&R70!l_{PTFcxI#h+WsO^Y<`hE*z1vg9n7nG-6n0xBU8F8yDd}=?${Kl$qim3(S98@^W*vvSs{l zU}!oUIXap-i#nT`er(?avm4Q4-snuM&-cwu#-M{K8n;l1gP$ z3sw?`ls1z%eb%&mNBvLuEci8}-Q`|kUw6;F0-pHb?+A)+BLSn7_@my}6u%J=Ub~(* zU1n~wcfO|73IBZF;|Bhy$0FeO^>lmmZz?ZuZC8$p6<>B{Lsp-*mS05IVU00ergKWv z(LIsLS=?(>QLLQQ?bdTpyO?iiEL`;>(XJw^lA*7FCd|$g@c3VRy#tUf-Lfs*_HNs@ zZQC|>+qT`k+qP}nwz1o`ZNC1_y*J{2=fCentcZ%LwW?M`<;L&dcdwa@4GT@LCkltq=Xfy+OasOLT!lXrqy` zEW9YuDcfQtJ$oJ|Ln|b|q*_a|YPgCbBBfQ|5;-1(P3R`sK~3T`TtVV6yrtDbioJKI zPDV1BAaj#O~V^ll>$# zNC?nv_r5RiH^A2t<)qzcvns9Qd$_UU$`jN;KUSNqMCQiCFCi3A$*D#(v=FXCqz$SB zyC8vjHyJhMy$5kCi}FBy0NdSCJa6{q(|*9I^zwX1NHX*dHOIDB8bsI3_{(*-kkQV@ng|lWd*nWx!(xQ1stGMcRDjH=YUQvY2^uCZuO%-0Jw5az*F1nW_|h zR~z5DT4j&Z7527|#z9b}pmRW}p^|OrU(TWox^&Kn>YUn%%JlZJ^16vzy|O|GnZsf3 zSXEMjOhuYZlh*ikE0&zHt5va@6&GI{1&D+NPop@Tss&f!V4;}nqX@iOvdonoDa}J_ zE-u%qrrUpYVYSGU5NeXJr?#B#3dkObD8uk*U|u*zS;T2YgAk;_kdF0s4A6A*YGO4)#dKwYLQi+*i=C3N85d93 zAe#Lng7EX?@}-FPvIdp0y!`J@^1tg|IHwZ=C-i6LW7u!d>#==7<(?=6?caFCo;)AM zwwV6XHIU7}%D3 z75#&7SiVq=f6k4N*gy{?o~K9`+fsId8Co*62ksPHLm=SB>G)@44I(Fbs1stfE==|e z5WM)k7Hs~OwT#*$%<~0|BEb_6HV0F0=kYy;P zdAZbN(@{*9FL}4bSi-&#J^2;N`G{J?KFD@i^8BEXQq3$Q#~shvw_cx5r%ZlgHz2&Y z*cU<9UD1(G6qg=Yx{LRix``xh^Yi7@j|r7hm00t{(0ei78ZQbt`JV={$XlXvX91YH zxbI<;-YQG@9xrY>Ar~yWklR>hQ-X6TUxD-S!;~b9lu;Tu@f59S=euifnkTO2C*G;S z@TJZ5{$VG<^ThBbq_74=9q9r7DxC6VBngr@olJ}~W87-NEagn(;M*)7Oj2!(TG+}U zsLu!TV4B7DH{}gtanAHawLkpH5_$jk$0~;0`rM1Hjkl;4D-KsjXTl<*z|E`_8Nlb6 zroi&vNu(socja8wZ}9J>;D}esqgs4BR?_u7ZyELz2k%GQjtG%Vx+yeS&QI*AK1Q~e z;1-8)WjT?WqB>et(n%42u5UPI+!F^B7Hx#oW{i;??}{9#vpvk}lwvHPB$=-+pnIAL zGBd3sTO%TRGFw?`Nh>DzU#VeO7C?`w!-QT4ZgBE!WsS1clJ&i=m$ zHn^;?BNx^_wESMCsSKfxi542WFvUJUh%GpT-JP-b+D|wh`H$h4?*AT6uKyK)=>%&^oOXr5Al10+ld z9x<66pEk?hlV|$s!otJ~_Kz3DcB~XFzWq<@HMwvNFc2}VQuS$6g{U$+nN4G0`E zua0)-H1D8k;mm6E{(!pNomCz*qxv$pI3NvG>(+Q4AcJvK#K8 zb9SOKS@GC!pN|JW#<}*37GFj>D1wi~_)k#-N5izNy0%(q7hMm?oL_Ju8jMFGA9bKb zv$!gbC9lC0>Unx?+*3GF(6ZZH<(4j|5-Om02Y2z2IG_&xn+2Z`6;N1An(~^lQwwUQ zOiKj)?fuj7EGlb8nv@wDs4us&o=Bt%l*TAhB{h=R+Pddpm83-ms{V0T&ofYt=D7dS=Kr=V{~wzR|1=j_+3Fh+3mcp0J6k#Z&$+yVt*OJ$s$BYK zRx!5u|IH#%N;9@dV#r@$o(;Dy3GBon{2-)SK+R!>`0yL(nq~lFeelQy_)_BZt2i}m z8rSXb0|MpaMQpG<_IaUCD@=+=`KtLmC}H1)-vV;8Y!fw&`K2B6oou$QOj%XL`Ye$dX*5~GV? zjoCc8{4m*B_lFn=K@#mp@(*Vga>;sjA3Ds|(a_aGGbuFi)9-z>)&hY^h=PM>jvvAt z$Q7Zfbr%lPeu2OFHW3uNyavs`ezAXnB`OuCGx+U1e%!gwF?S3T3XLaG+BzOfiLB-f zLsTI!R2nT{#3)Z+EHpqiKXE$CK-~2S!*Tvgi)l{*o7SZiuHQf&N=jK$gt6|+nF)`Gm z!Txq?dNfctW^}=z-436nDud8w974=Iuf~cqED93ykXqf1w8FZK9fiO>iyHhGH6`Xa zy99CYP)x3@)FSqPdVt-Br1$H%x6;EwpuBzZ?#_D^RUI0KPMzf^_Q2rPhK)0jFB8Xm zlV*;2seylEHqM|s4!E5>k-zx$17R0R2*LcwM(ea^%K>Rf92id$mc6SChy+Lhh?+zh zvO6({dx7GOFjsuW1#TIks9C3Y1NS^K;IL#Bmt5WRAnNcc>QhlO{Vj2vmon)s*asQd z33&IEDekAAXHibwHHW4Kjin6FB;UgbL))#+*%fRgjq!Uy)J$xt^A4P* z=wpGU$DPMXW)DL%DW!nu39E+G5tKB@YM$r#?rOf~PwEaIWOZ?-rZteokPGZsqWYS4;B z|0LjjIbp)2Q9#;HApIi0rAAv&MKYgXU3KhsoOYe|YT)zr{({<}EXL67@nFgE$g8n) zlwsHK7H3m?1l)9j7MVEeKIFU&$Urel=||l_I+%2%vpEWGJ4%Ae=4~9emV-GN((dey zu%{X&7)-JZ@$2L0Yqtni7;-H%fWs%8= z=kT2S6oOA<-_q!hTShh=6tYB`my{cf^+Lx>yzS~3hAy^=8Fn4^M9*a;F$7-pPb`5WTTi>BH<(hQt<2d>L}bEO@qeR~R5CV6M#}U~hOs$t?sI z7o&N-naKA!$TJ z>&^XTo(>zGjv|b*XTI$ut5?7&&KtRH*Xif1`>gBEp7*Joo(B{{&6%EYr?;2euFLC6 zyxINGDCvA&Z9Ke6+p?I9Q!BMcUI`b0h}(?yqWH@VsM zQOR!?^5j*fLK3_B=$34i3+r{u7IgD)M~W2q7y3L-307k;BupXtBuqlRxD3=-rhwa9 z?bS^@iS*Hnd^;p2cOp}nC~VDSN?;3$3z!yI^$)`1W?UAhtCjjqn>M&ph0;8EaiL{z zu|C4KQm1Ko&6~iXk*x&^ph_a+*qDsevtmcT;T0k>1Tvc@2_|YU#phijBjGm~(FAS> zlUlF>J!lV+cX^mbgNt|q+%c)}o#I2L8tL)BII4PpHABevx1oqq4Fk=enLf)lPJppehzt;iO9UQ2qK{ycJZ}25$Em8#QCj@IGeY)Ih;t1C_j5#Indn9> z?q%Mr*&t<`FGYDnXUw!Q9F(&(vc=j2NyA|}`{O%(aBk4&ic|F*CyG^zcJTh7Jbkku znj-MdZ0aPz3?=kXncCW=-<;dP;J9T1y-C;{aJj^)J(P2N6H-0wO?ZvS=U!GHKVCK< z=aWv?u%5>H&8MwXa49`eLmGW<%;nt}*#2=)K*`axE(dLvH|fGa6F34#8tRY?cr_y0 ze3Ys0rp;JgADiP65s|!r+v;Bhhv}`Vm{n>M24Hc%zOJ&UhG2A;(vSJbsM4>fU{u2_ z-6VIhEcV`qxROML_k8tmxBr)-{ z0Nki4Ka!>@`U^UZ)eJ*+dVEKh%hU52puWKbEG44AD>zWsBPQobQCa)OTlz41wS`U5 zA(_e!#MIkQ_D?<^L@2G~TpSiQGc{2i*D?M}9=ed6<%52)rPN_&_Zz}kJyQ*xrss+n z+*}R)Uzw_8MN}8>Nin$jkrHrz;R3n*HT*JD&M9fIRS?wRHq#A#i(f4q5+z;_5Ij)k z55fi>(u^$A=GCiS!o_k6hWVWf;@9>(C^LB-^lw%JYn+7v`}UC04jw=#dbI?>PxGb< z^hYM;a|^$Xv8HwRyEFBlC0EGDeVFD zsI=F15ChE=aHP6tL~Ao9#WHh`H@ZcicgWiJi5Wg12JkaFg6%fLuw^#2^+FGSBYJC) zcLQaBfXhJJeIf<*h>U>kVP9*cRCfKc<$@qO~wd*)<>-)SK6P zJ@I^4#us1Hf$yt#&=?VaIkhDY^^W;!&OFd#L5S3wEK(42b#OVRSI3Yn=DLC>djb3m zOx*FMX7ymI4;B56>=L7Cv?Opmx_j#kUAIX{b-S2c8Z$v=gOMvo?-ij^Qg7+-IsiMdRFM)v7G{O9O zb{zD!lmDA*H)}70ZFQ4xTkLM$F*jknM@CK!9fA;1rEyA1T;kT|rRhl7MQ@3Z8K3<$ zthbXo^c6w1sy3usEhrD|+wtJ{DqW>!SzzMAYG&n5P_48!FI7^!mt^UsJ=Ii%VFz|f zC`{_0n8zVxPB%8P&U9wpG3=awF3lq(pY)ZY+X0iPX>u?nXvOVKqHlZ!kPr!p?==9sB_~DS`Wz) z-C{l?ZU7>v`xhem*b=STWhZXwe7a@WUN>CeYu(sj2^yMe+X__p(O0XKfx z%AXEQxVFsfTzy)ozm#eCQhr*;4iF$jVCn@40VgXeH%1E z29UQ3y$aVZ3TOp-E~*g`Gz^slv`Lf|RO$MFBa@P)tKRuI=cc?XxIqzmXgmw~OWv_3 z79M~sk*g{jtNxD4ShkFGO@d3`N{)-(L`+B$P3o{T)|L%BE`c71nj=koezdtBY4~a%t^5r3-m!3Kj%V`9dB?v%w?BxOI$&~!jUNWa z@o8Q~I6n%f3*aDLLYK<|4FU2X@*``7jnlDRq5+VebLwb4vJVL_1XDYFTUc;$dW3relP0}p?81NZ&{!uRJU{&9)O%uEL4Mkts~ z&T=;)Kjl_c^Tc3YX*8y9Lb`*cpyU^wFHkn{Z--k1SA~|n0bO2_YwyEVv91paW(>>D z5A?fn$`0!!94mEWTUFmE5+yocu&wZDj;aE3+jOFJ95*T%`pKWaqKNiaixt!T^#`@p zHlA$6Fj^5&7!Hb19 zHyE9zQWe<12XmH)8IDIOtwPeM zHRd&LKn-qMRQRtyy5LYzR9#*8JDBD2K-E^^INa=#S{XA+rW5XKtg>7Nn^Of&Vhir! z+P>KycTUF|e~Hw_vAX%ap<+u9o9)jcAVaw~|4zkmS zZa8>nl~i|D8zjQ^%<{;ZR6cbVD>%?nlBzUD&(9h}VOpBkVW!AuVW!MGuz;OfTWE_| z{yi!0mE#74$DH%4$iv357s-5PS(g3aXJUS?=I-+Jz4Y{Czu2{VMepL1!wV0l8b0k) zSH~&|HJ~YYm{WKY&gKO*WNzB=l|JE3C?T`VIh$Fi$wHFx68QWYRy%ziF%z4Zc<{>B zjkGSyv*i{+F*O@tKQ!EDM%7xw!z{Yx)~Woo$kr{Z7+t7ve;X$MoE{R-LVe22TZY;% zOIFYRqSw}4;Mcno^z?O*G8Q`&wbgNV%>E*DX{fnqK*lP#K0dvcU3endLW%GugLOH< z>Y{oG#ECe$UPvO#$t@?@GA5JFE*6oY@?+$jRxnx(BiZ8q{AuRkwymR+;{*D6-bh*) z-5@PC8lo`?K**Ec9*n$U>OJRjK0H$J@vnMoQZa4ti zMegzJ2oft=1Y+aEG$4JE9{t_I{tH*SwKVixk$IyL|hvQq*qu&_4C6X zp>36)v+qAXl|OfXL8koN-RrhNjjA36)N;pjmTkOO>jg}c>35j<2gH)fb7QYv#8VV2-AXJ1-O{Vpi$uIz3lMp3dl`?Wwpp>|6_$}|ROmbQ- z+O3VID2pdMNR%dc(_#%+-P-%bNIb5Irk&d>rOY(_mq8%P;dkWuH0mR4vhl=r?rV5g z%=n2Yz2%@f5#I6!(KxF>D%1-3IyJU|VW-!(l$}cWBQtobb>#9D+>HlD>@kp+qgiCj zU_Y+2nP+9m^gw~vIRygs?R~aXBZ*Vk8cFZj_&b8(pTaY{Y}cTT z*fRuKeL3=89rk16#2TNQ%KL}Ryx)%5M0MHy=A(uL9M*f_;^wBL-FO~J+@|(7I)GQF zGxu8y$fzRDE)xoI0MCR3S^FKd3Mzir$&35HZu)9V$~5*Kk^r{%vt!7ISD#%fswRS1 z7x8ugQ&u(usOPXbN5Z5URhEFc|NLc;g}f4JzVjlUxu&$T#yH-Omy4s=$~b=B<)v}= z;R7RHY}oe#TExRVjM2_)jF*Q3%G{)3ZZqgSTa^}wnjk_InITrx)tW> zN_A5pLZ9CogVv`5^1_9Jm_n4I&Od-1kC6YSPp-Oxyt0!D zIplg&zC_?4NKvoQui_?BUY3EYOP5n0W0#hYf21a%4Fg1xeEs;w-CE2d_X6pd9A`2e zuiIRY)}Lqe0J(eXdpq{`UG}5w@h=I2qwDlnybY&n3-F)3(mWK*z~Y1=sqQ352UCF4 zQlI=T^y5Lp>gG~>1T94`()}Z4=w<|*zIWTL=+#(!PT$k6nPOoI-RVk#s?iWB=$tTc z;v`#9_oLoCy7W1j8Mn^hfr?}kDKcERb3jxH4>hafqve(?N%m6{o48;*Aj`VQb5)Ul zHK-31_Fm*+OH8EXSzh8{$7fljqN=ahTv<75(Rp-SR$Zz#EMGFOcXfT5%J^HHx8x@r zP2)nIWHes~>%OVy%4>O3(0{X?N*ukyQv5>kKb>M|32-D&p%1(V8j7s?3w|Lp63nOV z937ts^a~AioVI92W$?353}~XMK~{A}5JkKH5b=n9Ciq@IDBAB;Z!IUAV+ciiDvH*j zMD^3Dk+a${QM5$azio{#f^OHOx>LnJ+5kbRm4^N`5ii4(4>XD|b?3s1jrWv1Z}MFy zT9v+!?Ds9SiLUpcRnr?JG+C=^SKkC=BwXt~F8Tyir)=)czcAl$Z)2R5pR!H;e=OVl z8*}D=$~ONscK(|=^G~^sSitaqkw<2U?vov$hY7)fa=I8~62|7IuK10w(qZq9BnSjK zt$S9yI^QU{77(-&cteiu27n8-8*tNC&-dMPS#upD2hi$Q=J$O0#Os?xwTN{WtSzZC zp0+5nsTrDO-C3RykP7Y)6z8U{uiQ@973Pg|STBrbPO4R4VU>jA3ZJD%OK)mD`u%Bq zjUA|-$B9L(11X}nY*naJ%@8ESe`WsFWU8vR= z2;2}9@)$?_zbc_riw26%Kg!e8Kd<=z-OEDxpIr0*^LqcyFQ+uzy_6rD_)MF*+Au)L zK+sV!gc8RX!}1A93BeHY86igj>{s@tCS@2Inb@Wg|3Ir$G(TxPHZ`*>y-_zsskEEv zlcqu`YL%;Yn6XuOyEIg6vQ;HLymz>grb&Gw(*q#A5?6USh=@|D2=%(`I*cmsk7f^9^}}P? z?OW5EW$5ivagZURMyiQ!)dSTd0?Cq6Pu{r&OKRfiuu+&nj(M|bhppFk4ze_}sSz1;);PvKNiaE=q^G|5w^Vy2SN zBs0Xts91C^d0dq<=JmXesd8D;1K5UvF9?WTYl6d%lJqXxN`Pj}5LxPgSRE$%)Se9Nn;^;MLmXCiH$)23AiNRlj3 zB5S`@U11=y{xj(rqgS3zSUD^dhUILAwb|IZt>UN#gv=Rm63ig{MK*6HQPQQC{?1ODO*flB7}Q(AO3hFI}(g&O+0tS_v* zssss=fjAF6c7M%h{bJFcbm>-<=R>Xa4X{qGb3|a97zk+R8pO+p(k2^QM<;%(sz0y~ zRB?%#!Lct8vXEtAzqvF2#xo$NsieLB9TCSs^E_?X{@2BD7<@uv#vvJzQhJD^v3!dT zl|$vIA|g+p5nMz|Au5{UAyp|$2kfI)S~hhN0%yOnr(#(o-&bKg$Y+VeF{*sx3Du~N znZWwrE{QHx{GA?2J*uLTQ+AKA)Nbt+N2AXvftlF`pev3SOJ$4`MSDf=HiGkA5i0UO zd~$T7PLbVXMt2^U57wmD5}@X1U>&QO#B&jZ0J18_+exP+Z@5Me9xd0Jbq&L^e7(>X zNNZ(5fx4(0i?cEE=!j+2!b@EfJXIo&j};GwfS*019h#N=Yt|*|0J4`!D5 zN_q7;3^d-)FNmK&7&H^rwGK+yh}q{Hpt?|PFC?Fm#mlG5xknmlrQ>IgB05c3KF~=a zh6K*nAvP~CiOXlXY$wlxYQ8_)WN;>NeiQS5Mb-&Nuox?GER-8$-`li(QhmzUy}Keq zW@+_RPM`C|bx|r{2{VLpv4kQKehI>QOprT%3zknCxVb_F`5u!3W#trOn>06Z6D*XH z=M)M2!jWK4RGLfuttE%E2P@F6hVZljI&jmjn43^ zPJ~{D)br75_H1XB8(ej-Emk3-$#Qk8x9>hEB<9vjxJQ=EG&)&*v=3TD&pvVnxeR-) z?Lb+YlOky39f%jYERz8;%h7@zQH?O%8>!r^nUZ(>IPqq+lbCHA8Ax24#IZ@dwzGe_ zNr{+ocSoD-L2*Xdg%@t^OiJbgq#@1W&4(>T_SLJKpM5HrJSQaRRfbG&uyI9+T~>My zyWR{C12~~%bhg$$vJk%xRx<*^v~v)B^3%hV33i~-tUvA5Sfb|5i=rmc9n>)2!GqKa z^P&<_F>DtK$|77CJ5xuKX-Q%!OtxP3n%EsDQrn82M%6F*?l55XtzSVcMPQG0ZuQjl zmq*Ic&aackwk$S6PqbQ!TT;VJDSX~x&h0RoXfrD8&a{@qUZfVn6$ilU9V(GVzCpk^ zP$Zf;Ui%dnVGK2;ueF6kZ zFhW{mY7j^Tftei%owFtP`AO&4M?tOT( z;Htw$hS6rDA9#f<0l{2DA~U)NOfScqg!^m^q#5Caibizsnh)JfGIIAiSiC=S%J|_X-AWeS|ich7A5v3!>zaS0qG@+}6 zF+61ADkXR}zFbZ1mX?PdOp=@C9DI^|;2Tz^0qedK3>_4z?WYMY85qL(rt=Zq14q`G zmX)L~hGa0K_F1zeK5O`YjYkt&x-#C=rX%}-v%xC}Z95zssU#Mk{YR8Je z@U4Wha=tl!xo6aPg=VsfWT-Uw*s!bATd!Jrcam6JES#?b>09?3j3HtW9zjdZo{@vm z;Qsw!K~TU*LK!uvRJbS;OkNH2Wt%Y^x3I4&v!zodO!!r6#`%hm7yl~tBXG|sE%(t= zztYj^vC$ivB^+7S$l7s@do8-L_omu&g;hi4Q7^#p%DB);DAqKLC_yf{M--fbVCW4Q zpLSAJpyR=Jw|FpZ7!OY9&`o&H;FE5C-006%H7z?V^+c?EUl19l4m+%pxM%W-d$e~- zt(|&Ex@CFK^ihfbnmM|@OUuO+x=YOaa6Up`MZSv=z+ zj&v;Xfs>|(JoZyyf*n#2H&qEvkEBqz1th01TIY?cy1siJEZd%upf04|88q_e^UcqIJI$qO^tX{0Q=;ytn*d0;d>W zpbMg2hvsXQ_P18QOkwPq?4dM+V|(uRBPZ<<$bpw08v0vS$9$VUpbm=Fv(IMqMe~ij zM>0rOq>iZMoC}d%y?jB;97(AMLyv&6Zzi(5LIvB?<#Ywf0)mZ_~Rdangdl z&@8jcCHuwoEo63_;{rqY2HFx=n@YZylX9a} zl&P9Yv{)Lgc|b3Q1o2l|SANshLidoYfmF5?I`bsF`E$9kGP};}K?$qva#L^~CH` z!TFGfb4WF(Bq_ENC#V_OREgx>tR!Qa(Jg2?b%7g;M5AE-&>&(JHfZkcmN2s4eJeN!nCrcl9Way`gTk=o|nGo|BD1pGHLvB0ih$H-WM^@K##RBrgEQ`4$CSNzg z8QjInTy|bpvXE2PqeM9*$mGvZ!Ps7Fn?$@*V_0OIlsGq$7xq#m0A&oC)8WX5OB{I{& z&m4D92ULj=J&5P>4A>lRn(KPS@|aiq-&TfHnOC`uYpkgbZ!za!sgrKX&HmC&DR$Qw znLUwmqe#(ab!;OBsne)NG--Cm>qV#<+25uf(vCyt?AGIMoJse#4t}n3bFn42(girok)X zsLlF0m3f3uPV@^VjN3J zs7vW$dREOUH=t;vnxK-_6qp*ejG&zM*m*>v9wu&xniWe@+eJ-67VZtoVET-b0X5{6 zr(c*Y=7z@KB`=B#zMR8)M_(&sn@t?LtNkyD`lrk0nJapT+`Ued`PVEyOY{v7f2Alh zxP{mY>C3kmqt~@Sx9=weAH3PUD&9e;-4Z?DM%u2JrA~7?nOo3Fg!@?ilHRb~Q9Vh0 zS~k)vttP$Xy9A>{?$-j{oKIM^!~^qOk9nFfO9U;uX<{Z}MGPU&T0}pPw4d7EHF*^c z(1Qo888T#p5hW(|Q-(yg#r6vVzhg0gpd>56bb9oH0wu}%3M)p2fxFLEy>QG4R_-h8 zU+Al?!eBv?3%sHzLA?4>j0E@%7$S|RYf_S$ylY+ z4n%*ot_mG#p83HvVERPUjJRH!Ay-9T%yQe2biJr+b%|?XeE(`??bZyWEqp{h5`F<$ z|26&q>X&o$0crC>TI-zNN~}*w7-kFnefLs z2fQs{{%-wM-9ryBgJ*Iuv&{5yuKy+Eoc^si>??Jju|gyAn_Uf`ajXB1%g`EBtwiQ1 zx^awk%lc*V?-yf2mx&<2oHk?3d{TaxpMu&Sc>d+t2h>+*DNg;iw%P+Pbq56MHt1{8 zuC!j;1YlpBL2hXi-rks7|L=db0Mz7?nWiEF08stMZRP$Sn6!kAqm#as74d%`|J5u1 zZ`hY{-1iNNl z1=2bj@r1^~3~TeQTAAId%fY2ha|!FRU6VMpiAkkk@VViqVwhBxz8SBI0v70InyyD6 z3Bn|Jj3nVomoatTh{xa7jx;yvi_UnW_#l*M<|9E)rOc4j#iVycL>cKHTtp3#k-nKL z+7?|mS#aSINetxl?nE8)%Zyk>!C1k`<{`huyPwZD2`YbK4!99|Okznl56^r1}88nU&cpyn*~f zRP2FGaX0@#FpvKuii!WfqnQ6~#DBA2l_uoxjK6W&?wmdns)%IKg2?m;9KE4d3H+J4 z{P-@21_oU4WQ76zv4`7rf2c8VBqkLlTWX8sn;VP7*r9$|Zvr<123Vyh&st-dNnOt) zxtL4AjW-w3bde9fPrdt&)f0toUJ2&UdD?Duy5Ap7dEF=0V82i93p+Kxkri{*^!QAa z`)V#?MO?Egc}EaN?0rV`N9>*U}noU~6E-WouZiR;Mgh z;i}OVBurvrDpRj7!i%ICbMj)VT&(w5JB7dEWs8$MSfbZaa1D^jw$rlh41JSI!*+g5 zc`HjldKt~dEdKiq-t`OW#SHiFi#h4kU3|pR`S;CF5SvpIp|Cl8#>|qEO zL6o_yj`uN0$wSqXQfj)_qWIKrnS3$j-u8y`GrF8k5xy*m3E_xC>4xG+3@28lsi2dl zG->G?bNPxG)$u+RlKOK*4722EnDvKFTfCP}MVn#i1AP7T_HVVXeMTs4JO zpT_!OPG@)cEQ+es9a7Q~8ZJxuwg`RN6PqI_ZGrR{=g#vc28nWQy+I8dcb5dFR^-u; z&&P%sTVJJ;F`R;9s*$hDbF31St>mkHWdp=P*}5fF!x?lQhPw$TMi}e=#xDm^PWJok zBklIX+F!cN8)z!@No~Er@9ywmEwj?-&7I}xh?Aw0SPtK(3EQ+5LHqwwu+}k1p;#vH zrvh`dw3QgL-4@kIQ!Av--?{@#~s8|+dQ;(;Mo#ndpY6spn{3TJBv8{Ee0%vgX2)N zCCV1=Y(p9TH+hpYR^mG9QF6nF>tHb9wDPpXRlL7F+QvVV*IK(W=+D|wiR-*I;elS7 zY`O=x^{a5b-2CDtug6c%+y!Jb>;Y$1|5k+KbP-$ndnLz+PK~0IJ6_kenCmP!NG!nT z0oX@l4sD#DBU$@kjnc{sh4baeOf!mqY{x0?+@X-P%tFTkGt+fK8Xnl}SW!g#bX7&^ z+2;eo?q}&im*rirs}E*eubvzp8ZZ##(eDL0O^$sfaX!0;rmj^d#vG<0v5$vbadqkM z;c@S>jXq)Rz%lvuo_XtEk0U!0-X%0LG%_Oo&y;sC!y!Vzbv!1e%gjo7+E(!P5CXQg zglw~&%zv|GAITU4^EUXYL*ba5L|+fG{n2f#<$P`;XXQzw!rFG>1xIQtjYXPCx$0Tg z_y1H9*k8*NMu;cG(T9I5k|_z+!6-KvLctWLG?awCF`Wto6>5{_B*kX_J!#TlRfW|Q zTxT2;H#0}=YR;55U1N;$dTp5H%;k}GCmbbyfA00QK5!SnK;wWT_=y7G3YX(F_2ej zekKG-;-FFYlnsInfBS-ue-l(=JyzlnCV;dv+bFa!pd>$1xZyr37BgGGzr|0+^O~0j z15^}t&e-E6dU|#)QNVmuka5beLq1^$=n5hx6Mg@fLV!rjf(f07zjUyE!{MRr^$O81 z9c&-SdtEZ{pn(T}h6ZnUS7wPMBn?d!5HMe!BHRBbb05=@24O?2h_`+1 zSkky=Y6p<;hK&MFs_UV3Pi4-ZFlQ5qOdAaJ4>=1O04Q<~*!bCF?FPS~o{er4?b z@BAktYAQF=_~SF#TF%vAsN~HdgBetV+7Sn}tl<@KS7SOg0f&fC(;da%oL1YWSL+*m zGM#5P_te#*^#`lcd2E#Bzrd<*Ozyihcs6GM{UIN@;iOnS-MRs~qr?3IfIIow<-ibm z1axfeXk3WdOtrvL9~RrkL@RPE27Wm{vO5xg=Y{Si6xRMyB}nHWVL(7VUs(tiyCf+=eFX z^v*e{k1Tj6MkZdZ0LiaYY^zFpCUo+Dxx=bBlNeU*IS#VeeOAzI)Vt^$zh$j^EZMHM z**h+Kz~xZ6N@mz-#ETTbxO`K|Nr-N;@=2jQ#7ZgkFx(W;GWygjB|Jx@jU+qS`t!IrL_@Mh#X_TZx%@ z^4p_*L+-*ol_Bw(5gpCY^}j0qLkVl4eKqJivQEuSwK~_wQU=a?(Pr}B&EB% zySux)K|s1&x?55}O1is2>5>k~O$h(?yyyFj*W>Z~9|mI&_Fz2Mnsd!nbFSyU4NmP* zk_r34gxePNOJ$h6cykvyCw$qW0>}3|r&9U*AFcQWu@^Z90;YM#zVCO^+rx zNH@pXoqevqr|SqP@$wvXr8J@&d_JP>=uXmMSW8G@sN0shx}NXhJ^U;k3^P3*Y9*{X zT_){Q>`WUL%w79gi?=u4Dq=QB^rnC>Qexc!1mCKET58qi_4>ylhJterN@VVP&{9R} zf`VGjgzL=<92XlYXsi4V{!C1%tpasaKFas6LJV)K-=vfm;P_v(pq!FX4Y?&YsVKhO zR%%faHzRDbQ!M3E;64T2WnRzcuczPxKYjJ4E?oK+r6|}!&xa}zY4)CB2A?|sZ9Z0a z|7}5bo3I!eu5axh5J}j*49lzaa_Zc8rw3g>pdb(cSDK@($H8DyJ~4-_*`cwZ$s? ze5h6-?o%Yb`5-tXa|0?FF6Y2tk6?PhbB~VSfa6cTW01)6;9^4dE+jka44m<(+qOx| zS7+%A4{cV1vYAlL_6DE@7TAVxXLfPEJy)0APHnPc=nL6sYxCkc(#=FY#J=VU)@bgA z0_~_L;7&Dz1PtGWxfn&<4}Ma94p>_udw=f*7k4kv58VQ0lC!J^kehlmGtWV4Mi6UiYHz1L*lE`k@;g5_yK$-= zZtu<-NFGqxlm4JpB#T7g%Ex-iNmQO!&y7g$cHfwbO|=&7md}4l4Mn9|n24rEQ^>Ux zYO+gTedMAD(2~_1Q6k*FOpy38A*yn7gLcbXj?+s+U;2tl$BG4xn$@hHmfNzSfuA*V zDR8OI{FbT?yi6r34Q}@hSTAGKo2ggB19-#DmV2x|Zadz2|rHCQV8f=qYq3S-XQKr)V!L{fbjC(JB{i1oZ ziF#JsGKmxT>@0|5a3}*}b2#dWUIr!i`8n>4;r7E*)&qvB!SvEbZkC%_T$i>HF_iTK znSw(apn9nYdcK)KaXd!E__$?es}T}>(H*ztldjGo3~FxJOQHIwDEbA;V7L2u0y+iR zI z`Ta|+1SVzj1fro-ACvhOxw!`lkeVnt+5zUv+2Q>l6W3DEHS!?GkLeUc=jF=*DYi;4 zgAmXvqwtL98S&@oBP*(OL2;6Q!{jJ!x!SIzc(UKP=n25KVnzea3MJKb=3u8Cm>iLlc zo>?@$-95+WQf~)EAZt_5R=Kx&-+eesXf5(h%iWVsgV-k<5sR4Bt?SzA!_Si!Vs17{ z{6tvfF)5Sptk|88Zta~Yi^wNgFB3D>72<4rA$j}O^elvaJgTjo4ShF~YmiNpHeGbr zyKXGp)-!&Ibd!z^zbI+4QbF?)fGbwcwDyLFza9Z}=ghoEC1>_-5DRf*_-4`0`D_3% z-j$9^NUELnMfu|?&hgFGHu3n@;Oi!chfyGFC1tj zysM2L<;pVB&eZILeivP-DG6^E!_0P@Pv$*0)yMcNP8S ztipdgy#t~iDVyOeruzZb?;xzt0NZ53utk9^3ZvN}(iFQco`XI5+!2~Bt*g7s$UI9V zqTk}E=N|5KTZK~u!6+3ngR++0rc2UcL~b2^1ySOpH^5EkBa;19dk^IoLT_D(^eYV? zh)u!~KjQmm97L8GO!T6q$6zM-+4)P@I(QCal||#8B$YWzh+EnD6~{;lGD;KM(2Z~x zbfm^>#(c>3<`9QS(Mb$0_NoT37Om8`p*ft5u4+)-eY&scXqIdG8ph(=r%k3w~PVLOXd zvY%SJgzTUS)}20bSmIE#Ku2ArE#^+hFkz~5s)Jq}y~;DcyBxahE*PlD`+}A(u^rn<&8zczVDn%^A5dk-Vy_mr0qL*uM z+kH(G>dhnCDc>o`r?(AIs+^*rfe)ECTkV3CYD3Q#19fXQhe<>BD4P`WFJ{4fglrGp zMC#o(hLNzR_6BG%EOWFS0kBYlhLR^aX`ly0}L;y&ATq9Kgir+g(JSTR7eC^Kd70rtk@Qwh@u3M8?jc zvgkQ+ER2q@6iY?Es?2yUOPXy52HHmmw09OlCy8i1JSX$cFQ?Kz?WxLaD*;xXXdOZ= zBkjariS2=U=4{ztOD4WdLby%7@-N=%81G7r_onmAC}*~wh&dH`ElcXAaT1YCg!*3c zydPyIQxoLY1}B)t!AYV-sVm|=v@yqXQI~?W4Le?d1`+uZEGOQ|ee*VGf zrT|&74wW?}lFB{`V02N9RseY6=RHwR+vczuOFPU6KW$IutXl`cwNkIGa12qG zrJ%bP3TNk7J?}yS3x6XEWxoN1EKl;n-Jr)OR82@8A-lLcqJ0m!DhivFnJu)P!CIZozRj3Dupfu>UuxP6njtRWN0x(t)#GPjJ(W*QX;@KZebajIc;dm zCW~hL0jRsrD=aVq-P|3Oy{?-lW2lzd!ihrjVFr)oLbOS5oQOiE*S-!;?Lbx&bB@wB zIBCNkoH#5Y8I#5PlHx>EpLUEIfBnTV;pU3R%nfkZ z!YFhE-!>M@7lKEDX})s?nHWmd;*DDNM6GEm7PaY{ePtQ7vU*E6^Yo7t_xmKXg?pIw zLetbL($kGYR?TwDFJ{6?y@??DP->A;k*WI-u5h`r_Fj=a1?c8CaYv_fx+w3Y&sz)# z5l!Eerg8T>?FtY$ym)%@xf}a@V)bx@rCghzp-=;#(K|s@NOO*IZA)NzB23n8Oyp`N z6Y_)!pjq5GpOl;|9mspLVAjuk4Swf>dB>Z+oWGfksTiJHt6LL8{)`TN&}5mlo&S@f zn?k$j;4E88b8ms}U06xznINvR%znonws$*X0nXu~KR;D&0=; zq1MxLBj~1VFmZ3_rpJ&0B|edG0LL4z$TA%JtOE-~IHfCXompV+wy z8-&6rt-RaR;6BG2HZ5IoYkQ!W1K80!*5H1C5|T&@US7!VmLWU9nG%2IR0sf%g(q;p zir%R2#OCiM-FRbfu?u|_l)-Q7I{}F_K#B)nXF9wXSLm-9xO`&}clEL58GaMK6`1Uo zQKob~3zs=o{h-kD;27bhfCkdw{8=X?mD$rB(iIfJLV2z}Inma$btemM>{3VY_dH`c zRmH*W_;0{4Bi*0y!=kq3gCg}!KzsqQv(?<&2%Y|52_E_JZZE7axCF6;pWKz-h9;(1 zFEg|lBDp{TkLtU9pc8X{8!)$h;lT}wYiX`cFvH{sCC$IJ1nrkGsX1R-c54t zLc9jBHVaK(PZqQAK)*w|rQxaCi@4yDsR;BKp_0+QMY4^V@oQdty=y?g5jigp7$EqZ zjDUR~x@7qfAlguTFi<0JZx{E(?05$3ZrE!(`+7JwC(6-O)0zPfL-;9#k~GMZLtGy?nM#)>2+T`kNj ze-Cd%!Vd{3rx0cOIo+1L-plN7F!@)*0?vWum?{xsvwILKF<=UycOWzqNrt^1DAHo{ z&>l4+Ab^}}aY{#leq4;cq6#<-V$Ho7UKVZ81@Wh+CFOY)SxBEZUOMd5^n&4mJBI5y zhiL&%RP$EK=dU%dsx>v_%dKWSAnH{~OU>To6_twC8@+RTFwOV zjN#5sZh{G`WWFrn$+vV8xa_EdxGegTh$iG5fdf8|IkR2eF_u{^F!2%tv7EYty{ytY zfTzxF4)ngPoP_WTG|Fer08u&Q$%>o}_7yWw_VUke{^I-nDIPLL`#{~ep5)0hW*8ez z$=vvIc7ys0bTt^Z4cC$pSAr8jP+)*}S0n5;J4~41b{%cIM*fv_$1_a{7~CzEGF*%a zmo!~DyV(mH=a!>N6aTXY|l>8fd_G+w#(nF|q5jcLBA z13?#dl>PPCA}RNzqD6oVO(@OKym{I-Pa5JmLRwqW$FBiUBnL+P2)@~J(ec|s_sm!R2@$OKicGYN*2GqU(J&T z{Lqn)*=vxuAX1Gv0Dk!C`pCTtlDrGq_gKcHI?^jian>rS^UL?G0{-ilaNK#DTyw56 z{Mo5FbQ?Hew~5Kllovle5o!-n7?EA%~9 z%jQnBip8H@%a9KGo;gZW59-6s%P>_Y62@fk&z9tt_3vec<8wZNl}y-DPVJOG|Iin_ z626Fx(_8z21@R?Y6h3=m$wyZ(m0~u^gGm$C_>_E9bIWd}w}}Fi6`vO0&SEgSdVWB! z70oGSTwI5)%Dq)n3w0Upp_=|g;_;3OZw=}>WJUsdX*M=A4EsAwYD>0ZPrKc^Y`%(P zR4QJgyJNu4aNup&3279U6_ zdbsfLmw#jb+-(ai0SJf=$M4ESh--^XS307Zgwt`pJ8{}aNm%u@LRcdGx zw~H)F7#NIpX{7#kW5V(1H5 zz5AdL#5;!Xs~elu2h{fX{pR6_V=3+&^ruJ{iTx$`s^O_)RYD@?{ol+}(o43PDCFcy z>6@z&ig(9lnQ&Je#^YG*qG0nV5izc-nDi1Oya!vptC5L&xq!LbWas62!Jk9@Hgg$u zcf|NzytpAfC_?Eo)ZG&ywyD+)KyrtAk@F|5=o#Mda4t2W8yW1la)U@5zE9jn2t8L( zX81%5B2%>F4iIQQ*!=|^;t?PSN?@8gFwrSJ@S3$#y8xt&xUbuD-u=7}9#eLWR72-qTT@xu+BTcA6}iClYMq3D|3PS&w~_olnHK zbbUG}X3XIIUV2VpcbYSqR^lWK`E;G4pb|N_JYdhO-P9g;3Pq zx#XGZHE!5Xc?m~}&3$AbIXJZLI=xQV><&VT5CXbQ&*Kz10ue(bo$2A61QOcN*>`p;EOKRNXLPtn*{8w3F-Cleb(>;Dq;Q;C(4 zd?J7xq=(1C&}V+H(IjuWE!QWIPhSF^7YZk!fUfOIo+QzqwU^5k7P>3Y8U%-;?GA!O zHYcntF5ohIP^By2K2uO|W-gA~czK@O*61M(U{K*rXX`j+=FR!L5*bC z8%ZNoC}V;XL!Kpb>sP)JkSj_sf;rwMx2$<+g%bK77T7~8tSw-VD@GV=JA)2g5Hs@& zN(X^2sMAj;J;5fpbBvQ$s%Wr@mKo`t|+60qbQv%_fRc(1N8*2fDS zc~Y)?i3pyo`Y`?2GK=TmHMB1Sk?@)-KhzR}Oj=qWo(Ut-uUx}_lC%xNatZzBfmEBJ zSB2ILfPtS-VxP5RivoeD?|F1}MKFC}S2DXwe+>&i*)@^(pNc<0Ylm@t;ENoizkQkG z#jnpbKyf#qNVcsT*VPwT{GWW9AfDFmg(z^eN2;&JR3~wRYIg?8~`b z6w+Q}ETeZ#j>1Z?z5425VK$AnXI=J;)o?YW1AC@*n=7rc0xy8rmLo~Jcb!bgn3ceG zv1@S2g~rpP*}ia;hD~CRV%Kn2XA_Ux$o_4-22CZ*sM5r!eGy6Peeyw==5WHgAUBr! zfvRYibkq^Pj~pB0`BIi)Xx#xu3H)+%OM`sS+HY@3+2tFUh{#~*CgyA#2A6>lqfn z6S5O{6{Wk3D3`MS+HG^VfwulGBaN;h`#huNIg<4%zjQE;0edb^GBt_26eM9Eg~2<= z%x&8wNd;sz2J(b`T`Vn+b%GZu!pg_&@u44I_b|jc_M^Ast*GX% z~cER`C{E`DzN*%y4r>@ti4A$Le2~6EEK|BE&%nFopIQQ zN!-D9pX<=ija}?3M}Wur)SnR4!Q^=N{TZI>K-5OX+PuZ@ecEdP)O|3 z;Z49IgbEtgSJg(*(Aa^$Aoi=5ZV6^_E4HzP)mn?bbRzqSk-Q@}P! zU^@l7uS{R0FQ1#*uh%#!jP+VDBI7|deK+xz-o;cMwsFQa_N6oU`m|HL^uTLD=QXI? zqFiDND9*>fT!W9Zuh{5;R})jH-(6Au;dQ~kD`bIM)20??E{+DjC_(m7K9a=~L+3%m zmtNX7LSUw(wb78YdD4gQYKDwb0w6BK=Xyc%RRPAvWSvJs>w0h2R385!%w)PxhWr&M01bMie zx>a1ez2u_4;Q$qR#^a%(z`bD;W}PcbW;gZp$;XJ(jj16;20aY3xp5(V_)^EWM`}Gr zK#ADYB0DVWY&9JP_oH)FDL~K(Y0HNT%jo5+7MAC6`q*B*BqP)IfOA zSs1}p4ht#5?g87B?XYTl`HxLvWh($kg4e|Fz2Zvohr;hXR?n)(=s&V%ugp%$J_YTVFooJk<#&j9b704}aM+b!QM* zY2B{6NUDF@2GpzM?B-{6Ghg#rk|qw*Qr=FO%CA^HN`cxwni?*?^I8;o%^2I|#b!@H z!~kFZVrVLm*xR}zG$0!nJB)j{!+gufR3EieNl0$mvb9e%%PXc-huMH^XTw*p?1 zYyBDhW(uaF%N2hMyCTWakzvUi@hY_+R8p{u`b*vcrP^U z_*g|+yWK|d2olI`sQ^ThBwo*25*7;P@yH3tB(f9HU$-isz0RnuWHIEzUyNIb?n@Re zv$Du(b|ul3b3Fq0U>?6%DxBrqHZ@M!(Q9Sr<$XXSD&RZR=lmi8#WaVOpR03FJ!gJX7}xq)vi!L65L~h`COI7w7PQN!xMG^TmKZsOTAK%u z#7EYSymBa>Y&`4@Ffm&lxog|JGhG>BPx$u;Ig zhanra)@5TBV{@8(le)od=MZScTHK2=8cikHIuNW>^0PQLiQ-@U95r?P0sc?spnX8XB-Fwp8ZN9nk*gQNY==j2)0kCP> zDS3wH9LV%ani_3bU2|xy#zAU$rwL<`uAe~6y>{(&G8kQVUiZh>m`rur~bZ0XVL~QQ(q<_ClM)5o8+`+95hA?X0lOj&2f6?i%}xEm~y3R zZA1w3h^*;MJ*GFdRrP9o(a}EeSy$0MRB1H>ND#EI?o(ILX|D1yXsML7Jz;PiQelZ+ zp!i9t0BZQ}Y0c!zH|4A21GdDR7i)Cpg{XY}^=@lm1vWb9>y^p4F^Fj{5|XH~U(`1y zf0U&kUb4c0uQ(#`!MNRwE;%*DP}`saRhM}Q@8)WSInEKkDq_N)ih@A^4cDIuzpTR1 zg1^TRqQx;vVRq~}7XnA(a3&`_p-X}Rp+M!R82&a9yRuU2)qbcH!*(OuBG-ZxL$7^3 zk&b$I^~5I@OdQRRR`nvwa|Z8Ax*#R#RSH|9#$u7?>1oDhG*RHFDlwSr4bi&61QLwz zDLzl|vh{cbR+{+2Riced&uLkYy9`dK_ScE8u`N&ueqg2cUruA%=)P)#35CF58vwV> zIFPBlmMmvWShXzwjAC;X9Q9dnE`&F@@U8Utn=nx1ySEfLX(0((;LiiMhO*{o z332vyIVs;A+_1A?y(oW|?Fl2oUa(^_iON_+oYqiYgd}-iq2eyFl8e*2C7b|Q$7#)w zm1s2=sH^Fdv2u>d+BWU{?4KqFr-5CP>KbEH1xpYDVVij6M-c8AG=ym^@?d!I(P`9u z(W@77VDq{wy0<#R`)C@Tr;x*YPD61$^u=U&KnFrtLk+}c7XYQ}!}&%5t49-o8#I6j z8$BWc@|_PmISg)MZFq}`=(Tu&Y0*gn=!zUT%R6}HnzGC1I3zr#o#GHqMQG@>OzQj7okNAF z(psjhjkl6sE-6TI^GhnVg0K&Qnd~;28l$D{!$=pSZL9m)_hz5f__8{k;McQxsl7yL zoV4+ZL@DetHhsB+u&|Sr*#=j%+t!eitu!F$RMK>tLL_&GeKR_!oe^eQ=FnS3U9fs4 zI?FrCXlH>RT``+eW}G!(+Yec7JR&Y?WJi( zmoa%r*|6?kWI2MyMWFR&UR94W?=gsTJxJ}_*g_YkdUWL!owBrj-lX=Hx;)8+BIbFr zftcCqOWQ7{96mH7cGBrD==xgg7+$j^gyKT_a)O9QZ?{T>TX!jrkd>J#Cm|;2;tO2| z=43{SY5NJhTQKQ*&oeNy$u#WO!de&b$r+usOzH|f+vA&o_9PCcYXVad((7s>b=O!Z zxvTY)LL%1i&SDV@+C7(o`!I)3_ln}{m?q?=Y~@fKh>zj!lY5>N_O3$Ml2U5KPx+(7 zN0LYrf4JaN?NRvbXSVht{+PCc8`(XyfG??_f2D8e;jKH>`WI|T!;WbjqP9zrm*ZR7KW`bM%aMZ4>;lijsSslVlc+pT}&WfxFuQSMv0}uM1%mqJA$7GWa z6pIIode$f6LrBHlm1tMmunGE`=P4W`HIGYvT#t8kYINF0AA{{c=jGrCMA7YO`<&7m znPRW=3T+R(iyAEZD5LAgt+0a^)JQ95Y} zArV<65fxQBr;(Bl?f2HlYs0 ziGdJ%;O|#epl^W;RG_kRG@~>7OHhi=$l8MLJ1b@ZM>7{2pdviba?Qm47dPlXx4gn5 zAS((u#k2^#&-gl#^es}6f5-WyC+g41pS(8g(F7)M06uYiwe0*BfoQ)={+9!*<1+zM zpe4zFKtG#={Y zWh|VWfPQ@cp#n$BpCHi$Fq3A1NJ*f0`j5@bc=iX#zgcbujwXNJ%$8|Sv|Ql8_W^R* zf9Tq6-~sy2ga7Yw^MCDC&}KYbVj#*CIDmc}rk9j|j8g*IG1;2^%l?~tkPAUa! zoPSK-#rj{#|LUpVSk(V~Fn@15{M8crTjX;6d-DGbxPRIH@BK7?9A#=eKOijruWrUa zH|Ben#;-;|-(p?xH>CfwTj$T*@7>LQyk=br|G@pFquD<@LjKJ8-uCLNSK7B=k^Fbg zA3CS~4E^4B>8qpGw|FJ}1N48^U;fBn>u1XM)-XTrI(OM$QvTNt=KtpC^fUK+i;S=aQLge^)V~~G-zzMBoxuDS=O(|*`v;1gKX3c@GJ`*k za60qfF#ev4`Df+EpE=)Gb$=Bt{1(v`f5!Qj&icO6_{Yu)@%|;?4@$*8G>EADkeqE{m7WL`BO#91q`=2-V`_;N1uP(+}zs&l(<<*~)e?RN~b;0jj z5a;|l`5!F*{S5hjw(!SY+EDOI$ls&#chmVlGroU@`a19UEsRQj$M}a?NO>s;-~$;5 R2np~f1o-$>Q}y+){|A@R9n$~+ literal 0 HcmV?d00001 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 100644 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..090b62393 --- /dev/null +++ b/runtime/java/prompty-anthropic/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + id("java-library") +} + +dependencies { + api(project(":prompty")) + + testImplementation(platform("org.junit:junit-bom:5.11.4")) + testImplementation("org.junit.jupiter:junit-jupiter") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} diff --git a/runtime/java/prompty-foundry/build.gradle.kts b/runtime/java/prompty-foundry/build.gradle.kts new file mode 100644 index 000000000..090b62393 --- /dev/null +++ b/runtime/java/prompty-foundry/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + id("java-library") +} + +dependencies { + api(project(":prompty")) + + testImplementation(platform("org.junit:junit-bom:5.11.4")) + testImplementation("org.junit.jupiter:junit-jupiter") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} diff --git a/runtime/java/prompty-openai/build.gradle.kts b/runtime/java/prompty-openai/build.gradle.kts new file mode 100644 index 000000000..090b62393 --- /dev/null +++ b/runtime/java/prompty-openai/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + id("java-library") +} + +dependencies { + api(project(":prompty")) + + testImplementation(platform("org.junit:junit-bom:5.11.4")) + testImplementation("org.junit.jupiter:junit-jupiter") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} diff --git a/runtime/java/prompty/build.gradle.kts b/runtime/java/prompty/build.gradle.kts new file mode 100644 index 000000000..1d6331faf --- /dev/null +++ b/runtime/java/prompty/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + id("java-library") +} + +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") +} 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..9423e0815 --- /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..45d81c2b0 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AnonymousConnection.java @@ -0,0 +1,86 @@ +// +// 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 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("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")); + } + } + + + public Map save(SaveContext context) { + SaveContext ctx = context == null ? new SaveContext() : context; + AnonymousConnection obj = ctx.processObject(this); + Map result = super.save(ctx); + if (obj.kind != null) result.put("kind", serializeScalar(obj.kind)); + 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..fec889f03 --- /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..14dcea5bd --- /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..a6af62777 --- /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..5594c00c1 --- /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..865ec04ab --- /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..d6095fa0a --- /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..4f7407e86 --- /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..dc0585f82 --- /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..ffcf475e8 --- /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..4577cf496 --- /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..624924bfb --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ApiKeyConnection.java @@ -0,0 +1,91 @@ +// +// 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 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("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("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.kind != null) result.put("kind", serializeScalar(obj.kind)); + 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..f0cbd2987 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ArrayProperty.java @@ -0,0 +1,86 @@ +// +// 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 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("kind") && map.get("kind") != null) { + result.kind = String.valueOf(map.get("kind")); + } + 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.kind != null) result.put("kind", serializeScalar(obj.kind)); + 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..7e8f4bffa --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/AudioPart.java @@ -0,0 +1,91 @@ +// +// 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 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("kind") && map.get("kind") != null) { + result.kind = String.valueOf(map.get("kind")); + } + 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.kind != null) result.put("kind", serializeScalar(obj.kind)); + 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..d3ca51e6f --- /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..9c54fc03e --- /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..59a83f7e7 --- /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..8df766ba8 --- /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..9cc7347ba --- /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..df96ea40b --- /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..4f599c43c --- /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..b235669c8 --- /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("Missing Connection discriminator property: 'kind'"); + } + 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..9ad156485 --- /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("Missing ContentPart discriminator property: 'kind'"); + } + 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..9da483851 --- /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..4930974c1 --- /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..1b86ff69f --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/CustomTool.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 CustomTool extends Tool { + + 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("kind") && map.get("kind") != null) { + result.kind = String.valueOf(map.get("kind")); + } + 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.kind != null) result.put("kind", serializeScalar(obj.kind)); + 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..9d3a0f57b --- /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..cd050ba04 --- /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..3705cb3da --- /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..03bf54f83 --- /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..1e45e0dc9 --- /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..b93a66186 --- /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..808c49e12 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ErrorChunk.java @@ -0,0 +1,86 @@ +// +// 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 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("kind") && map.get("kind") != null) { + result.kind = String.valueOf(map.get("kind")); + } + 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.kind != null) result.put("kind", serializeScalar(obj.kind)); + 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..2d3216e29 --- /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..2a3b66687 --- /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..572358d4b --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/FilePart.java @@ -0,0 +1,91 @@ +// +// 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 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("kind") && map.get("kind") != null) { + result.kind = String.valueOf(map.get("kind")); + } + 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.kind != null) result.put("kind", serializeScalar(obj.kind)); + 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..71b83387f --- /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..3580bb7e3 --- /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..2693dce38 --- /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..97dfed478 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/FoundryConnection.java @@ -0,0 +1,96 @@ +// +// 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 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("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("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.kind != null) result.put("kind", serializeScalar(obj.kind)); + 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..e99d38291 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/FunctionTool.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 FunctionTool extends Tool { + + 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("kind") && map.get("kind") != null) { + result.kind = String.valueOf(map.get("kind")); + } + 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.kind != null) result.put("kind", serializeScalar(obj.kind)); + 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..2e868d015 --- /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..90b1bc466 --- /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..40018d4cc --- /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..b52a08317 --- /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..dcbd3ab28 --- /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..1fca8bc0b --- /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..54abb310d --- /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..56bcf019c --- /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..8e23528de --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ImagePart.java @@ -0,0 +1,96 @@ +// +// 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 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("kind") && map.get("kind") != null) { + result.kind = String.valueOf(map.get("kind")); + } + 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.kind != null) result.put("kind", serializeScalar(obj.kind)); + 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..7551a5b38 --- /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..2abecd943 --- /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.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..878ec411f --- /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..b2751d407 --- /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..dfe817f23 --- /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..4b25e0804 --- /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..fc63ec5e7 --- /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..3a4dd8978 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/McpTool.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 class McpTool extends Tool { + + 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("kind") && map.get("kind") != null) { + result.kind = String.valueOf(map.get("kind")); + } + 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.kind != null) result.put("kind", serializeScalar(obj.kind)); + 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..1cdf66c00 --- /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.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..1a20f39af --- /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..5fd94443d --- /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.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() { + throw new UnsupportedOperationException("Implement this helper outside generated code."); + } + + public String text() { + throw new UnsupportedOperationException("Implement this helper outside generated code."); + } + + 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/MessagesUpdatedPayload.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/MessagesUpdatedPayload.java new file mode 100644 index 000000000..3898a7a43 --- /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..93725f3d2 --- /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..dea4e24e9 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ModelCollections.java @@ -0,0 +1,131 @@ +// +// 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)) { + 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..b704c742a --- /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..2782f1212 --- /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..e81e6688c --- /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..323596d7e --- /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..95cdf2ae5 --- /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..cffbee242 --- /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..52ddb9e73 --- /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..f003357e8 --- /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..053450fca --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/OAuthConnection.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 class OAuthConnection extends Connection { + + 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("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("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.kind != null) result.put("kind", serializeScalar(obj.kind)); + 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..c97afc8b9 --- /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..f81dd3148 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ObjectProperty.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 ObjectProperty extends Property { + + 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("kind") && map.get("kind") != null) { + result.kind = String.valueOf(map.get("kind")); + } + 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.kind != null) result.put("kind", serializeScalar(obj.kind)); + 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..697d43862 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/OpenApiTool.java @@ -0,0 +1,91 @@ +// +// 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 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("kind") && map.get("kind") != null) { + result.kind = String.valueOf(map.get("kind")); + } + 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.kind != null) result.put("kind", serializeScalar(obj.kind)); + 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..bf3ad2d3c --- /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..41dbb6ec3 --- /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..f4155e47c --- /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..669a5cfb3 --- /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..cf67d38c6 --- /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..67ae41a0c --- /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..e41e34720 --- /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..77cb6b6d4 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/PromptyTool.java @@ -0,0 +1,91 @@ +// +// 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 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("kind") && map.get("kind") != null) { + result.kind = String.valueOf(map.get("kind")); + } + 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.kind != null) result.put("kind", serializeScalar(obj.kind)); + 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..a33b9ffd2 --- /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..52c453088 --- /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..1a09c4f92 --- /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..4e8c5c593 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ReferenceConnection.java @@ -0,0 +1,91 @@ +// +// 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 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("kind") && map.get("kind") != null) { + result.kind = String.valueOf(map.get("kind")); + } + 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.kind != null) result.put("kind", serializeScalar(obj.kind)); + 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..962797a51 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/RemoteConnection.java @@ -0,0 +1,91 @@ +// +// 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 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("kind") && map.get("kind") != null) { + result.kind = String.valueOf(map.get("kind")); + } + 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.kind != null) result.put("kind", serializeScalar(obj.kind)); + 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..749a4f146 --- /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..d0de2ef19 --- /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..938b117d3 --- /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..f00ff126d --- /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..039d8dab7 --- /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..b7c542bdc --- /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..124c80b2e --- /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..3068b0c54 --- /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..ae78fdf8c --- /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..accc59cac --- /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..f9a7a1329 --- /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..984dab176 --- /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..a8a9bffa1 --- /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..97c32c227 --- /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..0f1933201 --- /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..293ad8ec0 --- /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..f773b9723 --- /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..52867fb49 --- /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..264f4de38 --- /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("Missing StreamChunk discriminator property: 'kind'"); + } + 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..5354d8914 --- /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..8d249ee7f --- /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..c44b1ac2f --- /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..8576e5cc0 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TextChunk.java @@ -0,0 +1,86 @@ +// +// 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 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("kind") && map.get("kind") != null) { + result.kind = String.valueOf(map.get("kind")); + } + 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.kind != null) result.put("kind", serializeScalar(obj.kind)); + 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..a9a8e9d62 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/TextPart.java @@ -0,0 +1,86 @@ +// +// 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 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("kind") && map.get("kind") != null) { + result.kind = String.valueOf(map.get("kind")); + } + 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.kind != null) result.put("kind", serializeScalar(obj.kind)); + 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..0f423751c --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ThinkingChunk.java @@ -0,0 +1,86 @@ +// +// 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 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("kind") && map.get("kind") != null) { + result.kind = String.valueOf(map.get("kind")); + } + 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.kind != null) result.put("kind", serializeScalar(obj.kind)); + 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..53d8e9bb5 --- /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..41f7bb380 --- /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..e3361e864 --- /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..f3d43ddf0 --- /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..601ea914d --- /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("Missing Tool discriminator property: 'kind'"); + } + 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..91b42b3a7 --- /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..6fb395a87 --- /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..131946534 --- /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..c2e777c1e --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolChunk.java @@ -0,0 +1,86 @@ +// +// 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 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("kind") && map.get("kind") != null) { + result.kind = String.valueOf(map.get("kind")); + } + 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.kind != null) result.put("kind", serializeScalar(obj.kind)); + 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..b5ef2dcb3 --- /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..e9d643265 --- /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..c275f4835 --- /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..af500fa39 --- /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..9bd4c1f46 --- /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() { + throw new UnsupportedOperationException("Implement this helper outside generated code."); + } + + 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/ToolResultPayload.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolResultPayload.java new file mode 100644 index 000000000..09249f440 --- /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..19469cc2f --- /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..077db8260 --- /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..74d5c31e9 --- /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.0; + + 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..12c893ae0 --- /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..ac66849e1 --- /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..19edc0ddb --- /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..6169e58ea --- /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..e74c76f3a --- /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..189b7b3eb --- /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..96625676d --- /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..ed97978c3 --- /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..93de61d1c --- /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..703cdaba8 --- /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..3cad84b04 --- /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..71439d66b --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/UnionProperty.java @@ -0,0 +1,101 @@ +// +// 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 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("kind") && map.get("kind") != null) { + result.kind = String.valueOf(map.get("kind")); + } + 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.kind != null) result.put("kind", serializeScalar(obj.kind)); + 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..84c226198 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/model/UsageChunk.java @@ -0,0 +1,86 @@ +// +// 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 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("kind") && map.get("kind") != null) { + result.kind = String.valueOf(map.get("kind")); + } + 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.kind != null) result.put("kind", serializeScalar(obj.kind)); + 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..a6d46bb81 --- /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..9b72b14d9 --- /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/test/java/com/microsoft/prompty/GeneratedModelExamplesTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/GeneratedModelExamplesTest.java new file mode 100644 index 000000000..4f31719ea --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/GeneratedModelExamplesTest.java @@ -0,0 +1,33 @@ +package com.microsoft.prompty; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.DynamicTest.dynamicTest; + +import com.microsoft.prompty.model.GeneratedModelExamples; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; + +/** + * Executes every example suite emitted alongside the Typra-generated model. + * + *

The generated suites are plain {@code run()} entry points rather than JUnit + * methods, so the generation pipeline emits {@link GeneratedModelExamples} as a + * deterministic registry and this factory turns each entry into its own test. + */ +final class GeneratedModelExamplesTest { + + @TestFactory + List generatedExamples() { + Map cases = GeneratedModelExamples.all(); + assertFalse(cases.isEmpty(), "No generated model examples were registered"); + + List tests = new ArrayList<>(cases.size()); + for (Map.Entry entry : cases.entrySet()) { + tests.add(dynamicTest(entry.getKey(), entry.getValue()::run)); + } + return tests; + } +} 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..0b1a1532a --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/ModelNormalizationTest.java @@ -0,0 +1,458 @@ +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()); + } + + @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("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/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/GeneratedModelExamples.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/GeneratedModelExamples.java new file mode 100644 index 000000000..33ffbb55e --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/GeneratedModelExamples.java @@ -0,0 +1,162 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. +package com.microsoft.prompty.model; + +import java.util.LinkedHashMap; +import java.util.Map; + +public final class GeneratedModelExamples { + private GeneratedModelExamples() { } + + public static Map all() { + Map cases = new LinkedHashMap<>(); + cases.put("AiResourceInfoGeneratedTest", AiResourceInfoGeneratedTest::run); + cases.put("AnonymousConnectionGeneratedTest", AnonymousConnectionGeneratedTest::run); + cases.put("AnthropicImageBlockGeneratedTest", AnthropicImageBlockGeneratedTest::run); + cases.put("AnthropicImageSourceGeneratedTest", AnthropicImageSourceGeneratedTest::run); + cases.put("AnthropicMessagesRequestGeneratedTest", AnthropicMessagesRequestGeneratedTest::run); + cases.put("AnthropicMessagesResponseGeneratedTest", AnthropicMessagesResponseGeneratedTest::run); + cases.put("AnthropicTextBlockGeneratedTest", AnthropicTextBlockGeneratedTest::run); + cases.put("AnthropicToolDefinitionGeneratedTest", AnthropicToolDefinitionGeneratedTest::run); + cases.put("AnthropicToolResultBlockGeneratedTest", AnthropicToolResultBlockGeneratedTest::run); + cases.put("AnthropicToolUseBlockGeneratedTest", AnthropicToolUseBlockGeneratedTest::run); + cases.put("AnthropicUsageGeneratedTest", AnthropicUsageGeneratedTest::run); + cases.put("AnthropicWireMessageGeneratedTest", AnthropicWireMessageGeneratedTest::run); + cases.put("ApiKeyConnectionGeneratedTest", ApiKeyConnectionGeneratedTest::run); + cases.put("ArrayPropertyGeneratedTest", ArrayPropertyGeneratedTest::run); + cases.put("AudioPartGeneratedTest", AudioPartGeneratedTest::run); + cases.put("AuthorizationCodeFlowGeneratedTest", AuthorizationCodeFlowGeneratedTest::run); + cases.put("BindingGeneratedTest", BindingGeneratedTest::run); + cases.put("CheckpointGeneratedTest", CheckpointGeneratedTest::run); + cases.put("CompactionCompletePayloadGeneratedTest", CompactionCompletePayloadGeneratedTest::run); + cases.put("CompactionConfigGeneratedTest", CompactionConfigGeneratedTest::run); + cases.put("CompactionFailedPayloadGeneratedTest", CompactionFailedPayloadGeneratedTest::run); + cases.put("CompactionStartPayloadGeneratedTest", CompactionStartPayloadGeneratedTest::run); + cases.put("ConnectionGeneratedTest", ConnectionGeneratedTest::run); + cases.put("ContentPartGeneratedTest", ContentPartGeneratedTest::run); + cases.put("ContextCandidateGeneratedTest", ContextCandidateGeneratedTest::run); + cases.put("ContextRequestGeneratedTest", ContextRequestGeneratedTest::run); + cases.put("CustomToolGeneratedTest", CustomToolGeneratedTest::run); + cases.put("DelegatedStateReferenceGeneratedTest", DelegatedStateReferenceGeneratedTest::run); + cases.put("DeviceAuthorizationGeneratedTest", DeviceAuthorizationGeneratedTest::run); + cases.put("DoneEventPayloadGeneratedTest", DoneEventPayloadGeneratedTest::run); + cases.put("EngineCheckpointGeneratedTest", EngineCheckpointGeneratedTest::run); + cases.put("EngineEventGeneratedTest", EngineEventGeneratedTest::run); + cases.put("EnginePermissionDecisionGeneratedTest", EnginePermissionDecisionGeneratedTest::run); + cases.put("ErrorChunkGeneratedTest", ErrorChunkGeneratedTest::run); + cases.put("ErrorEventPayloadGeneratedTest", ErrorEventPayloadGeneratedTest::run); + cases.put("FileNotFoundErrorGeneratedTest", FileNotFoundErrorGeneratedTest::run); + cases.put("FilePartGeneratedTest", FilePartGeneratedTest::run); + cases.put("FinalOutputPolicyRequestGeneratedTest", FinalOutputPolicyRequestGeneratedTest::run); + cases.put("FinalOutputPolicyResultGeneratedTest", FinalOutputPolicyResultGeneratedTest::run); + cases.put("FormatConfigGeneratedTest", FormatConfigGeneratedTest::run); + cases.put("FoundryConnectionGeneratedTest", FoundryConnectionGeneratedTest::run); + cases.put("FunctionToolGeneratedTest", FunctionToolGeneratedTest::run); + cases.put("GuardrailResultGeneratedTest", GuardrailResultGeneratedTest::run); + cases.put("HarnessContextGeneratedTest", HarnessContextGeneratedTest::run); + cases.put("HookEndPayloadGeneratedTest", HookEndPayloadGeneratedTest::run); + cases.put("HookStartPayloadGeneratedTest", HookStartPayloadGeneratedTest::run); + cases.put("HostPolicyRequestGeneratedTest", HostPolicyRequestGeneratedTest::run); + cases.put("HostPolicyResultGeneratedTest", HostPolicyResultGeneratedTest::run); + cases.put("HostToolRequestGeneratedTest", HostToolRequestGeneratedTest::run); + cases.put("HostToolResultGeneratedTest", HostToolResultGeneratedTest::run); + cases.put("ImagePartGeneratedTest", ImagePartGeneratedTest::run); + cases.put("InvocationContextDecisionGeneratedTest", InvocationContextDecisionGeneratedTest::run); + cases.put("InvocationContextStateGeneratedTest", InvocationContextStateGeneratedTest::run); + cases.put("InvocationUsageGeneratedTest", InvocationUsageGeneratedTest::run); + cases.put("InvokerErrorGeneratedTest", InvokerErrorGeneratedTest::run); + cases.put("LlmCompletePayloadGeneratedTest", LlmCompletePayloadGeneratedTest::run); + cases.put("LlmStartPayloadGeneratedTest", LlmStartPayloadGeneratedTest::run); + cases.put("McpApprovalModeGeneratedTest", McpApprovalModeGeneratedTest::run); + cases.put("McpToolGeneratedTest", McpToolGeneratedTest::run); + cases.put("MemoryEntryGeneratedTest", MemoryEntryGeneratedTest::run); + cases.put("MemoryStoreGeneratedTest", MemoryStoreGeneratedTest::run); + cases.put("MessageGeneratedTest", MessageGeneratedTest::run); + cases.put("MessagesUpdatedPayloadGeneratedTest", MessagesUpdatedPayloadGeneratedTest::run); + cases.put("ModelGeneratedTest", ModelGeneratedTest::run); + cases.put("ModelInfoGeneratedTest", ModelInfoGeneratedTest::run); + cases.put("ModelInvocationContextSnapshotGeneratedTest", ModelInvocationContextSnapshotGeneratedTest::run); + cases.put("ModelInvocationRequestGeneratedTest", ModelInvocationRequestGeneratedTest::run); + cases.put("ModelInvocationResponseGeneratedTest", ModelInvocationResponseGeneratedTest::run); + cases.put("ModelOptionsGeneratedTest", ModelOptionsGeneratedTest::run); + cases.put("ModelReconciliationStateGeneratedTest", ModelReconciliationStateGeneratedTest::run); + cases.put("ModelToolRequestGeneratedTest", ModelToolRequestGeneratedTest::run); + cases.put("ModelToolResultGeneratedTest", ModelToolResultGeneratedTest::run); + cases.put("OAuthConnectionGeneratedTest", OAuthConnectionGeneratedTest::run); + cases.put("OAuthTokenGeneratedTest", OAuthTokenGeneratedTest::run); + cases.put("ObjectPropertyGeneratedTest", ObjectPropertyGeneratedTest::run); + cases.put("OpenApiToolGeneratedTest", OpenApiToolGeneratedTest::run); + cases.put("ParserConfigGeneratedTest", ParserConfigGeneratedTest::run); + cases.put("PermissionCompletedPayloadGeneratedTest", PermissionCompletedPayloadGeneratedTest::run); + cases.put("PermissionDecisionGeneratedTest", PermissionDecisionGeneratedTest::run); + cases.put("PermissionRequestGeneratedTest", PermissionRequestGeneratedTest::run); + cases.put("PermissionRequestedPayloadGeneratedTest", PermissionRequestedPayloadGeneratedTest::run); + cases.put("ProjectInfoGeneratedTest", ProjectInfoGeneratedTest::run); + cases.put("PromptyGeneratedTest", PromptyGeneratedTest::run); + cases.put("PromptyToolGeneratedTest", PromptyToolGeneratedTest::run); + cases.put("PropertyGeneratedTest", PropertyGeneratedTest::run); + cases.put("RedactedFieldGeneratedTest", RedactedFieldGeneratedTest::run); + cases.put("RedactionMetadataGeneratedTest", RedactionMetadataGeneratedTest::run); + cases.put("ReferenceConnectionGeneratedTest", ReferenceConnectionGeneratedTest::run); + cases.put("RemoteConnectionGeneratedTest", RemoteConnectionGeneratedTest::run); + cases.put("ReplayJournalRecordGeneratedTest", ReplayJournalRecordGeneratedTest::run); + cases.put("ReplayMismatchGeneratedTest", ReplayMismatchGeneratedTest::run); + cases.put("ReplayVerificationRequestGeneratedTest", ReplayVerificationRequestGeneratedTest::run); + cases.put("ReplayVerificationResultGeneratedTest", ReplayVerificationResultGeneratedTest::run); + cases.put("ResumeContextGeneratedTest", ResumeContextGeneratedTest::run); + cases.put("RetryPayloadGeneratedTest", RetryPayloadGeneratedTest::run); + cases.put("RetryPolicyRequestGeneratedTest", RetryPolicyRequestGeneratedTest::run); + cases.put("RunTurnRequestGeneratedTest", RunTurnRequestGeneratedTest::run); + cases.put("RunTurnResultGeneratedTest", RunTurnResultGeneratedTest::run); + cases.put("SessionEndPayloadGeneratedTest", SessionEndPayloadGeneratedTest::run); + cases.put("SessionEventGeneratedTest", SessionEventGeneratedTest::run); + cases.put("SessionFileRefGeneratedTest", SessionFileRefGeneratedTest::run); + cases.put("SessionRefGeneratedTest", SessionRefGeneratedTest::run); + cases.put("SessionStartPayloadGeneratedTest", SessionStartPayloadGeneratedTest::run); + cases.put("SessionSummaryGeneratedTest", SessionSummaryGeneratedTest::run); + cases.put("SessionTraceGeneratedTest", SessionTraceGeneratedTest::run); + cases.put("SessionWarningPayloadGeneratedTest", SessionWarningPayloadGeneratedTest::run); + cases.put("StatusEventPayloadGeneratedTest", StatusEventPayloadGeneratedTest::run); + cases.put("StreamChunkGeneratedTest", StreamChunkGeneratedTest::run); + cases.put("StreamOptionsGeneratedTest", StreamOptionsGeneratedTest::run); + cases.put("SubscriptionInfoGeneratedTest", SubscriptionInfoGeneratedTest::run); + cases.put("TemplateGeneratedTest", TemplateGeneratedTest::run); + cases.put("TextChunkGeneratedTest", TextChunkGeneratedTest::run); + cases.put("TextPartGeneratedTest", TextPartGeneratedTest::run); + cases.put("ThinkingChunkGeneratedTest", ThinkingChunkGeneratedTest::run); + cases.put("ThinkingEventPayloadGeneratedTest", ThinkingEventPayloadGeneratedTest::run); + cases.put("ThreadMarkerGeneratedTest", ThreadMarkerGeneratedTest::run); + cases.put("TokenEventPayloadGeneratedTest", TokenEventPayloadGeneratedTest::run); + cases.put("TokenUsageGeneratedTest", TokenUsageGeneratedTest::run); + cases.put("ToolCallCompletePayloadGeneratedTest", ToolCallCompletePayloadGeneratedTest::run); + cases.put("ToolCallGeneratedTest", ToolCallGeneratedTest::run); + cases.put("ToolCallStartPayloadGeneratedTest", ToolCallStartPayloadGeneratedTest::run); + cases.put("ToolChunkGeneratedTest", ToolChunkGeneratedTest::run); + cases.put("ToolContextGeneratedTest", ToolContextGeneratedTest::run); + cases.put("ToolDispatchResultGeneratedTest", ToolDispatchResultGeneratedTest::run); + cases.put("ToolExecutionCompletePayloadGeneratedTest", ToolExecutionCompletePayloadGeneratedTest::run); + cases.put("ToolExecutionStartPayloadGeneratedTest", ToolExecutionStartPayloadGeneratedTest::run); + cases.put("ToolGeneratedTest", ToolGeneratedTest::run); + cases.put("ToolResultGeneratedTest", ToolResultGeneratedTest::run); + cases.put("ToolResultPayloadGeneratedTest", ToolResultPayloadGeneratedTest::run); + cases.put("TraceFileGeneratedTest", TraceFileGeneratedTest::run); + cases.put("TraceSpanGeneratedTest", TraceSpanGeneratedTest::run); + cases.put("TraceTimeGeneratedTest", TraceTimeGeneratedTest::run); + cases.put("TrajectoryEventGeneratedTest", TrajectoryEventGeneratedTest::run); + cases.put("TurnCommitGeneratedTest", TurnCommitGeneratedTest::run); + cases.put("TurnEndPayloadGeneratedTest", TurnEndPayloadGeneratedTest::run); + cases.put("TurnEngineResultGeneratedTest", TurnEngineResultGeneratedTest::run); + cases.put("TurnEventGeneratedTest", TurnEventGeneratedTest::run); + cases.put("TurnModelRequestGeneratedTest", TurnModelRequestGeneratedTest::run); + cases.put("TurnModelResponseGeneratedTest", TurnModelResponseGeneratedTest::run); + cases.put("TurnOptionsGeneratedTest", TurnOptionsGeneratedTest::run); + cases.put("TurnStartPayloadGeneratedTest", TurnStartPayloadGeneratedTest::run); + cases.put("TurnSummaryGeneratedTest", TurnSummaryGeneratedTest::run); + cases.put("TurnTraceGeneratedTest", TurnTraceGeneratedTest::run); + cases.put("UnionPropertyGeneratedTest", UnionPropertyGeneratedTest::run); + cases.put("UsageChunkGeneratedTest", UsageChunkGeneratedTest::run); + cases.put("ValidationErrorGeneratedTest", ValidationErrorGeneratedTest::run); + cases.put("ValidationResultGeneratedTest", ValidationResultGeneratedTest::run); + return cases; + } +} 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/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.json b/schema/package.json index a0d593116..ffc42fe33 100644 --- a/schema/package.json +++ b/schema/package.json @@ -6,7 +6,7 @@ "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", "build": "npm run format:tsp && npm run generate && npm run format:rust" }, diff --git a/schema/scripts/clean-java-output.mjs b/schema/scripts/clean-java-output.mjs new file mode 100644 index 000000000..3cfa08be1 --- /dev/null +++ b/schema/scripts/clean-java-output.mjs @@ -0,0 +1,40 @@ +// 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. + +import { readdirSync, rmSync, readFileSync, existsSync } from "node:fs"; +import { join } from "node:path"; + +const MARKER = "// "; + +const ROOTS = [ + join("..", "runtime", "java", "prompty", "src", "main", "java", "com", "microsoft", "prompty", "model"), + join("..", "runtime", "java", "prompty", "src", "test", "java", "com", "microsoft", "prompty", "model"), +]; + +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}`); + } + if (!readFileSync(path, "utf8").startsWith(MARKER)) { + throw new Error( + `Refusing to clean ${path}: it is missing the generated marker. ` + + "Generated output directories must not contain hand-written files.", + ); + } + rmSync(path); + } +} diff --git a/schema/scripts/normalize-java-output.mjs b/schema/scripts/normalize-java-output.mjs new file mode 100644 index 000000000..ca095062b --- /dev/null +++ b/schema/scripts/normalize-java-output.mjs @@ -0,0 +1,1166 @@ +// 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@0.4.2 emits source that does not +// compile and 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). +// +// Defects addressed (reported upstream): +// +// J1 `default` is a Java reserved word but is emitted verbatim as a field +// name on `Property`. +// J2 `@abstract` models are emitted as `abstract class` yet their own +// `load()` fallback calls `new X()`; the `Tool` dispatch also omits the +// wildcard `*` catch-all subtype. +// J3 Enum-typed fields are assigned raw `String` values in `@factory` +// bodies and in shorthand `load()` paths. +// J4 `Long`/`Double`/`Float` fields are initialized with `int` literals. +// J6 Enum type names are emitted in lowerCamelCase. +// J7 Enums are emitted package-private alongside a public class in the same +// compilation unit, which is not legal Java. +// J8 Derived-class `load()` never populates base-class properties, and +// re-declares the discriminator field so it shadows the base field. +// J9 Named collections are not normalized between their dictionary and list +// forms on load, and `collectionFormat` is not honoured on save. +// J10 Generated tests dot into `List<>` fields as if they were maps. +// J11 Generated tests double-escape expected string literals. +// J12 Generated tests compare object-valued fields against scalar wire values. +// J13 `Property.load` emits two consecutive `instanceof Number` branches, so +// the integer branch is unreachable. +// J14 Derived `save()` runs the `postSave` hook again after the base class has +// already run it. +// J15 `SaveContext` lacks the `collectionFormat` and `useShorthand` knobs the +// other backends expose. +// 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. +// +// Every rewrite below is structural and idempotent: running the emitter and +// this shim again from a clean tree produces byte-identical output. +// +// J5 — `@method` stubs are emitted without an extension seam (Rust emits a +// trait, C# emits partial `*Helpers` classes) — is deliberately NOT addressed +// here. Those helpers are hand-written in the runtime package instead, so the +// generated model layer stays untouched. + +import { existsSync, readdirSync, readFileSync, writeFileSync, unlinkSync } from "node:fs"; +import { join } from "node:path"; + +/** Subtypes selected when a discriminated union sees an unknown discriminator. */ +const WILDCARD_SUBTYPE = { Tool: "CustomTool" }; + +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 enumRenames = extractEnums(root, units); + applyRenames(units, enumRenames); + const enumNames = collectEnumNames(units); + const enumDefaults = collectEnumDefaults(units); + + const classes = indexClasses(units); + const described = describeClasses(classes); + const stats = { + namedDictionary: 0, + namedDictionarySave: 0, + derivedSave: 0, + scalarShorthand: 0, + loadBodies: 0, + optionalDefaults: 0, + requiredEnums: 0, + wildcard: 0, + misses: [], + problems: [], + }; + + for (const [name, unit] of units) { + let text = unit.text; + text = fixReservedFieldNames(text); + text = fixNumericDefaults(text); + text = clearOptionalDefaults(text, optional.get(name), stats); + text = fixRequiredEnumDefaults(text, optional.get(name), enumDefaults, stats); + text = fixDiscriminatorDispatch(text, name, stats); + text = fixEnumAssignments(text, classes.get(name), enumNames); + 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); + + reshapeLoadMethods(units, classes, stats); + assertRewritesApplied(stats, classes); + + for (const unit of units.values()) { + writeFileSync(unit.path, unit.text); + } + + return { renames: enumRenames, 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; +} + +// --------------------------------------------------------------------------- +// J6 + J7 — hoist enums into their own public compilation units +// --------------------------------------------------------------------------- + +function extractEnums(root, units) { + const renames = new Map(); + const header = "// \n// Code generated by Typra emitter; DO NOT EDIT.\n"; + + for (const unit of units.values()) { + const lines = unit.text.split("\n"); + const kept = []; + let index = 0; + while (index < lines.length) { + const match = /^enum (\w+) \{$/u.exec(lines[index]); + if (!match) { + kept.push(lines[index]); + index += 1; + continue; + } + const end = lines.indexOf("}", index); + const original = match[1]; + const renamed = original[0].toUpperCase() + original.slice(1); + if (renamed !== original) { + renames.set(original, renamed); + } + const body = lines.slice(index + 1, end).join("\n"); + const source = `${header}package com.microsoft.prompty.model;\n\npublic enum ${renamed} {\n${body}\n}\n`; + units.set(renamed, { path: join(root, `${renamed}.java`), text: source }); + // Drop the enum block plus the blank line that follows it. + index = end + 1; + if (lines[index] === "") { + index += 1; + } + } + unit.text = kept.join("\n"); + } + + return renames; +} + +function applyRenames(units, renames) { + if (renames.size === 0) { + return; + } + for (const unit of units.values()) { + let text = unit.text; + for (const [from, to] of renames) { + text = withLiteralsMasked(text, (code) => code.replace(new RegExp(`\\b${from}\\b`, "gu"), to)); + } + unit.text = text; + } +} + +function collectEnumNames(units) { + const names = new Set(); + for (const [name, unit] of units) { + if (/^public enum (\w+) \{$/mu.test(unit.text)) { + names.add(name); + } + } + return names; +} + +/** + * 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), + ); +} + +// --------------------------------------------------------------------------- +// J1 — reserved-word field names +// --------------------------------------------------------------------------- + +const RESERVED_FIELDS = { default: "defaultValue" }; + +function fixReservedFieldNames(text) { + return withLiteralsMasked(text, (code) => { + let result = code; + for (const [reserved, replacement] of Object.entries(RESERVED_FIELDS)) { + // Declaration: `public Object default = null;` + result = result.replace( + new RegExp(`^(\\s*public\\s+[\\w<>,\\[\\]. ]+?\\s+)${reserved}(\\s*=)`, "gmu"), + `$1${replacement}$2`, + ); + // Field access: `result.default`, `obj.default`, `this.default` + result = result.replace(new RegExp(`\\.${reserved}\\b(?!\\s*:)`, "gu"), `.${replacement}`); + } + return result; + }); +} + +/** + * Runs `transform` over the source with every string literal, text block, char + * literal and comment replaced by an opaque placeholder, then restores them. + * + * Rewrites that target Java identifiers must never see literal or comment text: + * `default` is both a reserved field name and a substring of real payload data + * (for example the OAuth scope `https://cognitiveservices.azure.com/.default`). + */ +function withLiteralsMasked(text, transform) { + const stash = []; + const masked = text.replace( + /"""[\s\S]*?"""|"(?:[^"\\\n]|\\.)*"|'(?:[^'\\]|\\.)*'|\/\*[\s\S]*?\*\/|\/\/[^\n]*/gu, + (match) => { + stash.push(match); + return `\u0000MASK${stash.length - 1}\u0000`; + }, + ); + return transform(masked).replace(/\u0000MASK(\d+)\u0000/gu, (_whole, index) => stash[Number(index)]); +} + +// --------------------------------------------------------------------------- +// J4 — boxed numeric literal defaults +// --------------------------------------------------------------------------- + +function fixNumericDefaults(text) { + return text + .replace(/^(\s*public Long \w+ = -?\d+)(;)$/gmu, "$1L$2") + .replace(/^(\s*public Double \w+ = -?\d+)(;)$/gmu, "$1.0$2") + .replace(/^(\s*public Float \w+ = -?\d+)(;)$/gmu, "$1f$2"); +} + +// --------------------------------------------------------------------------- +// J2 — discriminated-union dispatch semantics +// --------------------------------------------------------------------------- + +function fixDiscriminatorDispatch(text, className, stats) { + let result = text.replace( + /switch \(String\.valueOf\(discriminator\)\) \{/gu, + "switch (String.valueOf(discriminator).toLowerCase(java.util.Locale.ROOT)) {", + ); + + const wildcard = WILDCARD_SUBTYPE[className]; + if (wildcard && result.includes("Object discriminator = dispatchMap.get(")) { + result = result.replace( + /(\n\s*)default:\n\s*break;\n/u, + (whole, prefix) => { + stats.wildcard += 1; + return `${prefix}default:\n return ${wildcard}.load(data, ctx);\n`; + }, + ); + } + return result; +} + +// --------------------------------------------------------------------------- +// J3 — enum-typed fields assigned raw strings +// --------------------------------------------------------------------------- + +function fixEnumAssignments(text, info, enumNames) { + if (!info || info.enumFields.size === 0) { + return text; + } + let result = text; + for (const [field, type] of info.enumFields) { + if (!enumNames.has(type)) { + continue; + } + // Shorthand load path: `result.kind = String.valueOf(data);` + result = result.replace( + new RegExp(`(\\bresult\\.${field} = )String\\.valueOf\\(data\\);`, "gu"), + `$1${type}.fromValue(String.valueOf(data));`, + ); + // Factory bodies: `this.role = "assistant";` + result = result.replace( + new RegExp(`(\\bthis\\.${field} = )"([^"]*)";`, "gu"), + `$1${type}.fromValue("$2");`, + ); + } + return result; +} + +// --------------------------------------------------------------------------- +// 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)) { + 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 = []; + if (stats.misses.length > 0) { + problems.push( + `these classes declare load() but their field-population block was not recognised: ${stats.misses.join(", ")}`, + ); + } + 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, + wildcard: 1, + loadBodies: 100, +}; + +// --------------------------------------------------------------------------- +// J8 — base-property population and shadowed discriminator fields +// --------------------------------------------------------------------------- + +const LOAD_BODY_RE = + /(\n {4}if \(!\(data instanceof Map<\?, \?> map\)\) \{\n {6}return ctx\.processOutput\(new (\w+)\(\)\);\n {4}\}\n {4}\2 result = new \2\(\);\n)([\s\S]*?)(\n {4}return ctx\.processOutput\(result\);\n {2}\})/u; + +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; +} + +function reshapeLoadMethods(units, classes, stats) { + // Pass 1 — split each class's field-population block into `loadBaseInto`. + for (const [name, info] of classes) { + const unit = units.get(name); + const match = LOAD_BODY_RE.exec(unit.text); + if (!match) { + if (unit.text.includes(`public static ${name} load(Object input, LoadContext context) {`)) { + stats.misses.push(name); + } + continue; + } + const [whole, prologue, , body, epilogue] = match; + info.hasLoadBody = true; + stats.loadBodies += 1; + + const chain = info.base ? ` ${info.base}.loadBaseInto(result, map, ctx);\n` : ""; + + const helper = + `\n static void loadBaseInto(${name} result, Map map, LoadContext ctx) {\n` + + `${chain}${body.replace(/^\n/u, "")}\n }\n`; + + let replacement; + if (info.isAbstract) { + replacement = + `\n throw new IllegalArgumentException(` + + `"Missing ${name} discriminator property: 'kind'");\n }`; + } else { + replacement = + `${prologue} ${name}.loadBaseInto(result, map, ctx);${epilogue}`; + } + + unit.text = unit.text.replace(whole, `${replacement}${helper}`); + } + + // Pass 2 — remove shadowed field declarations, seeding defaults in the ctor. + for (const [name, info] of classes) { + if (!info.base) { + continue; + } + const baseInfo = classes.get(info.base); + if (!baseInfo) { + continue; + } + const unit = units.get(name); + let text = unit.text; + const seeds = []; + for (const [field, { type, initializer }] of info.fields) { + const baseField = baseInfo.fields.get(field); + if (!baseField || baseField.type !== type) { + continue; + } + text = text.replace( + new RegExp(`^ {2}public ${escapeRe(type)} ${field} = .+;\\n`, "mu"), + "", + ); + if (initializer !== "null") { + seeds.push(` this.${field} = ${initializer};`); + } + } + if (seeds.length > 0) { + text = text.replace( + new RegExp(`^ {2}public ${name}\\(\\) \\{ \\}$`, "mu"), + ` public ${name}() {\n${seeds.join("\n")}\n }`, + ); + } + unit.text = text; + } +} + +function escapeRe(value) { + return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"); +} + +// --------------------------------------------------------------------------- +// 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 renames = model?.renames ?? new Map(); + const classes = model?.classes ?? new Map(); + const stats = { doubleEscaped: 0 }; + + const runnable = []; + for (const name of readdirSync(root)) { + if (!name.endsWith("GeneratedTest.java")) { + continue; + } + const path = join(root, name); + let text = readFileSync(path, "utf8"); + + for (const [from, to] of renames) { + text = withLiteralsMasked(text, (code) => code.replace(new RegExp(`\\b${from}\\b`, "gu"), to)); + } + text = fixReservedFieldNames(text); + text = relaxEnumAssertions(text); + text = fixDoubleEscapedExpectations(text, stats); + text = dropStructuredAssertions(text, classes, name.replace(/GeneratedTest\.java$/u, "")); + + const body = /\n {2}static void run\(\) \{\n([\s\S]*?)\n {2}\}\n/u.exec(text); + if (!body || body[1].trim() === "") { + unlinkSync(path); + continue; + } + + writeFileSync(path, text); + runnable.push(name.replace(/\.java$/u, "")); + } + + runnable.sort(); + const entries = runnable.map((name) => ` cases.put("${name}", ${name}::run);`).join("\n"); + writeFileSync( + join(root, `${REGISTRY_CLASS}.java`), + "// \n" + + "// Code generated by Typra emitter; DO NOT EDIT.\n" + + "package com.microsoft.prompty.model;\n\n" + + "import java.util.LinkedHashMap;\n" + + "import java.util.Map;\n\n" + + `public final class ${REGISTRY_CLASS} {\n` + + ` private ${REGISTRY_CLASS}() { }\n\n` + + " public static Map all() {\n" + + " Map cases = new LinkedHashMap<>();\n" + + `${entries}\n` + + " return cases;\n" + + " }\n" + + "}\n", + ); + + // 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.`, + ); + } +} + +const MINIMUM_DOUBLE_ESCAPED = 5; + +const REGISTRY_CLASS = "GeneratedModelExamples"; + +/** + * 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. + */ +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) { + return text.replace( + /( {2}private static void assertEquals\(Object expected, Object actual, String message\) \{\n)/u, + "$1 expected = unwrapEnum(expected);\n actual = unwrapEnum(actual);\n", + ).replace( + /( {2}private static void assertEquals\(Object expected, Object actual, String message\) \{\n)/u, + " 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..847e6812c 100644 --- a/schema/tsp-output/.typra-generated/export-surfaces.json +++ b/schema/tsp-output/.typra-generated/export-surfaces.json @@ -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..a70745394 100644 --- a/schema/tsp-output/.typra-generated/manifest.json +++ b/schema/tsp-output/.typra-generated/manifest.json @@ -3053,6 +3053,1556 @@ "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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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" From c921c15792611531cede66c4435a9fee43c9e014 Mon Sep 17 00:00:00 2001 From: Seth Juarez Date: Tue, 4 Aug 2026 01:18:34 -0700 Subject: [PATCH 02/29] Add the Java runtime core: load, render, parse, prepare Implements the front half of the Prompty pipeline in Java, built on the Typra-emitted model package rather than a second hand-written model layer. Rust is the behavioural reference throughout. Loader/Frontmatter/References .prompty parsing, ${env:} and ${file:} resolution, legacy migration Nonces/Threads rich-input markers and thread expansion Registry/PromptyExtension ServiceLoader-based invoker discovery Renderer/Parser/Executor/ the four extension protocols Processor Pipeline validate, render, parse, prepare, process, run, invoke Tracer/Streams/StructuredResult/CancellationToken Conformance is proven against the shared cross-runtime vectors: 25 load, 23 render and 15 parse cases all pass, alongside the generated-model example suite. 264 tests, 0 failures. Four deliberate divergences from Rust, each strictly more correct: * parse() dispatches through the registered parser instead of short-circuiting on "prompty", which in Rust skips nonce validation. * Role markers resolve case-insensitively; Rust silently reads "User:" as system. * Thread history accepts both string and part-list content. * Only thread markers are expanded during parsing. Image, file and audio markers survive to wire conversion as spec section 5.2 requires; Rust swallows them. References inside lists are now resolved too, which the spec requires and Rust misses. Environment adds an override layer because a JVM cannot mutate its own environment the way the Rust harness does. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../microsoft/prompty/CancellationToken.java | 91 +++++ .../com/microsoft/prompty/Environment.java | 56 +++ .../java/com/microsoft/prompty/Executor.java | 147 +++++++ .../com/microsoft/prompty/Frontmatter.java | 170 ++++++++ .../microsoft/prompty/InvokerException.java | 157 +++++++ .../com/microsoft/prompty/LoadException.java | 81 ++++ .../com/microsoft/prompty/LoadOptions.java | 29 ++ .../java/com/microsoft/prompty/Loader.java | 249 +++++++++++ .../java/com/microsoft/prompty/Messages.java | 161 ++++++++ .../java/com/microsoft/prompty/Nonces.java | 89 ++++ .../java/com/microsoft/prompty/Parser.java | 40 ++ .../java/com/microsoft/prompty/Pipeline.java | 334 +++++++++++++++ .../java/com/microsoft/prompty/Processor.java | 153 +++++++ .../microsoft/prompty/PromptyExtension.java | 34 ++ .../com/microsoft/prompty/References.java | 222 ++++++++++ .../java/com/microsoft/prompty/Registry.java | 215 ++++++++++ .../java/com/microsoft/prompty/Renderer.java | 24 ++ .../java/com/microsoft/prompty/Streams.java | 195 +++++++++ .../microsoft/prompty/StructuredResult.java | 113 +++++ .../java/com/microsoft/prompty/Threads.java | 156 +++++++ .../java/com/microsoft/prompty/Tracer.java | 110 +++++ .../prompty/parsers/PromptyChatParser.java | 222 ++++++++++ .../prompty/renderers/JinjaRenderer.java | 60 +++ .../prompty/renderers/MustacheRenderer.java | 48 +++ .../microsoft/prompty/LoadVectorsTest.java | 207 ++++++++++ .../microsoft/prompty/ParseVectorsTest.java | 113 +++++ .../microsoft/prompty/RenderVectorsTest.java | 115 ++++++ .../com/microsoft/prompty/RuntimeTest.java | 385 ++++++++++++++++++ .../com/microsoft/prompty/SpecVectors.java | 249 +++++++++++ 29 files changed, 4225 insertions(+) create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/CancellationToken.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/Environment.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/Executor.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/Frontmatter.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/InvokerException.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/LoadException.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/LoadOptions.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/Loader.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/Messages.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/Nonces.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/Parser.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/Pipeline.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/Processor.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/PromptyExtension.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/References.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/Registry.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/Renderer.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/Streams.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/StructuredResult.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/Threads.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/Tracer.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/parsers/PromptyChatParser.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/renderers/JinjaRenderer.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/renderers/MustacheRenderer.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/LoadVectorsTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/ParseVectorsTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/RenderVectorsTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/RuntimeTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/SpecVectors.java 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/Environment.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Environment.java new file mode 100644 index 000000000..278490096 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Environment.java @@ -0,0 +1,56 @@ +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. + */ +public final class Environment { + + private static final Map OVERRIDES = new ConcurrentHashMap<>(); + + private Environment() {} + + /** Supply a value for {@code name}, taking precedence over system properties and the process environment. */ + public static void set(String name, String value) { + if (value == null) { + OVERRIDES.remove(name); + } else { + OVERRIDES.put(name, value); + } + } + + /** Remove a value previously supplied through {@link #set}. */ + public static void clear(String name) { + OVERRIDES.remove(name); + } + + /** Remove every value previously supplied through {@link #set}. */ + public static void clearAll() { + OVERRIDES.clear(); + } + + /** Look up {@code name}, or an empty optional if it is set nowhere. */ + public static Optional lookup(String name) { + String override = OVERRIDES.get(name); + if (override != null) { + return Optional.of(override); + } + 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/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/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/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..11ac7b927 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Pipeline.java @@ -0,0 +1,334 @@ +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 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); + } + + // ---------------------------------------------------------------- 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/Streams.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Streams.java new file mode 100644 index 000000000..83f6b8000 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Streams.java @@ -0,0 +1,195 @@ +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) {} + + /** + * 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. + */ + public static Iterator cancellable(Iterator source, CancellationToken cancellation) { + return new Iterator<>() { + @Override + public boolean hasNext() { + return !cancellation.isCancelled() && source.hasNext(); + } + + @Override + public T next() { + if (cancellation.isCancelled()) { + throw new NoSuchElementException("stream cancelled"); + } + return source.next(); + } + }; + } + + /** + * 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 Iterator<>() { + @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; + } + + 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/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/parsers/PromptyChatParser.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/parsers/PromptyChatParser.java new file mode 100644 index 000000000..cab4c8633 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/parsers/PromptyChatParser.java @@ -0,0 +1,222 @@ +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 { + + 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); + // An all-digit nonce is coerced to a number by attribute parsing, so compare as text. + 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()) { + attributes.put(matcher.group(1), coerce(matcher.group(2).trim())); + } + 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/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..e9e019044 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/LoadVectorsTest.java @@ -0,0 +1,207 @@ +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 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 { + + @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(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 env) { + List keys = new ArrayList<>(); + for (Map.Entry entry : env.entrySet()) { + Environment.set(entry.getKey(), String.valueOf(entry.getValue())); + keys.add(entry.getKey()); + } + 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/ParseVectorsTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/ParseVectorsTest.java new file mode 100644 index 000000000..7495be3c0 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/ParseVectorsTest.java @@ -0,0 +1,113 @@ +package com.microsoft.prompty; + +import static org.junit.jupiter.api.Assertions.assertEquals; +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"); + } +} 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..84cd57a60 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/RuntimeTest.java @@ -0,0 +1,385 @@ +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 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 { + + // ---------------------------------------------------------------- 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() { + Environment.set("PROMPTY_TEST_VALUE", "explicit"); + try { + assertEquals("explicit", Environment.lookup("PROMPTY_TEST_VALUE").orElseThrow()); + } finally { + Environment.clear("PROMPTY_TEST_VALUE"); + } + assertTrue(Environment.lookup("PROMPTY_TEST_VALUE").isEmpty(), "clearing should remove the value"); + } + + // ---------------------------------------------------------------- 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/SpecVectors.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/SpecVectors.java new file mode 100644 index 000000000..be4de1e1b --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/SpecVectors.java @@ -0,0 +1,249 @@ +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.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; + } + + /** 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 is absent or null, which is how a vector says "this + * field should not be populated" without having to describe the whole surrounding object. + * + *

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); + } + + private static void assertMatches(String label, String path, Object expected, Object actual) { + String where = label + (path.isEmpty() ? "" : " at " + path); + + if (expected == null) { + assertTrue(actual == null, where + ": expected absent, got " + 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; + } + + // ---------------------------------------------------------------- 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() + "\""); + } +} From 99298454a5725d67e372e87e49ce9a97449bf6b9 Mon Sep 17 00:00:00 2001 From: Seth Juarez Date: Tue, 4 Aug 2026 01:48:31 -0700 Subject: [PATCH 03/29] Add the OpenAI provider: wire, executor, processor Prompty speaks to a model through three separable pieces: a wire layer that turns the loaded prompt into a request body, an executor that sends it, and a processor that turns whatever comes back into a result. Splitting them this way means the two interesting halves - request shaping and response interpretation - are pure functions that can be graded against the shared spec vectors without a network, and the executor is left holding only the parts that genuinely need one. The wire layer covers all four API shapes (chat, responses, embedding, image), tool declarations, and structured output. Model options are not remapped here: the generated model already knows how each option is named per provider, so provider code asks it rather than keeping a second copy of that table. The processor handles both single responses and streams. Streaming is where the shape of the API matters most - text is forwarded as it arrives, tool calls are accumulated across deltas and emitted only once whole, and usage comes last, so a caller that stops at the first tool call has still seen everything that matters. Two deliberate departures from the Rust reference, both tested: - Streamed function-call arguments are routed by call_id, then item_id, then output_index. Rust matches on call_id alone, but the live Responses API sends item_id and output_index and no call_id, so streamed arguments are dropped there and only recovered because the terminal event repeats them. - Every tool call gets a result message even when the caller supplies fewer results than calls. OpenAI rejects a conversation with an unanswered call, so an empty answer beats an absent one. Transport lives in the core module rather than here, because Anthropic and Foundry need the same failure classification: whether a request definitely never reached the provider decides whether retrying it is safe or risks acting twice. Streams are closeable through every wrapper, so cancellation and refusal release the connection instead of leaving it checked out of the pool. The spec-vector harness moved to test fixtures so provider modules share one comparison implementation. 22 wire vectors and 17 process vectors now run against Java, matching the Rust suites case for case; 335 tests pass. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- runtime/java/prompty-openai/build.gradle.kts | 1 + .../prompty/openai/OpenAIExecutor.java | 378 +++++++++++ .../prompty/openai/OpenAIExtension.java | 17 + .../prompty/openai/OpenAIProcessor.java | 632 +++++++++++++++++ .../prompty/openai/SchemaException.java | 30 + .../com/microsoft/prompty/openai/Wire.java | 636 ++++++++++++++++++ .../com.microsoft.prompty.PromptyExtension | 1 + .../prompty/openai/OpenAIStreamingTest.java | 312 +++++++++ .../prompty/openai/ProcessVectorsTest.java | 64 ++ .../prompty/openai/WireSchemaTest.java | 241 +++++++ .../prompty/openai/WireVectorsTest.java | 127 ++++ runtime/java/prompty/build.gradle.kts | 6 + .../com/microsoft/prompty/Connections.java | 81 +++ .../main/java/com/microsoft/prompty/Http.java | 223 ++++++ .../com/microsoft/prompty/StreamFailure.java | 50 ++ .../java/com/microsoft/prompty/Streams.java | 57 +- .../microsoft/prompty/ConnectionsTest.java | 92 +++ .../java/com/microsoft/prompty/HttpTest.java | 219 ++++++ .../com/microsoft/prompty/SpecVectors.java | 0 19 files changed, 3163 insertions(+), 4 deletions(-) create mode 100644 runtime/java/prompty-openai/src/main/java/com/microsoft/prompty/openai/OpenAIExecutor.java create mode 100644 runtime/java/prompty-openai/src/main/java/com/microsoft/prompty/openai/OpenAIExtension.java create mode 100644 runtime/java/prompty-openai/src/main/java/com/microsoft/prompty/openai/OpenAIProcessor.java create mode 100644 runtime/java/prompty-openai/src/main/java/com/microsoft/prompty/openai/SchemaException.java create mode 100644 runtime/java/prompty-openai/src/main/java/com/microsoft/prompty/openai/Wire.java create mode 100644 runtime/java/prompty-openai/src/main/resources/META-INF/services/com.microsoft.prompty.PromptyExtension create mode 100644 runtime/java/prompty-openai/src/test/java/com/microsoft/prompty/openai/OpenAIStreamingTest.java create mode 100644 runtime/java/prompty-openai/src/test/java/com/microsoft/prompty/openai/ProcessVectorsTest.java create mode 100644 runtime/java/prompty-openai/src/test/java/com/microsoft/prompty/openai/WireSchemaTest.java create mode 100644 runtime/java/prompty-openai/src/test/java/com/microsoft/prompty/openai/WireVectorsTest.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/Connections.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/Http.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/StreamFailure.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/ConnectionsTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/HttpTest.java rename runtime/java/prompty/src/{test => testFixtures}/java/com/microsoft/prompty/SpecVectors.java (100%) diff --git a/runtime/java/prompty-openai/build.gradle.kts b/runtime/java/prompty-openai/build.gradle.kts index 090b62393..fbd3a0d9d 100644 --- a/runtime/java/prompty-openai/build.gradle.kts +++ b/runtime/java/prompty-openai/build.gradle.kts @@ -7,5 +7,6 @@ dependencies { 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..f343babe1 --- /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 = endpoint.endsWith("/") ? endpoint.substring(0, endpoint.length() - 1) : 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/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..7e1545747 --- /dev/null +++ b/runtime/java/prompty-openai/src/main/java/com/microsoft/prompty/openai/Wire.java @@ -0,0 +1,636 @@ +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)); + if (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/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..627654345 --- /dev/null +++ b/runtime/java/prompty-openai/src/test/java/com/microsoft/prompty/openai/ProcessVectorsTest.java @@ -0,0 +1,64 @@ +package com.microsoft.prompty.openai; + +import com.microsoft.prompty.SpecVectors; +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 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 = buildAgent(input); + 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.assertMatches(name, expected, actual); + })); + } + return tests; + } + + /** + * Build the agent the vector implies. + * + *

Only {@code has_outputs} matters here: declaring outputs is what makes the processor attempt + * to decode structured JSON rather than hand back text. + */ + private static Prompty buildAgent(Map input) { + 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")); + 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()); + } +} 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..a8e0034e2 --- /dev/null +++ b/runtime/java/prompty-openai/src/test/java/com/microsoft/prompty/openai/WireSchemaTest.java @@ -0,0 +1,241 @@ +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); + } + + @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); + } + + @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")); + // Nested objects list only their genuinely required members, matching every other runtime. + // OpenAI's strict mode actually wants every key listed at every level, so a nested optional + // yields a schema the API rejects — but that rule lives in the shared spec, so it is reported + // upstream rather than fixed in one runtime, which would break cross-runtime agreement. + assertEquals(List.of("color", "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")); + assertEquals( + List.of("theme"), + 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..dd0c05e34 --- /dev/null +++ b/runtime/java/prompty-openai/src/test/java/com/microsoft/prompty/openai/WireVectorsTest.java @@ -0,0 +1,127 @@ +package com.microsoft.prompty.openai; + +import com.microsoft.prompty.SpecVectors; +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 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 = buildAgent(input); + List messages = 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.assertMatches(name, expected, actual); + } + + /** Rebuild the agent from the vector's declarative description. */ + private static Prompty buildAgent(Map input) { + Map model = new LinkedHashMap<>(); + model.put("id", input.getOrDefault("model_id", "gpt-4")); + model.put("apiType", input.getOrDefault("apiType", "chat")); + model.put("provider", input.getOrDefault("provider", "openai")); + 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 the vector describes. + * + *

The fixtures name every part's payload {@code value}, while the model names it {@code source} + * for the parts that reference external media. Translating here keeps the fixtures uniform across + * runtimes and confines the difference to the harness, as the Rust suite also does. + */ + private 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; + } + + private static Map normalizePart(Map part) { + Map normalized = new LinkedHashMap<>(); + for (Map.Entry entry : part.entrySet()) { + String key = String.valueOf(entry.getKey()); + boolean isText = "text".equals(part.get("kind")); + normalized.put("value".equals(key) && !isText ? "source" : key, entry.getValue()); + } + return normalized; + } +} diff --git a/runtime/java/prompty/build.gradle.kts b/runtime/java/prompty/build.gradle.kts index 1d6331faf..d64e1050c 100644 --- a/runtime/java/prompty/build.gradle.kts +++ b/runtime/java/prompty/build.gradle.kts @@ -1,5 +1,8 @@ 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 { @@ -16,4 +19,7 @@ dependencies { 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/Connections.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Connections.java new file mode 100644 index 000000000..b4834b5a0 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Connections.java @@ -0,0 +1,81 @@ +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; + } +} 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..fa8317f6b --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Http.java @@ -0,0 +1,223 @@ +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.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")); + } + } + + /** + * 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()); + } + + 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/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 index 83f6b8000..129aa3e39 100644 --- a/runtime/java/prompty/src/main/java/com/microsoft/prompty/Streams.java +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Streams.java @@ -20,23 +20,48 @@ 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. + * valid. The source is released on cancellation, since nothing will read from it again. */ public static Iterator cancellable(Iterator source, CancellationToken cancellation) { - return new Iterator<>() { + return new ClosingIterator(source) { @Override public boolean hasNext() { - return !cancellation.isCancelled() && source.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(); @@ -44,13 +69,37 @@ public T 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 Iterator<>() { + return new ClosingIterator(source) { @Override public boolean hasNext() { return source.hasNext(); 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/HttpTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/HttpTest.java new file mode 100644 index 000000000..24370821f --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/HttpTest.java @@ -0,0 +1,219 @@ +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; + } + + @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/SpecVectors.java b/runtime/java/prompty/src/testFixtures/java/com/microsoft/prompty/SpecVectors.java similarity index 100% rename from runtime/java/prompty/src/test/java/com/microsoft/prompty/SpecVectors.java rename to runtime/java/prompty/src/testFixtures/java/com/microsoft/prompty/SpecVectors.java From 30f323c5b8ea16d2d8dd9373035cd75bec1c5bd5 Mon Sep 17 00:00:00 2001 From: Seth Juarez Date: Tue, 4 Aug 2026 02:32:39 -0700 Subject: [PATCH 04/29] Add the Anthropic provider: wire, executor, processor Anthropic's Messages API differs from OpenAI's in ways that rule out sharing the conversion, so this is a sibling module rather than an adapter: system messages leave the conversation and become a top-level string, content is always an array of typed blocks, tools are flat rather than nested under a function wrapper, max_tokens is required, and a tool round replays as exactly two messages instead of one per result. Assistant turns carry their raw content blocks in metadata. Thinking blocks are signed, and a signature only validates against the exact bytes Anthropic produced, so a replay rebuilt from message text would be rejected. Two deliberate divergences from the Rust reference, both documented in place: buildUrl honours ANTHROPIC_BASE_URL and collapses a duplicate /v1, matching the official SDKs and Rust's own OpenAI executor; and a mid-stream `error` event surfaces an ErrorChunk and releases the transport rather than being skipped, since Anthropic emits these for overload and ignoring one hands the caller a truncated answer that looks complete. Also strengthens the shared vector harness, which stands behind every suite in the runtime. It compared only the keys a vector mentioned, so a runtime could have added a field to every request without a test noticing, and it accepted an absent key wherever a vector stated an explicit null. Both now fail, matching the reference comparison but for its one deliberate relaxation: numbers are compared to single precision, because a 32-bit temperature cannot hold a value like 0.7 that a vector states in full. SpecVectorsTest grades the harness itself, and the vector rebuild logic the provider suites had begun to duplicate moves into a shared VectorAgents fixture. Endpoint normalization moves to Connections.trimTrailingSlashes, which strips every trailing slash rather than one; both providers left `host//v1` behind for an endpoint typed with two. 410 tests, 0 failures. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../java/prompty-anthropic/build.gradle.kts | 1 + .../prompty/anthropic/AnthropicExecutor.java | 193 +++++ .../prompty/anthropic/AnthropicExtension.java | 19 + .../prompty/anthropic/AnthropicProcessor.java | 368 +++++++++ .../prompty/anthropic/SchemaException.java | 18 + .../com/microsoft/prompty/anthropic/Wire.java | 743 ++++++++++++++++++ .../com.microsoft.prompty.PromptyExtension | 1 + .../anthropic/AnthropicExecutorTest.java | 146 ++++ .../anthropic/AnthropicStreamingTest.java | 303 +++++++ .../prompty/anthropic/ProcessVectorsTest.java | 46 ++ .../prompty/anthropic/WireMessageTest.java | 344 ++++++++ .../prompty/anthropic/WireSchemaTest.java | 337 ++++++++ .../prompty/anthropic/WireVectorsTest.java | 52 ++ .../prompty/openai/OpenAIExecutor.java | 2 +- .../prompty/openai/ProcessVectorsTest.java | 28 +- .../prompty/openai/WireVectorsTest.java | 80 +- .../com/microsoft/prompty/Connections.java | 18 + .../java/com/microsoft/prompty/Streams.java | 26 + .../microsoft/prompty/SpecVectorsTest.java | 112 +++ .../com/microsoft/prompty/SpecVectors.java | 66 ++ .../com/microsoft/prompty/VectorAgents.java | 118 +++ 21 files changed, 2920 insertions(+), 101 deletions(-) create mode 100644 runtime/java/prompty-anthropic/src/main/java/com/microsoft/prompty/anthropic/AnthropicExecutor.java create mode 100644 runtime/java/prompty-anthropic/src/main/java/com/microsoft/prompty/anthropic/AnthropicExtension.java create mode 100644 runtime/java/prompty-anthropic/src/main/java/com/microsoft/prompty/anthropic/AnthropicProcessor.java create mode 100644 runtime/java/prompty-anthropic/src/main/java/com/microsoft/prompty/anthropic/SchemaException.java create mode 100644 runtime/java/prompty-anthropic/src/main/java/com/microsoft/prompty/anthropic/Wire.java create mode 100644 runtime/java/prompty-anthropic/src/main/resources/META-INF/services/com.microsoft.prompty.PromptyExtension create mode 100644 runtime/java/prompty-anthropic/src/test/java/com/microsoft/prompty/anthropic/AnthropicExecutorTest.java create mode 100644 runtime/java/prompty-anthropic/src/test/java/com/microsoft/prompty/anthropic/AnthropicStreamingTest.java create mode 100644 runtime/java/prompty-anthropic/src/test/java/com/microsoft/prompty/anthropic/ProcessVectorsTest.java create mode 100644 runtime/java/prompty-anthropic/src/test/java/com/microsoft/prompty/anthropic/WireMessageTest.java create mode 100644 runtime/java/prompty-anthropic/src/test/java/com/microsoft/prompty/anthropic/WireSchemaTest.java create mode 100644 runtime/java/prompty-anthropic/src/test/java/com/microsoft/prompty/anthropic/WireVectorsTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/SpecVectorsTest.java create mode 100644 runtime/java/prompty/src/testFixtures/java/com/microsoft/prompty/VectorAgents.java diff --git a/runtime/java/prompty-anthropic/build.gradle.kts b/runtime/java/prompty-anthropic/build.gradle.kts index 090b62393..fbd3a0d9d 100644 --- a/runtime/java/prompty-anthropic/build.gradle.kts +++ b/runtime/java/prompty-anthropic/build.gradle.kts @@ -7,5 +7,6 @@ dependencies { 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/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..6b02a8fb0 --- /dev/null +++ b/runtime/java/prompty-anthropic/src/test/java/com/microsoft/prompty/anthropic/AnthropicExecutorTest.java @@ -0,0 +1,146 @@ +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() { + 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/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/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-openai/src/main/java/com/microsoft/prompty/openai/OpenAIExecutor.java b/runtime/java/prompty-openai/src/main/java/com/microsoft/prompty/openai/OpenAIExecutor.java index f343babe1..6d88668c0 100644 --- 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 @@ -308,7 +308,7 @@ protected String buildUrl(Prompty agent, String path) { endpoint = Environment.lookup("OPENAI_BASE_URL").filter(v -> !v.isEmpty()).orElse(DEFAULT_ENDPOINT); } - String base = endpoint.endsWith("/") ? endpoint.substring(0, endpoint.length() - 1) : 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")) { 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 index 627654345..31e321e23 100644 --- 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 @@ -1,10 +1,9 @@ package com.microsoft.prompty.openai; import com.microsoft.prompty.SpecVectors; -import com.microsoft.prompty.model.LoadContext; +import com.microsoft.prompty.VectorAgents; import com.microsoft.prompty.model.Prompty; import java.util.ArrayList; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import org.junit.jupiter.api.DynamicTest; @@ -16,8 +15,7 @@ class ProcessVectorsTest { @TestFactory Iterable processVectors() { List tests = new ArrayList<>(); - for (Map vector : - SpecVectors.readArray("process/process_vectors.json")) { + for (Map vector : SpecVectors.readArray("process/process_vectors.json")) { String name = SpecVectors.string(vector, "name"); Map input = SpecVectors.map(vector, "input"); @@ -29,7 +27,7 @@ Iterable processVectors() { DynamicTest.dynamicTest( name, () -> { - Prompty agent = buildAgent(input); + Prompty agent = VectorAgents.buildProcessAgent(input, "gpt-4", "openai"); Object actual = OpenAIProcessor.processResponse(agent, input.get("response")); Object expected = SpecVectors.map(vector, "expected").get("result"); @@ -38,27 +36,9 @@ Iterable processVectors() { if ("".equals(expected) && (actual == null || "".equals(actual))) { return; } - SpecVectors.assertMatches(name, expected, actual); + SpecVectors.assertEquivalent(name, expected, actual); })); } return tests; } - - /** - * Build the agent the vector implies. - * - *

Only {@code has_outputs} matters here: declaring outputs is what makes the processor attempt - * to decode structured JSON rather than hand back text. - */ - private static Prompty buildAgent(Map input) { - 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")); - 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()); - } } 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 index dd0c05e34..cb18d2514 100644 --- 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 @@ -1,12 +1,10 @@ package com.microsoft.prompty.openai; import com.microsoft.prompty.SpecVectors; -import com.microsoft.prompty.model.LoadContext; +import com.microsoft.prompty.VectorAgents; 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 org.junit.jupiter.api.DynamicTest; @@ -38,8 +36,8 @@ Iterable wireVectors() { } private static void runVector(String name, Map vector, Map input) { - Prompty agent = buildAgent(input); - List messages = buildMessages(input); + Prompty agent = VectorAgents.buildAgent(input, "gpt-4", "openai"); + List messages = VectorAgents.buildMessages(input); String apiType = String.valueOf(input.getOrDefault("apiType", "chat")); Map actual = @@ -52,76 +50,6 @@ private static void runVector(String name, Map vector, Map input) { - Map model = new LinkedHashMap<>(); - model.put("id", input.getOrDefault("model_id", "gpt-4")); - model.put("apiType", input.getOrDefault("apiType", "chat")); - model.put("provider", input.getOrDefault("provider", "openai")); - 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 the vector describes. - * - *

The fixtures name every part's payload {@code value}, while the model names it {@code source} - * for the parts that reference external media. Translating here keeps the fixtures uniform across - * runtimes and confines the difference to the harness, as the Rust suite also does. - */ - private 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; - } - - private static Map normalizePart(Map part) { - Map normalized = new LinkedHashMap<>(); - for (Map.Entry entry : part.entrySet()) { - String key = String.valueOf(entry.getKey()); - boolean isText = "text".equals(part.get("kind")); - normalized.put("value".equals(key) && !isText ? "source" : key, entry.getValue()); - } - return normalized; + SpecVectors.assertEquivalent(name, expected, actual); } } 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 index b4834b5a0..746758619 100644 --- a/runtime/java/prompty/src/main/java/com/microsoft/prompty/Connections.java +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Connections.java @@ -78,4 +78,22 @@ public static Connection resolve(Connection connection) { } 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/Streams.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Streams.java index 129aa3e39..94bbee075 100644 --- a/runtime/java/prompty/src/main/java/com/microsoft/prompty/Streams.java +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Streams.java @@ -238,6 +238,32 @@ public static Object pointer(Object root, Object... path) { 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/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..f1db036b7 --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/SpecVectorsTest.java @@ -0,0 +1,112 @@ +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)); + } +} 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 index be4de1e1b..116fb4a0e 100644 --- a/runtime/java/prompty/src/testFixtures/java/com/microsoft/prompty/SpecVectors.java +++ b/runtime/java/prompty/src/testFixtures/java/com/microsoft/prompty/SpecVectors.java @@ -94,6 +94,11 @@ public static String string(Map value, String key) { * null asserts only that the actual value is absent or null, which is how a vector says "this * field should not be populated" without having to describe the whole surrounding object. * + *

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}. */ @@ -101,6 +106,67 @@ 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 one deliberate relaxation: 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. 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); 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; + } +} From bec88289840300d6a355747e7ba674b3fe8fbc53 Mon Sep 17 00:00:00 2001 From: Seth Juarez Date: Tue, 4 Aug 2026 03:15:12 -0700 Subject: [PATCH 05/29] Add the turn engine: iterations, tool rounds, durability, resume The turn engine is the part of the runtime that survives a crash. It drives the model/tool loop, and after every effect it writes a checkpoint plus a journal entry so a host can resume mid-turn instead of replaying a turn that already spent money and ran side effects. Ported from the Rust engine, which is the behavioural reference. The five shared vectors in spec/vectors/engine/turn_vectors.json now run against Java, asserting the same statuses, snapshots, tool ordering, portability, and the exact ordered event stream that Rust asserts. Two things worth calling out: Resume arithmetic. Whether a resumed run continues the current iteration or starts the next one depends on whether anything is still outstanding. Rust clears the reconciliation flag on a cloned checkpoint before computing that, so the cleared flag participates in the decision; a direct reading of the public API would compute it from the stale flag and land an iteration early. Java mirrors the Rust ordering through a private overload. The flag only changes the answer for a checkpoint that holds no model response, so the two tests that guard it construct exactly that shape - an end-to-end assertion passes for an unrelated reason. Run identity. Rust takes the request by value, so an engine-assigned run id can never be seen by 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. Every durable path is covered by a gapless-sequence invariant: a hole in the journal means an effect was recorded that a resumed run would never replay. Each new guard was proven non-vacuous by perturbing the behaviour it claims to protect and confirming it fails. 446 tests, 0 failures. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../prompty/engine/ContextException.java | 22 + .../prompty/engine/ContextPipeline.java | 222 ++++ .../prompty/engine/DefaultPorts.java | 154 +++ .../prompty/engine/HostPolicyException.java | 25 + .../prompty/engine/ModelStreamChunk.java | 21 + .../prompty/engine/PortException.java | 77 ++ .../com/microsoft/prompty/engine/Ports.java | 111 ++ .../prompty/engine/RetryPolicyException.java | 35 + .../microsoft/prompty/engine/Snapshots.java | 79 ++ .../microsoft/prompty/engine/ToolResults.java | 39 + .../microsoft/prompty/engine/TurnEngine.java | 1052 ++++++++++++++++ .../prompty/engine/TurnEngineEffects.java | 78 ++ .../prompty/engine/TurnEngineException.java | 97 ++ .../prompty/engine/TurnEngineRequest.java | 355 ++++++ .../microsoft/prompty/engine/TurnState.java | 153 +++ .../prompty/engine/EngineTurnVectorsTest.java | 387 ++++++ .../prompty/engine/TurnEngineTest.java | 1066 +++++++++++++++++ 17 files changed, 3973 insertions(+) create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/ContextException.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/ContextPipeline.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/DefaultPorts.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/HostPolicyException.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/ModelStreamChunk.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/PortException.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/Ports.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/RetryPolicyException.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/Snapshots.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/ToolResults.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/TurnEngine.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/TurnEngineEffects.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/TurnEngineException.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/TurnEngineRequest.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/engine/TurnState.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/engine/EngineTurnVectorsTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/engine/TurnEngineTest.java 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/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); + } + } + } +} From 2040e4c99f0f4ad76e07ab3a922ecbf69a83cb04 Mon Sep 17 00:00:00 2001 From: Seth Juarez Date: Tue, 4 Aug 2026 04:02:44 -0700 Subject: [PATCH 06/29] Keep nonce attributes verbatim so all-digit nonces still validate Strict-mode parsing stamps a random hex nonce onto every role marker and rejects any marker that cannot reproduce it, so text injected through a template variable cannot pretend to be a new turn. Attribute values were being coerced to numbers wherever they parsed as one. Roughly one nonce in eighteen thousand comes out as digits only with a leading zero, and coercing that to a Long drops the zero, so the nonce no longer matched the one that had been stamped and a perfectly ordinary render was rejected as a prompt injection. It surfaced as an agent vector failing once every few runs and passing on a rerun. The nonce is an opaque token rather than data, so it is now captured as written. Other attributes still coerce, which is what the parse vectors expect. The Rust reference converts the coerced number back to text, which does not restore the lost zero, so it still carries this fault. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../prompty/parsers/PromptyChatParser.java | 11 +++++-- .../microsoft/prompty/ParseVectorsTest.java | 32 +++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) 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 index cab4c8633..273c03970 100644 --- 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 @@ -140,7 +140,7 @@ private static Message buildMessage( Role role, String content, Map attributes, String expectedNonce) { if (expectedNonce != null) { Object raw = attributes.get(NONCE_KEY); - // An all-digit nonce is coerced to a number by attribute parsing, so compare as text. + // 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( @@ -177,7 +177,14 @@ private static Map parseAttributes(String block) { Map attributes = new LinkedHashMap<>(); Matcher matcher = ATTRIBUTE.matcher(block); while (matcher.find()) { - attributes.put(matcher.group(1), coerce(matcher.group(2).trim())); + 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; } 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 index 7495be3c0..a67460c94 100644 --- a/runtime/java/prompty/src/test/java/com/microsoft/prompty/ParseVectorsTest.java +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/ParseVectorsTest.java @@ -1,6 +1,7 @@ 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; @@ -110,4 +111,35 @@ private static List save(List messages) { 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")); + } } From 5bffdcef60e0fc48cadbab8c0f394ecfdddbef8f Mon Sep 17 00:00:00 2001 From: Seth Juarez Date: Tue, 4 Aug 2026 04:02:54 -0700 Subject: [PATCH 07/29] Correct tool parameters in the shared agent vectors FunctionTool.parameters is a flat list of properties, or a name-keyed dict of them. The agent vectors instead wrapped every tool's parameters in a {properties: [...]} object, which the schema does not define. It reads plausibly by analogy with inputs and outputs, which genuinely are schemas with a properties field, so the mistake went unnoticed. It went unnoticed because Rust tolerates it. Its loader skips an array-valued entry when reading a dict, so the malformed shape silently became an empty parameter list and no assertion covered it. C# rejects it, and so does the Java runtime being added here, which is how it came to light. All thirty-four occurrences are unwrapped to the flat form, matching the load and wire vectors, which were already correct. Rust stays green on the corrected fixture and now actually loads the parameters rather than discarding them. The generated Java loader gains a note explaining why it rejects the shape rather than following Rust, via the normalizer that authors it. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../prompty/model/ModelCollections.java | 4 + schema/scripts/normalize-java-output.mjs | 4 + spec/vectors/agent/agent_vectors.json | 550 ++++++++---------- 3 files changed, 249 insertions(+), 309 deletions(-) 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 index dea4e24e9..46e607403 100644 --- 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 @@ -27,6 +27,10 @@ static List loadList( 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 diff --git a/schema/scripts/normalize-java-output.mjs b/schema/scripts/normalize-java-output.mjs index ca095062b..a9c88a2cc 100644 --- a/schema/scripts/normalize-java-output.mjs +++ b/schema/scripts/normalize-java-output.mjs @@ -671,6 +671,10 @@ final class ModelCollections { 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 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": { From 6088aa3e1c93087f08a0396bac8d4991f12df0c7 Mon Sep 17 00:00:00 2001 From: Seth Juarez Date: Tue, 4 Aug 2026 04:03:08 -0700 Subject: [PATCH 08/29] Add the live turn adapter and the public turn API The turn engine added previously owns the loop: iterations, tool rounds, checkpoints and resume. It reaches the outside world only through ports. This supplies those ports and exposes the result as Pipeline.turn. Following Rust, the adapter holds no loop logic of its own. It binds the model, tool, permission, policy, durability and observation ports onto the existing renderer, parser, executor and processor, so there is one implementation of the agent loop rather than two that must be kept in step. Around that sit the pieces a turn needs but the engine does not define: tool dispatch with binding injection and four argument shapes, context trimming, a steering queue, guardrails over input, output and tool calls, and the event stream a caller observes. A stream that fails with an unknown outcome now keeps that distinction. It was matching the plain error branch, because the failure type extends the error chunk, and arriving as an ordinary execute error. The engine would then retry a request the provider may already have completed, repeating the model call and any tools it had triggered. Reverting the fix makes the new test report three attempts instead of one. All twenty-eight agent vectors pass. The context and steering tests assert against the conversation the model actually received, since the mock replays by index and would otherwise pass whatever the adapter did with the messages. Steering is checked for delivery and ordering only: the vectors name an iteration to inject before, but no runtime models that, and the reference drains its queue on every iteration including the first. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../com/microsoft/prompty/AgentEvent.java | 150 +++ .../com/microsoft/prompty/Compaction.java | 47 + .../java/com/microsoft/prompty/Context.java | 210 ++++ .../microsoft/prompty/GuardrailResult.java | 37 + .../com/microsoft/prompty/Guardrails.java | 79 ++ .../java/com/microsoft/prompty/LiveTurn.java | 1104 +++++++++++++++++ .../java/com/microsoft/prompty/Pipeline.java | 45 + .../java/com/microsoft/prompty/Steering.java | 54 + .../com/microsoft/prompty/ToolDispatch.java | 286 +++++ .../com/microsoft/prompty/ToolHandler.java | 29 + .../com/microsoft/prompty/TurnOptions.java | 271 ++++ .../microsoft/prompty/AgentVectorsTest.java | 1087 ++++++++++++++++ .../prompty/LiveStreamFailureTest.java | 141 +++ 13 files changed, 3540 insertions(+) create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/AgentEvent.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/Compaction.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/Context.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/GuardrailResult.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/Guardrails.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/LiveTurn.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/Steering.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/ToolDispatch.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/ToolHandler.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/TurnOptions.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/AgentVectorsTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/LiveStreamFailureTest.java 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/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/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/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/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/Pipeline.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Pipeline.java index 11ac7b927..f1af8eb1d 100644 --- a/runtime/java/prompty/src/main/java/com/microsoft/prompty/Pipeline.java +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Pipeline.java @@ -7,6 +7,7 @@ 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; @@ -286,6 +287,50 @@ 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 /** 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/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/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/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/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()); + } +} From 48b296616b63fd0d7925c9d4e3533c999c96e6fa Mon Sep 17 00:00:00 2001 From: Seth Juarez Date: Tue, 4 Aug 2026 04:19:44 -0700 Subject: [PATCH 09/29] Add model discovery and capability enrichment Each provider answers a model-list call in its own shape, and none of them answers it completely: OpenAI reports an id and an owner and nothing else, Anthropic omits the owner entirely, and Foundry splits the same facts across a data-plane and an ARM shape depending on which endpoint replied. A caller choosing a model needs the same facts from all three, so the wire mapping is per provider and a shared dataset fills what the endpoint left out. The fill is deliberately timid. Anything the provider stated wins, including an empty list, because a provider that says a model has no output modality knows something the dataset does not. The dataset only reaches fields the payload left null, and it matches model families by longest prefix at token boundaries, so gpt-4 claims gpt-4-0613 and gpt-4.1 but never gpt-45. The dataset itself is shared across runtimes and lives in spec/data, which a published artifact cannot reach, so each runtime vendors a copy. A test fails if the vendored copy drifts from the canonical one. Foundry's mappers are the whole of its contribution for now; listing needs OAuth and ARM, which arrive with the rest of that provider. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../anthropic/AnthropicModelLister.java | 159 ++++++++++++++ .../anthropic/DiscoveryVectorsTest.java | 63 ++++++ runtime/java/prompty-foundry/build.gradle.kts | 1 + .../prompty/foundry/FoundryModels.java | 189 +++++++++++++++++ .../prompty/foundry/DiscoveryVectorsTest.java | 46 +++++ .../prompty/foundry/FoundryModelsTest.java | 138 +++++++++++++ .../prompty/openai/OpenAIModelLister.java | 113 ++++++++++ .../prompty/openai/DiscoveryVectorsTest.java | 62 ++++++ .../java/com/microsoft/prompty/Discovery.java | 195 ++++++++++++++++++ .../main/java/com/microsoft/prompty/Http.java | 31 +++ .../microsoft/prompty/model_capabilities.json | 55 +++++ .../com/microsoft/prompty/DiscoveryTest.java | 159 ++++++++++++++ .../com/microsoft/prompty/SpecVectors.java | 19 ++ 13 files changed, 1230 insertions(+) create mode 100644 runtime/java/prompty-anthropic/src/main/java/com/microsoft/prompty/anthropic/AnthropicModelLister.java create mode 100644 runtime/java/prompty-anthropic/src/test/java/com/microsoft/prompty/anthropic/DiscoveryVectorsTest.java create mode 100644 runtime/java/prompty-foundry/src/main/java/com/microsoft/prompty/foundry/FoundryModels.java create mode 100644 runtime/java/prompty-foundry/src/test/java/com/microsoft/prompty/foundry/DiscoveryVectorsTest.java create mode 100644 runtime/java/prompty-foundry/src/test/java/com/microsoft/prompty/foundry/FoundryModelsTest.java create mode 100644 runtime/java/prompty-openai/src/main/java/com/microsoft/prompty/openai/OpenAIModelLister.java create mode 100644 runtime/java/prompty-openai/src/test/java/com/microsoft/prompty/openai/DiscoveryVectorsTest.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/Discovery.java create mode 100644 runtime/java/prompty/src/main/resources/com/microsoft/prompty/model_capabilities.json create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/DiscoveryTest.java 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/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-foundry/build.gradle.kts b/runtime/java/prompty-foundry/build.gradle.kts index 090b62393..fbd3a0d9d 100644 --- a/runtime/java/prompty-foundry/build.gradle.kts +++ b/runtime/java/prompty-foundry/build.gradle.kts @@ -7,5 +7,6 @@ dependencies { 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/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/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/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-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/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/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/Http.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Http.java index fa8317f6b..a6fc20390 100644 --- a/runtime/java/prompty/src/main/java/com/microsoft/prompty/Http.java +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Http.java @@ -59,6 +59,37 @@ public static Object postJson( } } + /** + * 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) { + HttpRequest.Builder builder = HttpRequest.newBuilder(URI.create(url)).GET(); + headers.forEach(builder::header); + + 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. * 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/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/testFixtures/java/com/microsoft/prompty/SpecVectors.java b/runtime/java/prompty/src/testFixtures/java/com/microsoft/prompty/SpecVectors.java index 116fb4a0e..404040dc6 100644 --- a/runtime/java/prompty/src/testFixtures/java/com/microsoft/prompty/SpecVectors.java +++ b/runtime/java/prompty/src/testFixtures/java/com/microsoft/prompty/SpecVectors.java @@ -72,6 +72,25 @@ public static List> readArray(String relativePath) { 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) { From f78f09aa6b0f5060c71314307cce63f9449a1c21 Mon Sep 17 00:00:00 2001 From: Seth Juarez Date: Tue, 4 Aug 2026 04:44:35 -0700 Subject: [PATCH 10/29] Add memory scoring and the reference turn harness Ports two remaining pieces of the Rust runtime so the Java side can replay a turn end-to-end and be graded against the shared vectors. Memory is a static-helper class over the generated MemoryStore, since Java cannot add inherent methods to generated types. Scoring follows memory.rs exactly: +2.0 per distinct query token found in content, +3.0 per token found in a tag, and +1.0 once for a CORE entry that matched at all. Ranking is score-descending with insertion-order ties, which List.sort gives us for free because it is contractually stable. The harness package mirrors harness.rs: a collecting event sink, an NDJSON journal writer, an in-memory checkpoint store, allow/deny permission resolvers, a function-backed host tool executor, and ReferenceTurnRunner, which adapts the canonical TurnEngine onto the host-facing protocol types. ReplayVectorsTest drives all five scenarios in spec/vectors/harness/replay_vectors.json and reproduces the normalized journal exactly. Ten behavioural rules were perturbation-proven and reverted; two of those perturbations came back vacuous and were closed with real changes rather than accepted: - The tool-round arm of the messages_updated rule is structurally unreachable, because CONVERSATION_UPDATED is emitted in the same batch as TOOL_RESULT_COMMITTED in both runtimes. Rather than delete a rule the shared contract states, it is extracted into a named predicate, shouldRecordMessagesUpdated, and tested directly. - The max_iterations turn_end and error payload rewrites were unobserved, because the normalized vectors compare only :status. Added a journal payload reader and assertions on both. Journal records are written with an explicit LF rather than the platform separator, matching Rust's writeln!, and that is now asserted on the raw bytes since line-based normalization cannot see it. The engine tool-request id fallback was also corrected to skip only null, not empty, matching Option::or_else. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../java/com/microsoft/prompty/Memory.java | 289 +++++++ .../harness/AllowAllPermissionResolver.java | 20 + .../prompty/harness/CollectingEventSink.java | 46 ++ .../microsoft/prompty/harness/Decisions.java | 22 + .../harness/DenyAllPermissionResolver.java | 19 + .../harness/FunctionHostToolExecutor.java | 83 ++ .../harness/InMemoryCheckpointStore.java | 51 ++ .../harness/JsonlEventJournalWriter.java | 111 +++ .../harness/ReferenceReplayVerifier.java | 61 ++ .../prompty/harness/ReferenceTurnRunner.java | 731 ++++++++++++++++++ .../com/microsoft/prompty/MemoryTest.java | 314 ++++++++ .../prompty/harness/ReplayVectorsTest.java | 520 +++++++++++++ 12 files changed, 2267 insertions(+) create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/Memory.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/harness/AllowAllPermissionResolver.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/harness/CollectingEventSink.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/harness/Decisions.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/harness/DenyAllPermissionResolver.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/harness/FunctionHostToolExecutor.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/harness/InMemoryCheckpointStore.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/harness/JsonlEventJournalWriter.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/harness/ReferenceReplayVerifier.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/harness/ReferenceTurnRunner.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/MemoryTest.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/harness/ReplayVectorsTest.java 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/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..5b1b1ba07 --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/harness/JsonlEventJournalWriter.java @@ -0,0 +1,111 @@ +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..d4e4c96ad --- /dev/null +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/harness/ReferenceTurnRunner.java @@ -0,0 +1,731 @@ +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/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/harness/ReplayVectorsTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/harness/ReplayVectorsTest.java new file mode 100644 index 000000000..b1d93406a --- /dev/null +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/harness/ReplayVectorsTest.java @@ -0,0 +1,520 @@ +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; + } +} + + From 5a99dc93f5d07f606fcd6152b64fed31ef74641b Mon Sep 17 00:00:00 2001 From: Seth Juarez Date: Tue, 4 Aug 2026 04:57:24 -0700 Subject: [PATCH 11/29] Add the Foundry provider: Azure URL routing, auth, processing Foundry speaks the OpenAI wire format, so the executor and processor subclass the OpenAI ones rather than duplicating the wire layer, which mirrors how the Rust crate depends on prompty-openai. Routing hooks the protected buildUrl and re-maps the OpenAI path onto the Azure surface: a foundry-kind connection hits {endpoint}/{operation} with no deployment or api-version, and every other kind hits the classic {endpoint}/openai/deployments/{id}/{operation}?api-version={v}. Responses is refused because Azure has no equivalent surface, matching Rust. stripProjectPath cuts at /api/projects and rewrites *.services.ai.azure.com to *.openai.azure.com, guarding against mistaking a non-numeric colon suffix for a port. Auth sends api-key for key connections and Authorization: Bearer for foundry, falling back to AZURE_OPENAI_API_KEY and AZURE_INFERENCE_CREDENTIAL respectively. The two are deliberately not cross-used. Java has no DefaultAzureCredential equivalent, so its absence is reported rather than silently degraded. Java connections are typed, so an inline foundry credential that Rust would read from raw JSON is dropped at load; the divergence is documented and asserted. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- runtime/java/prompty-foundry/build.gradle.kts | 3 + .../prompty/foundry/FoundryAuth.java | 60 ++++ .../prompty/foundry/FoundryExecutor.java | 204 +++++++++++++ .../prompty/foundry/FoundryExtension.java | 17 ++ .../prompty/foundry/FoundryProcessor.java | 24 ++ .../com.microsoft.prompty.PromptyExtension | 1 + .../prompty/foundry/FoundryAuthTest.java | 65 +++++ .../prompty/foundry/FoundryExecutorTest.java | 273 ++++++++++++++++++ 8 files changed, 647 insertions(+) create mode 100644 runtime/java/prompty-foundry/src/main/java/com/microsoft/prompty/foundry/FoundryAuth.java create mode 100644 runtime/java/prompty-foundry/src/main/java/com/microsoft/prompty/foundry/FoundryExecutor.java create mode 100644 runtime/java/prompty-foundry/src/main/java/com/microsoft/prompty/foundry/FoundryExtension.java create mode 100644 runtime/java/prompty-foundry/src/main/java/com/microsoft/prompty/foundry/FoundryProcessor.java create mode 100644 runtime/java/prompty-foundry/src/main/resources/META-INF/services/com.microsoft.prompty.PromptyExtension create mode 100644 runtime/java/prompty-foundry/src/test/java/com/microsoft/prompty/foundry/FoundryAuthTest.java create mode 100644 runtime/java/prompty-foundry/src/test/java/com/microsoft/prompty/foundry/FoundryExecutorTest.java diff --git a/runtime/java/prompty-foundry/build.gradle.kts b/runtime/java/prompty-foundry/build.gradle.kts index fbd3a0d9d..d7d26f302 100644 --- a/runtime/java/prompty-foundry/build.gradle.kts +++ b/runtime/java/prompty-foundry/build.gradle.kts @@ -4,6 +4,9 @@ plugins { 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") 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..e2aa04d3c --- /dev/null +++ b/runtime/java/prompty-foundry/src/main/java/com/microsoft/prompty/foundry/FoundryAuth.java @@ -0,0 +1,60 @@ +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 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 firstNonBlank(connection, BEARER_TOKEN_FIELDS); + } + + private static Optional firstNonBlank(Connection connection, String[] fields) { + if (connection == null) { + return Optional.empty(); + } + Map saved = connection.save(new SaveContext()); + 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..b5a32c860 --- /dev/null +++ b/runtime/java/prompty-foundry/src/main/java/com/microsoft/prompty/foundry/FoundryExecutor.java @@ -0,0 +1,204 @@ +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/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/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..298eef15f --- /dev/null +++ b/runtime/java/prompty-foundry/src/test/java/com/microsoft/prompty/foundry/FoundryExecutorTest.java @@ -0,0 +1,273 @@ +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() { + 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() { + 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() { + 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. + 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()); + } +} From 0e298e0f24bfbee32faa1e43a5fb0073594d3578 Mon Sep 17 00:00:00 2001 From: Seth Juarez Date: Tue, 4 Aug 2026 05:15:46 -0700 Subject: [PATCH 12/29] Add live end-to-end tests against real provider APIs The offline suite proves the runtime agrees with the shared spec vectors, but vectors are frozen fixtures: they cannot catch a request shape the services stopped accepting, or a response field a provider renamed. These tests close that gap by driving the real OpenAI, Anthropic and Foundry endpoints. They are excluded from the normal build by the `live` tag and run with `-PliveTests`. Credentials come from a gitignored `.env` at the Gradle root, installed only for keys the process environment does not already supply, so CI variables win over the file. Each test declares the variables it needs and skips -- honestly, via JUnit assumptions rather than a silent pass -- when they are absent, so a machine holding only some credentials still exercises the paths it can reach. Coverage: chat, option plumbing, streaming, embeddings, image generation, model listing, structured output, tool calls and the agent loop, plus the authentication failure path. Two things this uncovered: Streaming has two distinct surfaces and they are easy to conflate. `Pipeline.invoke` deliberately collapses a stream into accumulated text, so asserting on chunks through `invoke` only ever measures the collapse. Driving the executor and processor directly is what actually exercises the SSE parser, so both are covered separately. Anthropic sends a single `content_block_delta` for a short answer. That looked like a parser defect until the raw endpoint was queried directly and confirmed to behave the same way, so the test asks for a long enough response to span deltas rather than asserting something untrue about the service. Foundry method order is pinned. One test removes a credential to prove the absence is reported clearly rather than sent as an anonymous request, and it now restores it afterwards: without that, a later test skipped silently, looking exactly like "no credential configured" when the real cause was a previous test taking it away. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../prompty/anthropic/AnthropicLiveTest.java | 200 +++++++++++ .../prompty/foundry/FoundryLiveTest.java | 158 +++++++++ .../prompty/openai/OpenAILiveTest.java | 317 ++++++++++++++++++ .../java/com/microsoft/prompty/LiveEnv.java | 207 ++++++++++++ 4 files changed, 882 insertions(+) create mode 100644 runtime/java/prompty-anthropic/src/test/java/com/microsoft/prompty/anthropic/AnthropicLiveTest.java create mode 100644 runtime/java/prompty-foundry/src/test/java/com/microsoft/prompty/foundry/FoundryLiveTest.java create mode 100644 runtime/java/prompty-openai/src/test/java/com/microsoft/prompty/openai/OpenAILiveTest.java create mode 100644 runtime/java/prompty/src/testFixtures/java/com/microsoft/prompty/LiveEnv.java 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..4bd36e1f3 --- /dev/null +++ b/runtime/java/prompty-anthropic/src/test/java/com/microsoft/prompty/anthropic/AnthropicLiveTest.java @@ -0,0 +1,200 @@ +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-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..985015cfc --- /dev/null +++ b/runtime/java/prompty-foundry/src/test/java/com/microsoft/prompty/foundry/FoundryLiveTest.java @@ -0,0 +1,158 @@ +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"); + + // The credential is restored afterwards because `.env` supplies it as an override, and clearing + // one without putting it back would make a later test in this class skip for want of a + // credential the machine actually has. + String saved = com.microsoft.prompty.Environment.lookup("AZURE_INFERENCE_CREDENTIAL").orElse(null); + RuntimeException failure = null; + String credential; + try { + com.microsoft.prompty.Environment.clear("AZURE_INFERENCE_CREDENTIAL"); + credential = LiveEnv.get("AZURE_INFERENCE_CREDENTIAL", ""); + try { + Pipeline.invoke(foundryAgent("Hello", Map.of("maxOutputTokens", 5)), Map.of()); + } catch (RuntimeException e) { + failure = e; + } + } finally { + if (saved != null) { + com.microsoft.prompty.Environment.set("AZURE_INFERENCE_CREDENTIAL", saved); + } + } + + if (!credential.isBlank()) { + // The credential lives in the real process environment, which `clear` cannot remove, so the + // request was legitimately authenticated and there is no absence to assert on. This test only + // does real work when the credential arrives as an override -- a green result here is not + // evidence that the missing-credential path behaves correctly. + return; + } + 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-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..5145961a9 --- /dev/null +++ b/runtime/java/prompty-openai/src/test/java/com/microsoft/prompty/openai/OpenAILiveTest.java @@ -0,0 +1,317 @@ +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); + } + + // ------------------------------------------------------------------ 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/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; + } + } +} From ebadd994778fb1a20403398328e4f493dadee306 Mon Sep 17 00:00:00 2001 From: Seth Juarez Date: Tue, 4 Aug 2026 05:35:42 -0700 Subject: [PATCH 13/29] Add Foundry sign-in and Azure resource discovery A Foundry connection needs an endpoint and a token, and a user who has neither has no way to get them from inside the runtime. This adds the two halves of that: the OAuth flows that produce a token, and the ARM queries that turn it into a list of endpoints to choose from. FoundryOAuth implements the device authorization grant and the authorization-code-with-PKCE flow against Entra ID, ported from the Rust runtime so both agree on scopes, error codes, and backoff. The poll loop gets an injectable token endpoint, sleeper, and clock, which is a departure from Rust: Rust hardwires its HTTP client and timer, and as a result none of its poll state machine is covered. Here the pending, slow_down, expiry, and timeout branches are all exercised directly, with no network and no elapsed wall time. FoundryArm enumerates subscriptions, AI resources, and projects. Projects have two representations depending on when they were created, so it probes both and merges. Each probe fails soft to an empty list, because a tenant that denies one resource provider is ordinary and should not take the whole listing down with it. Control-plane calls are bounded at thirty seconds, which Http.getJson now supports per call; model calls stay unbounded, since a slow answer there is still an answer. Http.postForm returns non-2xx responses to the caller instead of throwing. That looks lax until you consider that the device-code grant reports "the user has not signed in yet" as an HTTP 400 with a machine-readable body, so the ordinary path of a sign-in has to be readable, not exceptional. Every rule in both files was checked by reverting it and confirming a test fails. Two of those reverts found real gaps rather than confirming coverage: a failure path that parsed cleanly but carried no error code had no test, and would have polled until the deadline against a service that had already answered. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../microsoft/prompty/foundry/FoundryArm.java | 292 +++++++++ .../prompty/foundry/FoundryOAuth.java | 420 +++++++++++++ .../prompty/foundry/FoundryArmTest.java | 293 ++++++++++ .../prompty/foundry/FoundryOAuthTest.java | 552 ++++++++++++++++++ .../main/java/com/microsoft/prompty/Http.java | 79 +++ .../java/com/microsoft/prompty/HttpTest.java | 61 ++ 6 files changed, 1697 insertions(+) create mode 100644 runtime/java/prompty-foundry/src/main/java/com/microsoft/prompty/foundry/FoundryArm.java create mode 100644 runtime/java/prompty-foundry/src/main/java/com/microsoft/prompty/foundry/FoundryOAuth.java create mode 100644 runtime/java/prompty-foundry/src/test/java/com/microsoft/prompty/foundry/FoundryArmTest.java create mode 100644 runtime/java/prompty-foundry/src/test/java/com/microsoft/prompty/foundry/FoundryOAuthTest.java 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..97e61ae37 --- /dev/null +++ b/runtime/java/prompty-foundry/src/main/java/com/microsoft/prompty/foundry/FoundryArm.java @@ -0,0 +1,292 @@ +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 + // --------------------------------------------------------------------------------------------- + + /** + * 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. + */ + @SuppressWarnings("unchecked") + static List> fetchAll(String token, String firstUrl) { + List> items = new ArrayList<>(); + String next = firstUrl; + + while (next != null && !next.isEmpty()) { + Object body = + Http.getJson( + PROVIDER, next, Map.of("Authorization", "Bearer " + token), REQUEST_TIMEOUT); + 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) { + try { + return fetchAll(token, url); + } catch (RuntimeException e) { + return List.of(); + } + } + + // --------------------------------------------------------------------------------------------- + // 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/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/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..2cb53f945 --- /dev/null +++ b/runtime/java/prompty-foundry/src/test/java/com/microsoft/prompty/foundry/FoundryArmTest.java @@ -0,0 +1,293 @@ +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.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.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. + * + *

The paging loop needs a live control plane, so the tests here work on 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); + } + } + + @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/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/src/main/java/com/microsoft/prompty/Http.java b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Http.java index a6fc20390..b627ca58a 100644 --- a/runtime/java/prompty/src/main/java/com/microsoft/prompty/Http.java +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Http.java @@ -7,6 +7,7 @@ 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; @@ -68,8 +69,26 @@ public static Object postJson( * @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 { @@ -106,6 +125,66 @@ public static Iterator postSse( 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, 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 index 24370821f..7da620c53 100644 --- a/runtime/java/prompty/src/test/java/com/microsoft/prompty/HttpTest.java +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/HttpTest.java @@ -62,6 +62,67 @@ private static List collect(Iterator stream) { 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}"); From 35864d8a37a71befac40de097a354152618d98d9 Mon Sep 17 00:00:00 2001 From: Seth Juarez Date: Tue, 4 Aug 2026 05:35:59 -0700 Subject: [PATCH 14/29] Add Foundry model listing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The last provider still missing a ModelLister. Foundry needs two, because its two connection kinds answer different questions: a project connection lists deployments, since a deployment name is what a user actually writes in model.id, while an Azure OpenAI key connection has no deployment sub-resource and lists the lower-level model catalog instead. Routing the wrong one at either would return names that cannot be invoked. An unsupported connection kind is refused before any request goes out, so a configuration mistake reads as one rather than as a transport failure from whichever service was guessed. The wire mapping itself already existed and is covered by the shared discovery vectors; this is the transport and dispatch around it. Credential lookup gains a raw-map overload, which lets listing see the undeclared snake_case aliases a host may have written. The typed executor path still cannot, because Java drops undeclared fields at load — but listing takes its connection as raw JSON, so on this path Java and Rust now accept the same connections. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../prompty/foundry/FoundryAuth.java | 24 +- .../prompty/foundry/FoundryModelLister.java | 143 +++++++++++ .../foundry/FoundryModelListerTest.java | 240 ++++++++++++++++++ 3 files changed, 404 insertions(+), 3 deletions(-) create mode 100644 runtime/java/prompty-foundry/src/main/java/com/microsoft/prompty/foundry/FoundryModelLister.java create mode 100644 runtime/java/prompty-foundry/src/test/java/com/microsoft/prompty/foundry/FoundryModelListerTest.java 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 index e2aa04d3c..021444794 100644 --- 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 @@ -29,6 +29,11 @@ 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); } @@ -39,14 +44,27 @@ static Optional apiKey(Connection connection) { * 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 Optional firstNonBlank(Connection connection, String[] fields) { - if (connection == null) { + 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(); } - Map saved = connection.save(new SaveContext()); for (String field : fields) { if (saved.get(field) instanceof String value) { String trimmed = value.trim(); 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/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..2ff4da9d2 --- /dev/null +++ b/runtime/java/prompty-foundry/src/test/java/com/microsoft/prompty/foundry/FoundryModelListerTest.java @@ -0,0 +1,240 @@ +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() { + 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() { + 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"))); + } + } +} From c4e7ab98b5a6f6d388fb294a7027cf651b182955 Mon Sep 17 00:00:00 2001 From: Seth Juarez Date: Tue, 4 Aug 2026 05:47:44 -0700 Subject: [PATCH 15/29] Fix repo hygiene on the Java runtime files The hygiene workflow rejects blank lines at end of file, and seven Java sources had one. Strip them. While here, close two related gaps the new runtime opened. gradlew was committed non-executable, so `./gradlew` fails on Linux and macOS; mark it 100755. And .gitattributes lists an explicit LF entry for every other source language but had none for Java or Gradle, so those files were relying on the `*` default. Add them, along with `*.jar binary` to pin what was already auto-detected. These entries match what the files already contain, so nothing is renormalized. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .gitattributes | 5 +++++ runtime/java/gradlew | 0 .../com/microsoft/prompty/anthropic/AnthropicLiveTest.java | 1 - .../java/com/microsoft/prompty/foundry/FoundryExecutor.java | 1 - .../java/com/microsoft/prompty/foundry/FoundryLiveTest.java | 2 -- .../java/com/microsoft/prompty/openai/OpenAILiveTest.java | 1 - .../microsoft/prompty/harness/JsonlEventJournalWriter.java | 1 - .../com/microsoft/prompty/harness/ReferenceTurnRunner.java | 1 - .../com/microsoft/prompty/harness/ReplayVectorsTest.java | 2 -- 9 files changed, 5 insertions(+), 9 deletions(-) mode change 100644 => 100755 runtime/java/gradlew 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/runtime/java/gradlew b/runtime/java/gradlew old mode 100644 new mode 100755 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 index 4bd36e1f3..d157e2364 100644 --- 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 @@ -197,4 +197,3 @@ void agentTurnCallsAToolAndUsesTheResult() { System.out.println("[anthropic] agent -> " + text); } } - 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 index b5a32c860..e20f8b799 100644 --- 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 @@ -201,4 +201,3 @@ private static boolean isFoundry(Connection connection) { return connection != null && FOUNDRY_KIND.equals(connection.kind); } } - 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 index 985015cfc..71e3d9a7f 100644 --- 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 @@ -154,5 +154,3 @@ void aMissingFoundryCredentialFailsBeforeAnyRequestIsSent() { System.out.println("[foundry/project] missing credential -> " + message); } } - - 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 index 5145961a9..3cc0fba11 100644 --- 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 @@ -314,4 +314,3 @@ void anInvalidKeyIsReportedRatherThanSwallowed() { } } } - 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 index 5b1b1ba07..c55b4e7ee 100644 --- 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 @@ -108,4 +108,3 @@ private boolean append(Map record) { } } } - 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 index d4e4c96ad..91fc2c251 100644 --- 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 @@ -728,4 +728,3 @@ public static ReferenceTurnRunner of( sequentialIds()); } } - 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 index b1d93406a..1ef02456f 100644 --- 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 @@ -516,5 +516,3 @@ private static int intOf(Object value) { return value instanceof Number n ? n.intValue() : 0; } } - - From 4594f56922bacec6d8c3adc15eebe8f0fb3ac773 Mon Sep 17 00:00:00 2001 From: Seth Juarez Date: Tue, 4 Aug 2026 05:52:44 -0700 Subject: [PATCH 16/29] Run the Java runtime in CI Every other runtime has a build-and-test workflow, but the Java one landed without any, so its 661 tests would never run on a pull request. Add prompty-java-check.yml, modelled on the existing per-runtime checks: ubuntu and windows, JDK 21 to match the Gradle toolchain, and the offline suite only. Live provider tests carry the "live" tag and stay excluded unless -PliveTests is passed, so the job needs no secrets and is safe on pull requests from forks. Also add Java to CodeQL, which scans every other language in the repo. The matrix now states a build mode per language, replacing the conditional Autobuild step. Java and Rust use none; C# and Go keep autobuild. Rust is listed as none because CodeQL rejects autobuild for it outright, which means the old Autobuild step was never doing anything for Rust -- init had already built the database in the default buildless mode. Making the mode explicit records what actually runs. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/codeql.yml | 13 +++++-- .github/workflows/prompty-java-check.yml | 49 ++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/prompty-java-check.yml 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..c7b41eb8c --- /dev/null +++ b/.github/workflows/prompty-java-check.yml @@ -0,0 +1,49 @@ +name: prompty Java build and test +on: + pull_request: + paths: + - 'runtime/java/**' + - '.github/workflows/prompty-java-check.yml' + + workflow_call: + +jobs: + 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 From f63b98b3d0fe8381f27bff1d191f34b85a89cb65 Mon Sep 17 00:00:00 2001 From: Seth Juarez Date: Tue, 4 Aug 2026 06:24:04 -0700 Subject: [PATCH 17/29] Stop the role-marker pattern backtracking on malformed input The attribute block in the role-marker pattern was written as a repeated `\w+\s*=\s*"?[^"]*"?\s*,?\s*`. The value class there matches commas and the closing bracket as well as the value itself, so it overlaps the start of the next iteration: an unterminated block such as `user[a= a= a= ...` can be divided among the iterations in exponentially many ways, and the missing bracket forces the engine to try all of them. Measured on JDK 21, rejecting that line cost 1.6s at ten repetitions, 12s at eleven and 52s at twelve, and did not finish inside five minutes at fourteen. That line is reachable. Rendered text embeds template variable values, so a marker-shaped line is attacker-influenced -- which is the same reason strict mode exists. Rewrite the loop so a value is either a quoted run or a run containing no quote, comma or bracket. The two branches cannot straddle an iteration boundary, so the division is forced rather than searched, and possessive quantifiers stop the engine attempting one anyway. Every attribute form the sibling runtimes exercise still parses, including a quoted value containing a comma, which a narrower value class would have broken. Three malformed forms are no longer accepted: an unquoted value containing a bracket or a comma, and an unbalanced quote. Only content can result, and the sole attribute marker in the repository is unaffected, as is the `role[nonce="..."]:` form the renderer stamps. Rust carries the same formulation and is safe only because its engine is a non-backtracking automaton; Python and TypeScript carry it on backtracking engines and appear to share the exposure. C# is unaffected, matching the block as a single lazy group. The pathological input is sized so the two outcomes stay distinguishable and so a regression leaks its uninterruptible thread for seconds rather than for the life of the test JVM. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../prompty/parsers/PromptyChatParser.java | 19 +++++- .../microsoft/prompty/ParseVectorsTest.java | 58 +++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) 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 index 273c03970..f4ac0a4fc 100644 --- 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 @@ -34,9 +34,26 @@ */ 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*$", + "^\\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*\"?([^\",\\]]*)\"?"); 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 index a67460c94..614809517 100644 --- a/runtime/java/prompty/src/test/java/com/microsoft/prompty/ParseVectorsTest.java +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/ParseVectorsTest.java @@ -142,4 +142,62 @@ void mismatchedNonceIsStillRejected() { 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; + } } From bb003f4e00811c90492db3b12ee594212094e1bd Mon Sep 17 00:00:00 2001 From: Seth Juarez Date: Tue, 4 Aug 2026 07:00:53 -0700 Subject: [PATCH 18/29] Cover the ARM paging loop by injecting its transport The paging helper behind subscription, resource, and project discovery was the one part of Foundry with no coverage at all: it reached for the HTTP transport directly, so nothing could exercise it without a control plane, and the test class said as much. Take the page read as a parameter instead, the way the device-code poll loop already takes its token endpoint, and drive the loop through it. The loop body is unchanged; only where a page comes from moved. That makes the decisions in it testable, and they are worth testing: that a nextLink is followed and results accumulate in order, that an empty or non-string one terminates rather than being fetched, that an unreadable page ends the walk without discarding what came before, and that a failure propagates out of the strict path while the project probes soft-fail whole rather than returning a truncated list that would look authoritative. One behaviour is called out as a deliberate divergence from Rust rather than left to be read as incidental. Rust keeps every value entry and maps projects infallibly, so a stray non-object becomes a project with empty fields, which makes the list non-empty and suppresses the classic-hub fallback that would have found the real projects. Dropping it here keeps that fallback reachable. ARM does not emit such payloads, so this is malformed input only, and the Rust side is better fixed than copied. The cycle hazard both runtimes share, where a nextLink pointing at its own URL walks forever, is left alone deliberately: fixing it in one runtime only would trade a shared bug for a divergence. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../microsoft/prompty/foundry/FoundryArm.java | 35 ++- .../prompty/foundry/FoundryArmTest.java | 203 +++++++++++++++++- 2 files changed, 230 insertions(+), 8 deletions(-) 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 index 97e61ae37..9b9ac516e 100644 --- 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 @@ -135,21 +135,35 @@ public static List listFoundryProjects( // 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. */ - @SuppressWarnings("unchecked") 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 = - Http.getJson( - PROVIDER, next, Map.of("Authorization", "Bearer " + token), REQUEST_TIMEOUT); + Object body = endpoint.get(next); if (!(body instanceof Map page)) { break; } @@ -168,13 +182,24 @@ static List> fetchAll(String token, String firstUrl) { /** {@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(token, url); + 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 // --------------------------------------------------------------------------------------------- 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 index 2cb53f945..f89e34e83 100644 --- 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 @@ -3,12 +3,17 @@ 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; @@ -18,9 +23,10 @@ * 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. * - *

The paging loop needs a live control plane, so the tests here work on 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. + *

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 { @@ -286,6 +292,197 @@ void resultsRoundTripThroughTheGeneratedModel() { } } + @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); From cec791dd825ffb6cb304c86bd62664937dba46fb Mon Sep 17 00:00:00 2001 From: Seth Juarez Date: Tue, 4 Aug 2026 08:37:00 -0700 Subject: [PATCH 19/29] Pin @typra/emitter 0.4.3, minimize the Java shim, and fill the seams Emitter 0.4.3 lands the J1-J8/J10/J18/J19 fixes reported upstream, so the post-emit normalization no longer has to synthesize them. Pin the emitter exactly, regenerate, and delete every rewrite that 0.4.3 makes redundant, keeping only the rewrites whose defect signature is still present in raw 0.4.3 output. Retained: J9 (named collections), J11 (double-escaped test expectations), J12 (structured assertions on object-valued fields, reported as J20), J13 (unreachable integer shorthand branch), J14 (derived save re-running postSave), J15 (missing SaveContext knobs), J16 (eager optional defaults), J17 (required enums initialized to null) and J21 (enum assertions comparing against the constant name). Removed: J1, J2, J3, J4, J6, J7, J8, J10, J18, J19, and the synthesized generated-example registry - 0.4.3 emits its own TypraGeneratedTests. Each retained rule keeps a floor in EXPECTED_MINIMUMS, so if a later emitter fixes one the guard fails loudly rather than leaving a silent no-op behind. J13 and J17 produce no test failure, so those floors are the only thing standing between a silent upstream fix and an undetected divergence. Regenerating forces two companion changes into the same commit, because the tree does not compile without them. The emitter creates a ${TypeName}Methods helper for each @method declaration only when the file is missing, and never rewrites it, so the seam is the designated home for those bodies - and until now both were the emitter's throwing stubs. Implement them against the Rust reference in model_ext.rs and the contract in schema/model: text() concatenates TextPart values joined by newline, and toTextContent() returns that string when every part is text and the saved wire form of each part otherwise, so providers receive a bare string for single-modality content and the structured form once a message carries an image, audio or file part. Replace the retired registry with GeneratedExamplesTest, which discovers the emitted example classes reflectively. Without it the suite silently dropped from 692 to 545 while Gradle still reported success. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- runtime/java/README.md | 9 +- .../prompty/model/AiResourceInfo.java | 2 +- .../prompty/model/AnonymousConnection.java | 7 +- .../prompty/model/AnthropicImageBlock.java | 2 +- .../prompty/model/AnthropicImageSource.java | 2 +- .../model/AnthropicMessagesRequest.java | 2 +- .../model/AnthropicMessagesResponse.java | 2 +- .../prompty/model/AnthropicTextBlock.java | 2 +- .../model/AnthropicToolDefinition.java | 2 +- .../model/AnthropicToolResultBlock.java | 2 +- .../prompty/model/AnthropicToolUseBlock.java | 2 +- .../prompty/model/AnthropicUsage.java | 2 +- .../prompty/model/AnthropicWireMessage.java | 2 +- .../prompty/model/ApiKeyConnection.java | 7 +- .../prompty/model/ArrayProperty.java | 7 +- .../microsoft/prompty/model/AudioPart.java | 7 +- .../prompty/model/AuthorizationCodeFlow.java | 2 +- .../com/microsoft/prompty/model/Binding.java | 2 +- .../microsoft/prompty/model/Checkpoint.java | 2 +- .../model/CompactionCompletePayload.java | 2 +- .../prompty/model/CompactionConfig.java | 2 +- .../model/CompactionFailedPayload.java | 2 +- .../prompty/model/CompactionStartPayload.java | 2 +- .../microsoft/prompty/model/Connection.java | 4 +- .../microsoft/prompty/model/ContentPart.java | 4 +- .../prompty/model/ContextCandidate.java | 2 +- .../prompty/model/ContextRequest.java | 2 +- .../microsoft/prompty/model/CustomTool.java | 7 +- .../model/DelegatedStateReference.java | 2 +- .../prompty/model/DeviceAuthorization.java | 2 +- .../prompty/model/DoneEventPayload.java | 2 +- .../prompty/model/EngineCheckpoint.java | 2 +- .../microsoft/prompty/model/EngineEvent.java | 2 +- .../model/EnginePermissionDecision.java | 2 +- .../microsoft/prompty/model/ErrorChunk.java | 7 +- .../prompty/model/ErrorEventPayload.java | 2 +- .../prompty/model/FileNotFoundError.java | 2 +- .../com/microsoft/prompty/model/FilePart.java | 7 +- .../model/FinalOutputPolicyRequest.java | 2 +- .../model/FinalOutputPolicyResult.java | 2 +- .../microsoft/prompty/model/FormatConfig.java | 2 +- .../prompty/model/FoundryConnection.java | 7 +- .../microsoft/prompty/model/FunctionTool.java | 7 +- .../prompty/model/GuardrailResult.java | 2 +- .../prompty/model/HarnessContext.java | 2 +- .../prompty/model/HookEndPayload.java | 2 +- .../prompty/model/HookStartPayload.java | 2 +- .../prompty/model/HostPolicyRequest.java | 2 +- .../prompty/model/HostPolicyResult.java | 2 +- .../prompty/model/HostToolRequest.java | 2 +- .../prompty/model/HostToolResult.java | 2 +- .../microsoft/prompty/model/ImagePart.java | 7 +- .../model/InvocationContextDecision.java | 2 +- .../prompty/model/InvocationContextState.java | 4 +- .../prompty/model/InvocationUsage.java | 2 +- .../microsoft/prompty/model/InvokerError.java | 2 +- .../prompty/model/LlmCompletePayload.java | 2 +- .../prompty/model/LlmStartPayload.java | 2 +- .../prompty/model/McpApprovalMode.java | 2 +- .../com/microsoft/prompty/model/McpTool.java | 7 +- .../microsoft/prompty/model/MemoryEntry.java | 4 +- .../microsoft/prompty/model/MemoryStore.java | 2 +- .../com/microsoft/prompty/model/Message.java | 8 +- .../prompty/model/MessageMethods.java | 51 +++ .../prompty/model/MessagesUpdatedPayload.java | 2 +- .../com/microsoft/prompty/model/Model.java | 2 +- .../microsoft/prompty/model/ModelInfo.java | 2 +- .../model/ModelInvocationContextSnapshot.java | 2 +- .../prompty/model/ModelInvocationRequest.java | 2 +- .../model/ModelInvocationResponse.java | 2 +- .../microsoft/prompty/model/ModelOptions.java | 2 +- .../model/ModelReconciliationState.java | 2 +- .../prompty/model/ModelToolRequest.java | 2 +- .../prompty/model/ModelToolResult.java | 2 +- .../prompty/model/OAuthConnection.java | 7 +- .../microsoft/prompty/model/OAuthToken.java | 2 +- .../prompty/model/ObjectProperty.java | 7 +- .../microsoft/prompty/model/OpenApiTool.java | 7 +- .../microsoft/prompty/model/ParserConfig.java | 2 +- .../model/PermissionCompletedPayload.java | 2 +- .../prompty/model/PermissionDecision.java | 2 +- .../prompty/model/PermissionRequest.java | 2 +- .../model/PermissionRequestedPayload.java | 2 +- .../microsoft/prompty/model/ProjectInfo.java | 2 +- .../com/microsoft/prompty/model/Prompty.java | 2 +- .../microsoft/prompty/model/PromptyTool.java | 7 +- .../com/microsoft/prompty/model/Property.java | 2 +- .../prompty/model/RedactedField.java | 2 +- .../prompty/model/RedactionMetadata.java | 2 +- .../prompty/model/ReferenceConnection.java | 7 +- .../prompty/model/RemoteConnection.java | 7 +- .../prompty/model/ReplayJournalRecord.java | 2 +- .../prompty/model/ReplayMismatch.java | 2 +- .../model/ReplayVerificationRequest.java | 2 +- .../model/ReplayVerificationResult.java | 2 +- .../prompty/model/ResumeContext.java | 2 +- .../microsoft/prompty/model/RetryPayload.java | 2 +- .../prompty/model/RetryPolicyRequest.java | 2 +- .../prompty/model/RunTurnRequest.java | 2 +- .../prompty/model/RunTurnResult.java | 2 +- .../prompty/model/SessionEndPayload.java | 2 +- .../microsoft/prompty/model/SessionEvent.java | 2 +- .../prompty/model/SessionFileRef.java | 2 +- .../microsoft/prompty/model/SessionRef.java | 2 +- .../prompty/model/SessionStartPayload.java | 2 +- .../prompty/model/SessionSummary.java | 2 +- .../microsoft/prompty/model/SessionTrace.java | 2 +- .../prompty/model/SessionWarningPayload.java | 2 +- .../prompty/model/StatusEventPayload.java | 2 +- .../microsoft/prompty/model/StreamChunk.java | 4 +- .../prompty/model/StreamOptions.java | 2 +- .../prompty/model/SubscriptionInfo.java | 2 +- .../com/microsoft/prompty/model/Template.java | 2 +- .../microsoft/prompty/model/TextChunk.java | 7 +- .../com/microsoft/prompty/model/TextPart.java | 7 +- .../prompty/model/ThinkingChunk.java | 7 +- .../prompty/model/ThinkingEventPayload.java | 2 +- .../microsoft/prompty/model/ThreadMarker.java | 2 +- .../prompty/model/TokenEventPayload.java | 2 +- .../microsoft/prompty/model/TokenUsage.java | 2 +- .../com/microsoft/prompty/model/Tool.java | 4 +- .../com/microsoft/prompty/model/ToolCall.java | 2 +- .../model/ToolCallCompletePayload.java | 2 +- .../prompty/model/ToolCallStartPayload.java | 2 +- .../microsoft/prompty/model/ToolChunk.java | 7 +- .../microsoft/prompty/model/ToolContext.java | 2 +- .../prompty/model/ToolDispatchResult.java | 2 +- .../model/ToolExecutionCompletePayload.java | 2 +- .../model/ToolExecutionStartPayload.java | 2 +- .../microsoft/prompty/model/ToolResult.java | 4 +- .../prompty/model/ToolResultMethods.java | 26 ++ .../prompty/model/ToolResultPayload.java | 2 +- .../microsoft/prompty/model/TraceFile.java | 2 +- .../microsoft/prompty/model/TraceSpan.java | 2 +- .../microsoft/prompty/model/TraceTime.java | 4 +- .../prompty/model/TrajectoryEvent.java | 2 +- .../microsoft/prompty/model/TurnCommit.java | 2 +- .../prompty/model/TurnEndPayload.java | 2 +- .../prompty/model/TurnEngineResult.java | 2 +- .../microsoft/prompty/model/TurnEvent.java | 2 +- .../prompty/model/TurnModelRequest.java | 2 +- .../prompty/model/TurnModelResponse.java | 2 +- .../microsoft/prompty/model/TurnOptions.java | 2 +- .../prompty/model/TurnStartPayload.java | 2 +- .../microsoft/prompty/model/TurnSummary.java | 2 +- .../microsoft/prompty/model/TurnTrace.java | 2 +- .../prompty/model/UnionProperty.java | 7 +- .../microsoft/prompty/model/UsageChunk.java | 7 +- .../prompty/model/ValidationError.java | 2 +- .../prompty/model/ValidationResult.java | 2 +- .../prompty/GeneratedExamplesTest.java | 97 +++++ .../prompty/GeneratedModelExamplesTest.java | 33 -- .../prompty/model/GeneratedModelExamples.java | 162 ------- schema/package-lock.json | 8 +- schema/package.json | 3 +- schema/scripts/normalize-java-output.mjs | 403 +++--------------- .../.typra-generated/export-surfaces.json | 4 +- .../tsp-output/.typra-generated/manifest.json | 110 +++++ 158 files changed, 545 insertions(+), 792 deletions(-) create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/MessageMethods.java create mode 100644 runtime/java/prompty/src/main/java/com/microsoft/prompty/model/ToolResultMethods.java create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/GeneratedExamplesTest.java delete mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/GeneratedModelExamplesTest.java delete mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/model/GeneratedModelExamples.java diff --git a/runtime/java/README.md b/runtime/java/README.md index c9cd19cf9..2d691db36 100644 --- a/runtime/java/README.md +++ b/runtime/java/README.md @@ -45,10 +45,11 @@ part of the generation pipeline, not a manual edit — running the pipeline twic from a clean tree produces byte-identical output. The emitter also produces example-driven suites next to the model. They are -surfaced to JUnit through the generated `GeneratedModelExamples` registry and -executed by `GeneratedModelExamplesTest`. `ModelNormalizationTest` covers the -behaviour the shim adds on top of the emitter output, which the generated -examples do not exercise. +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 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 index 9423e0815..260768528 100644 --- 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 @@ -30,6 +30,7 @@ public static AiResourceInfo load(Object input, LoadContext context) { 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")); @@ -51,7 +52,6 @@ static void loadBaseInto(AiResourceInfo result, Map map, LoadContext ctx) } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; AiResourceInfo obj = ctx.processObject(this); 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 index 45d81c2b0..86f156e67 100644 --- 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 @@ -8,6 +8,7 @@ import java.util.Map; public class AnonymousConnection extends Connection { + public static final String SHORTHAND_PROPERTY = null; public String endpoint = ""; @@ -26,22 +27,18 @@ public static AnonymousConnection load(Object input, LoadContext context) { 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("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")); } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; AnonymousConnection obj = ctx.processObject(this); Map result = super.save(ctx); - if (obj.kind != null) result.put("kind", serializeScalar(obj.kind)); if (obj.endpoint != null) result.put("endpoint", serializeScalar(obj.endpoint)); return result; } 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 index fec889f03..8ebcd0aa8 100644 --- 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 @@ -26,6 +26,7 @@ public static AnthropicImageBlock load(Object input, LoadContext context) { 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")); @@ -35,7 +36,6 @@ static void loadBaseInto(AnthropicImageBlock result, Map map, LoadContext } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; AnthropicImageBlock obj = ctx.processObject(this); 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 index 14dcea5bd..332be2f74 100644 --- 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 @@ -27,6 +27,7 @@ public static AnthropicImageSource load(Object input, LoadContext context) { 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")); @@ -39,7 +40,6 @@ static void loadBaseInto(AnthropicImageSource result, Map map, LoadContext } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; AnthropicImageSource obj = ctx.processObject(this); 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 index a6af62777..9c2243587 100644 --- 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 @@ -33,6 +33,7 @@ public static AnthropicMessagesRequest load(Object input, LoadContext context) { 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")); @@ -70,7 +71,6 @@ static void loadBaseInto(AnthropicMessagesRequest result, Map map, LoadCon } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; AnthropicMessagesRequest obj = ctx.processObject(this); 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 index 5594c00c1..89a3301fb 100644 --- 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 @@ -31,6 +31,7 @@ public static AnthropicMessagesResponse load(Object input, LoadContext context) 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")); @@ -60,7 +61,6 @@ static void loadBaseInto(AnthropicMessagesResponse result, Map map, LoadCo } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; AnthropicMessagesResponse obj = ctx.processObject(this); 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 index 865ec04ab..f1b876c13 100644 --- 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 @@ -26,6 +26,7 @@ public static AnthropicTextBlock load(Object input, LoadContext context) { 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")); @@ -35,7 +36,6 @@ static void loadBaseInto(AnthropicTextBlock result, Map map, LoadContext c } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; AnthropicTextBlock obj = ctx.processObject(this); 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 index d6095fa0a..ac893ca15 100644 --- 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 @@ -27,6 +27,7 @@ public static AnthropicToolDefinition load(Object input, LoadContext context) { 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")); @@ -41,7 +42,6 @@ static void loadBaseInto(AnthropicToolDefinition result, Map map, LoadCont } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; AnthropicToolDefinition obj = ctx.processObject(this); 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 index 4f7407e86..44da4c94b 100644 --- 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 @@ -27,6 +27,7 @@ public static AnthropicToolResultBlock load(Object input, LoadContext context) { 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")); @@ -39,7 +40,6 @@ static void loadBaseInto(AnthropicToolResultBlock result, Map map, LoadCon } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; AnthropicToolResultBlock obj = ctx.processObject(this); 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 index dc0585f82..772996529 100644 --- 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 @@ -28,6 +28,7 @@ public static AnthropicToolUseBlock load(Object input, LoadContext context) { 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")); @@ -45,7 +46,6 @@ static void loadBaseInto(AnthropicToolUseBlock result, Map map, LoadContex } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; AnthropicToolUseBlock obj = ctx.processObject(this); 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 index ffcf475e8..57af7e8c6 100644 --- 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 @@ -26,6 +26,7 @@ public static AnthropicUsage load(Object input, LoadContext context) { 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")))); @@ -35,7 +36,6 @@ static void loadBaseInto(AnthropicUsage result, Map map, LoadContext ctx) } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; AnthropicUsage obj = ctx.processObject(this); 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 index 4577cf496..544c0aab6 100644 --- 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 @@ -26,6 +26,7 @@ public static AnthropicWireMessage load(Object input, LoadContext context) { 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")); @@ -40,7 +41,6 @@ static void loadBaseInto(AnthropicWireMessage result, Map map, LoadContext } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; AnthropicWireMessage obj = ctx.processObject(this); 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 index 624924bfb..1a16fd19b 100644 --- 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 @@ -8,6 +8,7 @@ import java.util.Map; public class ApiKeyConnection extends Connection { + public static final String SHORTHAND_PROPERTY = null; public String endpoint = ""; public String apiKey = ""; @@ -27,11 +28,9 @@ public static ApiKeyConnection load(Object input, LoadContext context) { 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("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")); } @@ -40,12 +39,10 @@ static void loadBaseInto(ApiKeyConnection result, Map map, LoadContext ctx } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; ApiKeyConnection obj = ctx.processObject(this); Map result = super.save(ctx); - if (obj.kind != null) result.put("kind", serializeScalar(obj.kind)); if (obj.endpoint != null) result.put("endpoint", serializeScalar(obj.endpoint)); if (obj.apiKey != null) result.put("apiKey", serializeScalar(obj.apiKey)); return result; 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 index f0cbd2987..3f2d13d2d 100644 --- 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 @@ -8,6 +8,7 @@ import java.util.Map; public class ArrayProperty extends Property { + public static final String SHORTHAND_PROPERTY = null; public Property items = null; @@ -26,22 +27,18 @@ public static ArrayProperty load(Object input, LoadContext context) { 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("kind") && map.get("kind") != null) { - result.kind = String.valueOf(map.get("kind")); - } 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.kind != null) result.put("kind", serializeScalar(obj.kind)); if (obj.items != null) result.put("items", obj.items.save(ctx)); return result; } 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 index 7e8f4bffa..75a2e0f49 100644 --- 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 @@ -8,6 +8,7 @@ import java.util.Map; public class AudioPart extends ContentPart { + public static final String SHORTHAND_PROPERTY = null; public String source = ""; public String mediaType = null; @@ -27,11 +28,9 @@ public static AudioPart load(Object input, LoadContext context) { 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("kind") && map.get("kind") != null) { - result.kind = String.valueOf(map.get("kind")); - } if (map.containsKey("source") && map.get("source") != null) { result.source = String.valueOf(map.get("source")); } @@ -40,12 +39,10 @@ static void loadBaseInto(AudioPart result, Map map, LoadContext ctx) { } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; AudioPart obj = ctx.processObject(this); Map result = super.save(ctx); - if (obj.kind != null) result.put("kind", serializeScalar(obj.kind)); if (obj.source != null) result.put("source", serializeScalar(obj.source)); if (obj.mediaType != null) result.put("mediaType", serializeScalar(obj.mediaType)); return result; 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 index d3ca51e6f..336f95713 100644 --- 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 @@ -26,6 +26,7 @@ public static AuthorizationCodeFlow load(Object input, LoadContext context) { 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")); @@ -35,7 +36,6 @@ static void loadBaseInto(AuthorizationCodeFlow result, Map map, LoadContex } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; AuthorizationCodeFlow obj = ctx.processObject(this); 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 index 9c54fc03e..690930c87 100644 --- 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 @@ -31,6 +31,7 @@ public static Binding load(Object input, LoadContext context) { 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")); @@ -40,7 +41,6 @@ static void loadBaseInto(Binding result, Map map, LoadContext ctx) { } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; Binding obj = ctx.processObject(this); 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 index 59a83f7e7..8137866fd 100644 --- 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 @@ -35,6 +35,7 @@ public static Checkpoint load(Object input, LoadContext context) { 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")); @@ -75,7 +76,6 @@ static void loadBaseInto(Checkpoint result, Map map, LoadContext ctx) { } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; Checkpoint obj = ctx.processObject(this); 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 index 8df766ba8..c3cfbdd97 100644 --- 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 @@ -27,6 +27,7 @@ public static CompactionCompletePayload load(Object input, LoadContext context) 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")))); @@ -39,7 +40,6 @@ static void loadBaseInto(CompactionCompletePayload result, Map map, LoadCo } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; CompactionCompletePayload obj = ctx.processObject(this); 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 index 9cc7347ba..71866205c 100644 --- 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 @@ -27,6 +27,7 @@ public static CompactionConfig load(Object input, LoadContext context) { 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")); @@ -41,7 +42,6 @@ static void loadBaseInto(CompactionConfig result, Map map, LoadContext ctx } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; CompactionConfig obj = ctx.processObject(this); 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 index df96ea40b..0c8032657 100644 --- 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 @@ -25,13 +25,13 @@ public static CompactionFailedPayload load(Object input, LoadContext context) { 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); 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 index 4f599c43c..15c989c56 100644 --- 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 @@ -25,13 +25,13 @@ public static CompactionStartPayload load(Object input, LoadContext context) { 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); 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 index b235669c8..b9d3648de 100644 --- 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 @@ -41,8 +41,9 @@ public static Connection load(Object input, LoadContext context) { } } } - throw new IllegalArgumentException("Missing Connection discriminator property: 'kind'"); + 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")); @@ -55,7 +56,6 @@ static void loadBaseInto(Connection result, Map map, LoadContext ctx) { } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; Connection obj = ctx.processObject(this); 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 index 9ad156485..bb3aac2c6 100644 --- 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 @@ -35,15 +35,15 @@ public static ContentPart load(Object input, LoadContext context) { } } } - throw new IllegalArgumentException("Missing ContentPart discriminator property: 'kind'"); + 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); 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 index 9da483851..49e77a969 100644 --- 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 @@ -28,6 +28,7 @@ public static ContextCandidate load(Object input, LoadContext context) { 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")); @@ -46,7 +47,6 @@ static void loadBaseInto(ContextCandidate result, Map map, LoadContext ctx } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; ContextCandidate obj = ctx.processObject(this); 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 index 4930974c1..7ccb8738d 100644 --- 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 @@ -32,6 +32,7 @@ public static ContextRequest load(Object input, LoadContext context) { 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")); @@ -60,7 +61,6 @@ static void loadBaseInto(ContextRequest result, Map map, LoadContext ctx) } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; ContextRequest obj = ctx.processObject(this); 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 index 1b86ff69f..18dbabb21 100644 --- 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 @@ -8,6 +8,7 @@ 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<>(); @@ -27,11 +28,9 @@ public static CustomTool load(Object input, LoadContext context) { 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("kind") && map.get("kind") != null) { - result.kind = String.valueOf(map.get("kind")); - } if (map.containsKey("connection") && map.get("connection") != null) { result.connection = Connection.load(map.get("connection"), ctx); } @@ -42,12 +41,10 @@ static void loadBaseInto(CustomTool result, Map map, LoadContext ctx) { } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; CustomTool obj = ctx.processObject(this); Map result = super.save(ctx); - if (obj.kind != null) result.put("kind", serializeScalar(obj.kind)); if (obj.connection != null) result.put("connection", obj.connection.save(ctx)); if (obj.options != null) result.put("options", serializeScalar(obj.options)); return result; 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 index 9d3a0f57b..009e6d25b 100644 --- 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 @@ -28,6 +28,7 @@ public static DelegatedStateReference load(Object input, LoadContext context) { 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")); @@ -45,7 +46,6 @@ static void loadBaseInto(DelegatedStateReference result, Map map, LoadCont } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; DelegatedStateReference obj = ctx.processObject(this); 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 index cd050ba04..16b3b3558 100644 --- 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 @@ -30,6 +30,7 @@ public static DeviceAuthorization load(Object input, LoadContext context) { 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")); @@ -51,7 +52,6 @@ static void loadBaseInto(DeviceAuthorization result, Map map, LoadContext } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; DeviceAuthorization obj = ctx.processObject(this); 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 index 3705cb3da..0bbdb8bd7 100644 --- 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 @@ -26,6 +26,7 @@ public static DoneEventPayload load(Object input, LoadContext context) { 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"); @@ -36,7 +37,6 @@ static void loadBaseInto(DoneEventPayload result, Map map, LoadContext ctx } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; DoneEventPayload obj = ctx.processObject(this); 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 index 03bf54f83..e4857ae3e 100644 --- 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 @@ -48,6 +48,7 @@ public static EngineCheckpoint load(Object input, LoadContext context) { 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")); @@ -128,7 +129,6 @@ static void loadBaseInto(EngineCheckpoint result, Map map, LoadContext ctx } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; EngineCheckpoint obj = ctx.processObject(this); 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 index 1e45e0dc9..19be4eb17 100644 --- 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 @@ -36,6 +36,7 @@ public static EngineEvent load(Object input, LoadContext context) { 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")))); @@ -75,7 +76,6 @@ static void loadBaseInto(EngineEvent result, Map map, LoadContext ctx) { } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; EngineEvent obj = ctx.processObject(this); 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 index b93a66186..55e7b447e 100644 --- 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 @@ -27,6 +27,7 @@ public static EnginePermissionDecision load(Object input, LoadContext context) { 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")))); @@ -41,7 +42,6 @@ static void loadBaseInto(EnginePermissionDecision result, Map map, LoadCon } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; EnginePermissionDecision obj = ctx.processObject(this); 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 index 808c49e12..66e86e50d 100644 --- 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 @@ -8,6 +8,7 @@ import java.util.Map; public class ErrorChunk extends StreamChunk { + public static final String SHORTHAND_PROPERTY = null; public String message = ""; @@ -26,22 +27,18 @@ public static ErrorChunk load(Object input, LoadContext context) { 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("kind") && map.get("kind") != null) { - result.kind = String.valueOf(map.get("kind")); - } 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.kind != null) result.put("kind", serializeScalar(obj.kind)); if (obj.message != null) result.put("message", serializeScalar(obj.message)); return result; } 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 index 2d3216e29..c4a3902f6 100644 --- 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 @@ -27,6 +27,7 @@ public static ErrorEventPayload load(Object input, LoadContext context) { 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")); @@ -39,7 +40,6 @@ static void loadBaseInto(ErrorEventPayload result, Map map, LoadContext ct } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; ErrorEventPayload obj = ctx.processObject(this); 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 index 2a3b66687..515b8039b 100644 --- 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 @@ -26,6 +26,7 @@ public static FileNotFoundError load(Object input, LoadContext context) { 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")); @@ -35,7 +36,6 @@ static void loadBaseInto(FileNotFoundError result, Map map, LoadContext ct } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; FileNotFoundError obj = ctx.processObject(this); 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 index 572358d4b..2263a6809 100644 --- 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 @@ -8,6 +8,7 @@ import java.util.Map; public class FilePart extends ContentPart { + public static final String SHORTHAND_PROPERTY = null; public String source = ""; public String mediaType = null; @@ -27,11 +28,9 @@ public static FilePart load(Object input, LoadContext context) { 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("kind") && map.get("kind") != null) { - result.kind = String.valueOf(map.get("kind")); - } if (map.containsKey("source") && map.get("source") != null) { result.source = String.valueOf(map.get("source")); } @@ -40,12 +39,10 @@ static void loadBaseInto(FilePart result, Map map, LoadContext ctx) { } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; FilePart obj = ctx.processObject(this); Map result = super.save(ctx); - if (obj.kind != null) result.put("kind", serializeScalar(obj.kind)); if (obj.source != null) result.put("source", serializeScalar(obj.source)); if (obj.mediaType != null) result.put("mediaType", serializeScalar(obj.mediaType)); return result; 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 index 71b83387f..9a89ecd6b 100644 --- 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 @@ -30,6 +30,7 @@ public static FinalOutputPolicyRequest load(Object input, LoadContext context) { 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")); @@ -52,7 +53,6 @@ static void loadBaseInto(FinalOutputPolicyRequest result, Map map, LoadCon } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; FinalOutputPolicyRequest obj = ctx.processObject(this); 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 index 3580bb7e3..5d978d922 100644 --- 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 @@ -26,6 +26,7 @@ public static FinalOutputPolicyResult load(Object input, LoadContext context) { 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"); @@ -37,7 +38,6 @@ static void loadBaseInto(FinalOutputPolicyResult result, Map map, LoadCont } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; FinalOutputPolicyResult obj = ctx.processObject(this); 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 index 2693dce38..04a6cc235 100644 --- 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 @@ -32,6 +32,7 @@ public static FormatConfig load(Object input, LoadContext context) { 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")); @@ -46,7 +47,6 @@ static void loadBaseInto(FormatConfig result, Map map, LoadContext ctx) { } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; FormatConfig obj = ctx.processObject(this); 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 index 97dfed478..c110a134e 100644 --- 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 @@ -8,6 +8,7 @@ import java.util.Map; public class FoundryConnection extends Connection { + public static final String SHORTHAND_PROPERTY = null; public String endpoint = ""; public String name = null; @@ -28,11 +29,9 @@ public static FoundryConnection load(Object input, LoadContext context) { 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("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")); } @@ -44,12 +43,10 @@ static void loadBaseInto(FoundryConnection result, Map map, LoadContext ct } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; FoundryConnection obj = ctx.processObject(this); Map result = super.save(ctx); - if (obj.kind != null) result.put("kind", serializeScalar(obj.kind)); 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)); 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 index e99d38291..8f5a89e79 100644 --- 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 @@ -8,6 +8,7 @@ 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; @@ -27,11 +28,9 @@ public static FunctionTool load(Object input, LoadContext context) { 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("kind") && map.get("kind") != null) { - result.kind = String.valueOf(map.get("kind")); - } if (map.containsKey("parameters") && map.get("parameters") != null) { result.parameters = ModelCollections.loadList( map.get("parameters"), "parameters", Property.SHORTHAND_PROPERTY, Property::load, ctx); @@ -41,12 +40,10 @@ static void loadBaseInto(FunctionTool result, Map map, LoadContext ctx) { } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; FunctionTool obj = ctx.processObject(this); Map result = super.save(ctx); - if (obj.kind != null) result.put("kind", serializeScalar(obj.kind)); if (obj.parameters != null) { result.put("parameters", ModelCollections.saveList( obj.parameters, Property.SHORTHAND_PROPERTY, item -> item.save(ctx), ctx)); 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 index 2e868d015..0f6f62262 100644 --- 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 @@ -27,6 +27,7 @@ public static GuardrailResult load(Object input, LoadContext context) { 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")))); @@ -39,7 +40,6 @@ static void loadBaseInto(GuardrailResult result, Map map, LoadContext ctx) } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; GuardrailResult obj = ctx.processObject(this); 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 index 90b1bc466..b85afb16e 100644 --- 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 @@ -27,6 +27,7 @@ public static HarnessContext load(Object input, LoadContext context) { 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")); @@ -41,7 +42,6 @@ static void loadBaseInto(HarnessContext result, Map map, LoadContext ctx) } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; HarnessContext obj = ctx.processObject(this); 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 index 40018d4cc..bc5e21d7f 100644 --- 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 @@ -32,6 +32,7 @@ public static HookEndPayload load(Object input, LoadContext context) { 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")); @@ -61,7 +62,6 @@ static void loadBaseInto(HookEndPayload result, Map map, LoadContext ctx) } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; HookEndPayload obj = ctx.processObject(this); 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 index b52a08317..bbafcf36d 100644 --- 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 @@ -29,6 +29,7 @@ public static HookStartPayload load(Object input, LoadContext context) { 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")); @@ -49,7 +50,6 @@ static void loadBaseInto(HookStartPayload result, Map map, LoadContext ctx } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; HookStartPayload obj = ctx.processObject(this); 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 index dcbd3ab28..1e929cffe 100644 --- 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 @@ -30,6 +30,7 @@ public static HostPolicyRequest load(Object input, LoadContext context) { 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")); @@ -52,7 +53,6 @@ static void loadBaseInto(HostPolicyRequest result, Map map, LoadContext ct } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; HostPolicyRequest obj = ctx.processObject(this); 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 index 1fca8bc0b..8f534595d 100644 --- 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 @@ -27,6 +27,7 @@ public static HostPolicyResult load(Object input, LoadContext context) { 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( @@ -42,7 +43,6 @@ static void loadBaseInto(HostPolicyResult result, Map map, LoadContext ctx } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; HostPolicyResult obj = ctx.processObject(this); 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 index 54abb310d..8664fd58c 100644 --- 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 @@ -29,6 +29,7 @@ public static HostToolRequest load(Object input, LoadContext context) { 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")); @@ -49,7 +50,6 @@ static void loadBaseInto(HostToolRequest result, Map map, LoadContext ctx) } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; HostToolRequest obj = ctx.processObject(this); 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 index 56bcf019c..ea51e20ac 100644 --- 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 @@ -33,6 +33,7 @@ public static HostToolResult load(Object input, LoadContext context) { 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")); @@ -65,7 +66,6 @@ static void loadBaseInto(HostToolResult result, Map map, LoadContext ctx) } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; HostToolResult obj = ctx.processObject(this); 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 index 8e23528de..03d6fe928 100644 --- 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 @@ -8,6 +8,7 @@ import java.util.Map; public class ImagePart extends ContentPart { + public static final String SHORTHAND_PROPERTY = null; public String source = ""; public String detail = null; @@ -28,11 +29,9 @@ public static ImagePart load(Object input, LoadContext context) { 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("kind") && map.get("kind") != null) { - result.kind = String.valueOf(map.get("kind")); - } if (map.containsKey("source") && map.get("source") != null) { result.source = String.valueOf(map.get("source")); } @@ -44,12 +43,10 @@ static void loadBaseInto(ImagePart result, Map map, LoadContext ctx) { } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; ImagePart obj = ctx.processObject(this); Map result = super.save(ctx); - if (obj.kind != null) result.put("kind", serializeScalar(obj.kind)); 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)); 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 index 7551a5b38..36a64dac7 100644 --- 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 @@ -30,6 +30,7 @@ public static InvocationContextDecision load(Object input, LoadContext context) 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")); @@ -53,7 +54,6 @@ static void loadBaseInto(InvocationContextDecision result, Map map, LoadCo } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; InvocationContextDecision obj = ctx.processObject(this); 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 index 2abecd943..372486e1a 100644 --- 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 @@ -10,7 +10,7 @@ public class InvocationContextState { public static final String SHORTHAND_PROPERTY = null; - public InvocationContextPortability portability = InvocationContextPortability.PORTABLE; + public InvocationContextPortability portability = InvocationContextPortability.fromValue("portable"); public List delegatedState = null; public InvocationContextState() { } @@ -26,6 +26,7 @@ public static InvocationContextState load(Object input, LoadContext context) { 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"))); @@ -36,7 +37,6 @@ static void loadBaseInto(InvocationContextState result, Map map, LoadConte } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; InvocationContextState obj = ctx.processObject(this); 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 index 878ec411f..84ad73214 100644 --- 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 @@ -27,6 +27,7 @@ public static InvocationUsage load(Object input, LoadContext context) { 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")))); @@ -39,7 +40,6 @@ static void loadBaseInto(InvocationUsage result, Map map, LoadContext ctx) } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; InvocationUsage obj = ctx.processObject(this); 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 index b2751d407..7652fb5b4 100644 --- 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 @@ -27,6 +27,7 @@ public static InvokerError load(Object input, LoadContext context) { 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")); @@ -39,7 +40,6 @@ static void loadBaseInto(InvokerError result, Map map, LoadContext ctx) { } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; InvokerError obj = ctx.processObject(this); 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 index dfe817f23..07235a3a7 100644 --- 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 @@ -28,6 +28,7 @@ public static LlmCompletePayload load(Object input, LoadContext context) { 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")); @@ -43,7 +44,6 @@ static void loadBaseInto(LlmCompletePayload result, Map map, LoadContext c } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; LlmCompletePayload obj = ctx.processObject(this); 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 index 4b25e0804..a0cc9b910 100644 --- 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 @@ -28,6 +28,7 @@ public static LlmStartPayload load(Object input, LoadContext context) { 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")); @@ -43,7 +44,6 @@ static void loadBaseInto(LlmStartPayload result, Map map, LoadContext ctx) } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; LlmStartPayload obj = ctx.processObject(this); 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 index fc63ec5e7..3670cb2b5 100644 --- 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 @@ -32,6 +32,7 @@ public static McpApprovalMode load(Object input, LoadContext context) { 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"))); @@ -54,7 +55,6 @@ static void loadBaseInto(McpApprovalMode result, Map map, LoadContext ctx) } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; McpApprovalMode obj = ctx.processObject(this); 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 index 3a4dd8978..ac790f8a2 100644 --- 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 @@ -8,6 +8,7 @@ import java.util.Map; public class McpTool extends Tool { + public static final String SHORTHAND_PROPERTY = null; public Connection connection = null; public String serverName = ""; @@ -30,11 +31,9 @@ public static McpTool load(Object input, LoadContext context) { 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("kind") && map.get("kind") != null) { - result.kind = String.valueOf(map.get("kind")); - } if (map.containsKey("connection") && map.get("connection") != null) { result.connection = Connection.load(map.get("connection"), ctx); } @@ -57,12 +56,10 @@ static void loadBaseInto(McpTool result, Map map, LoadContext ctx) { } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; McpTool obj = ctx.processObject(this); Map result = super.save(ctx); - if (obj.kind != null) result.put("kind", serializeScalar(obj.kind)); 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)); 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 index 1cdf66c00..43f4ce7c8 100644 --- 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 @@ -11,7 +11,7 @@ public class MemoryEntry { public static final String SHORTHAND_PROPERTY = null; public String content = ""; - public MemoryCategory category = MemoryCategory.CORE; + public MemoryCategory category = MemoryCategory.fromValue("core"); public String createdAt = null; public List tags = null; @@ -28,6 +28,7 @@ public static MemoryEntry load(Object input, LoadContext context) { 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")); @@ -48,7 +49,6 @@ static void loadBaseInto(MemoryEntry result, Map map, LoadContext ctx) { } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; MemoryEntry obj = ctx.processObject(this); 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 index 1a20f39af..6e057db9a 100644 --- 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 @@ -25,6 +25,7 @@ public static MemoryStore load(Object input, LoadContext context) { 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( @@ -32,7 +33,6 @@ static void loadBaseInto(MemoryStore result, Map map, LoadContext ctx) { } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; MemoryStore obj = ctx.processObject(this); 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 index 5fd94443d..5d3eabed4 100644 --- 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 @@ -10,7 +10,7 @@ public class Message { public static final String SHORTHAND_PROPERTY = null; - public Role role = Role.USER; + public Role role = Role.fromValue("user"); public List parts = new ArrayList<>(); public Map metadata = new LinkedHashMap<>(); @@ -27,6 +27,7 @@ public static Message load(Object input, LoadContext context) { 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"))); @@ -42,7 +43,6 @@ static void loadBaseInto(Message result, Map map, LoadContext ctx) { } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; Message obj = ctx.processObject(this); @@ -94,11 +94,11 @@ public static Message user(String text) { } public Object toTextContent() { - throw new UnsupportedOperationException("Implement this helper outside generated code."); + return MessageMethods.toTextContent(this); } public String text() { - throw new UnsupportedOperationException("Implement this helper outside generated code."); + return MessageMethods.text(this); } private static Map copyMap(Map source) { 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 index 3898a7a43..525e4b844 100644 --- 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 @@ -28,6 +28,7 @@ public static MessagesUpdatedPayload load(Object input, LoadContext context) { 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( @@ -45,7 +46,6 @@ static void loadBaseInto(MessagesUpdatedPayload result, Map map, LoadConte } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; MessagesUpdatedPayload obj = ctx.processObject(this); 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 index 93725f3d2..4b539cd28 100644 --- 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 @@ -34,6 +34,7 @@ public static Model load(Object input, LoadContext context) { 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")); @@ -52,7 +53,6 @@ static void loadBaseInto(Model result, Map map, LoadContext ctx) { } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; Model obj = ctx.processObject(this); 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 index b704c742a..d29ff6f2b 100644 --- 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 @@ -31,6 +31,7 @@ public static ModelInfo load(Object input, LoadContext context) { 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")); @@ -67,7 +68,6 @@ static void loadBaseInto(ModelInfo result, Map map, LoadContext ctx) { } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; ModelInfo obj = ctx.processObject(this); 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 index 2782f1212..dd935cab8 100644 --- 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 @@ -34,6 +34,7 @@ public static ModelInvocationContextSnapshot load(Object input, LoadContext cont 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")); @@ -71,7 +72,6 @@ static void loadBaseInto(ModelInvocationContextSnapshot result, Map map, L } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; ModelInvocationContextSnapshot obj = ctx.processObject(this); 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 index e81e6688c..6456c9c1c 100644 --- 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 @@ -25,13 +25,13 @@ public static ModelInvocationRequest load(Object input, LoadContext context) { 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); 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 index 323596d7e..176dec9ff 100644 --- 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 @@ -30,6 +30,7 @@ public static ModelInvocationResponse load(Object input, LoadContext context) { 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"); @@ -55,7 +56,6 @@ static void loadBaseInto(ModelInvocationResponse result, Map map, LoadCont } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; ModelInvocationResponse obj = ctx.processObject(this); 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 index 95cdf2ae5..26db1ac96 100644 --- 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 @@ -34,6 +34,7 @@ public static ModelOptions load(Object input, LoadContext context) { 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")))); @@ -74,7 +75,6 @@ static void loadBaseInto(ModelOptions result, Map map, LoadContext ctx) { } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; ModelOptions obj = ctx.processObject(this); 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 index cffbee242..aab9ed17d 100644 --- 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 @@ -29,6 +29,7 @@ public static ModelReconciliationState load(Object input, LoadContext context) { 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")); @@ -49,7 +50,6 @@ static void loadBaseInto(ModelReconciliationState result, Map map, LoadCon } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; ModelReconciliationState obj = ctx.processObject(this); 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 index 52ddb9e73..6045d749d 100644 --- 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 @@ -28,6 +28,7 @@ public static ModelToolRequest load(Object input, LoadContext context) { 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")); @@ -45,7 +46,6 @@ static void loadBaseInto(ModelToolRequest result, Map map, LoadContext ctx } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; ModelToolRequest obj = ctx.processObject(this); 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 index f003357e8..fc2b1b848 100644 --- 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 @@ -30,6 +30,7 @@ public static ModelToolResult load(Object input, LoadContext context) { 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")); @@ -53,7 +54,6 @@ static void loadBaseInto(ModelToolResult result, Map map, LoadContext ctx) } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; ModelToolResult obj = ctx.processObject(this); 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 index 053450fca..53d2e1706 100644 --- 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 @@ -8,6 +8,7 @@ import java.util.Map; public class OAuthConnection extends Connection { + public static final String SHORTHAND_PROPERTY = null; public String endpoint = ""; public String clientId = ""; @@ -30,11 +31,9 @@ public static OAuthConnection load(Object input, LoadContext context) { 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("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")); } @@ -57,12 +56,10 @@ static void loadBaseInto(OAuthConnection result, Map map, LoadContext ctx) } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; OAuthConnection obj = ctx.processObject(this); Map result = super.save(ctx); - if (obj.kind != null) result.put("kind", serializeScalar(obj.kind)); 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)); 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 index c97afc8b9..fe60b4210 100644 --- 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 @@ -29,6 +29,7 @@ public static OAuthToken load(Object input, LoadContext context) { 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")); @@ -47,7 +48,6 @@ static void loadBaseInto(OAuthToken result, Map map, LoadContext ctx) { } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; OAuthToken obj = ctx.processObject(this); 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 index f81dd3148..14c77c43a 100644 --- 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 @@ -8,6 +8,7 @@ import java.util.Map; public class ObjectProperty extends Property { + public static final String SHORTHAND_PROPERTY = null; public List properties = new ArrayList<>(); @@ -26,23 +27,19 @@ public static ObjectProperty load(Object input, LoadContext context) { 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("kind") && map.get("kind") != null) { - result.kind = String.valueOf(map.get("kind")); - } 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.kind != null) result.put("kind", serializeScalar(obj.kind)); if (obj.properties != null) { result.put("properties", ModelCollections.saveList( obj.properties, Property.SHORTHAND_PROPERTY, item -> item.save(ctx), ctx)); 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 index 697d43862..b7e81907e 100644 --- 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 @@ -8,6 +8,7 @@ import java.util.Map; public class OpenApiTool extends Tool { + public static final String SHORTHAND_PROPERTY = null; public Connection connection = null; public String specification = ""; @@ -27,11 +28,9 @@ public static OpenApiTool load(Object input, LoadContext context) { 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("kind") && map.get("kind") != null) { - result.kind = String.valueOf(map.get("kind")); - } if (map.containsKey("connection") && map.get("connection") != null) { result.connection = Connection.load(map.get("connection"), ctx); } @@ -40,12 +39,10 @@ static void loadBaseInto(OpenApiTool result, Map map, LoadContext ctx) { } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; OpenApiTool obj = ctx.processObject(this); Map result = super.save(ctx); - if (obj.kind != null) result.put("kind", serializeScalar(obj.kind)); if (obj.connection != null) result.put("connection", obj.connection.save(ctx)); if (obj.specification != null) result.put("specification", serializeScalar(obj.specification)); return result; 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 index bf3ad2d3c..a7b7de3f0 100644 --- 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 @@ -31,6 +31,7 @@ public static ParserConfig load(Object input, LoadContext context) { 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")); @@ -42,7 +43,6 @@ static void loadBaseInto(ParserConfig result, Map map, LoadContext ctx) { } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; ParserConfig obj = ctx.processObject(this); 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 index 41dbb6ec3..262562abe 100644 --- 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 @@ -31,6 +31,7 @@ public static PermissionCompletedPayload load(Object input, LoadContext context) 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")); @@ -57,7 +58,6 @@ static void loadBaseInto(PermissionCompletedPayload result, Map map, LoadC } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; PermissionCompletedPayload obj = ctx.processObject(this); 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 index f4155e47c..951e15275 100644 --- 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 @@ -30,6 +30,7 @@ public static PermissionDecision load(Object input, LoadContext context) { 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")); @@ -53,7 +54,6 @@ static void loadBaseInto(PermissionDecision result, Map map, LoadContext c } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; PermissionDecision obj = ctx.processObject(this); 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 index 669a5cfb3..72a783e4c 100644 --- 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 @@ -31,6 +31,7 @@ public static PermissionRequest load(Object input, LoadContext context) { 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")); @@ -59,7 +60,6 @@ static void loadBaseInto(PermissionRequest result, Map map, LoadContext ct } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; PermissionRequest obj = ctx.processObject(this); 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 index cf67d38c6..e10f67500 100644 --- 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 @@ -32,6 +32,7 @@ public static PermissionRequestedPayload load(Object input, LoadContext context) 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")); @@ -63,7 +64,6 @@ static void loadBaseInto(PermissionRequestedPayload result, Map map, LoadC } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; PermissionRequestedPayload obj = ctx.processObject(this); 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 index 67ae41a0c..38dad84b8 100644 --- 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 @@ -27,6 +27,7 @@ public static ProjectInfo load(Object input, LoadContext context) { 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")); @@ -39,7 +40,6 @@ static void loadBaseInto(ProjectInfo result, Map map, LoadContext ctx) { } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; ProjectInfo obj = ctx.processObject(this); 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 index e41e34720..b3782e7ac 100644 --- 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 @@ -34,6 +34,7 @@ public static Prompty load(Object input, LoadContext context) { 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")); @@ -72,7 +73,6 @@ static void loadBaseInto(Prompty result, Map map, LoadContext ctx) { } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; Prompty obj = ctx.processObject(this); 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 index 77cb6b6d4..cb5a8bd99 100644 --- 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 @@ -8,6 +8,7 @@ import java.util.Map; public class PromptyTool extends Tool { + public static final String SHORTHAND_PROPERTY = null; public String path = ""; public String mode = "single"; @@ -27,11 +28,9 @@ public static PromptyTool load(Object input, LoadContext context) { 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("kind") && map.get("kind") != null) { - result.kind = String.valueOf(map.get("kind")); - } if (map.containsKey("path") && map.get("path") != null) { result.path = String.valueOf(map.get("path")); } @@ -40,12 +39,10 @@ static void loadBaseInto(PromptyTool result, Map map, LoadContext ctx) { } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; PromptyTool obj = ctx.processObject(this); Map result = super.save(ctx); - if (obj.kind != null) result.put("kind", serializeScalar(obj.kind)); if (obj.path != null) result.put("path", serializeScalar(obj.path)); if (obj.mode != null) result.put("mode", serializeScalar(obj.mode)); return result; 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 index a33b9ffd2..5f14d6453 100644 --- 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 @@ -71,6 +71,7 @@ public static Property load(Object input, LoadContext context) { 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")); @@ -103,7 +104,6 @@ static void loadBaseInto(Property result, Map map, LoadContext ctx) { } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; Property obj = ctx.processObject(this); 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 index 52c453088..91ba083e9 100644 --- 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 @@ -27,6 +27,7 @@ public static RedactedField load(Object input, LoadContext context) { 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")); @@ -39,7 +40,6 @@ static void loadBaseInto(RedactedField result, Map map, LoadContext ctx) { } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; RedactedField obj = ctx.processObject(this); 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 index 1a09c4f92..4b03631cb 100644 --- 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 @@ -27,6 +27,7 @@ public static RedactionMetadata load(Object input, LoadContext context) { 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")))); @@ -40,7 +41,6 @@ static void loadBaseInto(RedactionMetadata result, Map map, LoadContext ct } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; RedactionMetadata obj = ctx.processObject(this); 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 index 4e8c5c593..7e8ea06a4 100644 --- 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 @@ -8,6 +8,7 @@ import java.util.Map; public class ReferenceConnection extends Connection { + public static final String SHORTHAND_PROPERTY = null; public String name = ""; public String target = null; @@ -27,11 +28,9 @@ public static ReferenceConnection load(Object input, LoadContext context) { 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("kind") && map.get("kind") != null) { - result.kind = String.valueOf(map.get("kind")); - } if (map.containsKey("name") && map.get("name") != null) { result.name = String.valueOf(map.get("name")); } @@ -40,12 +39,10 @@ static void loadBaseInto(ReferenceConnection result, Map map, LoadContext } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; ReferenceConnection obj = ctx.processObject(this); Map result = super.save(ctx); - if (obj.kind != null) result.put("kind", serializeScalar(obj.kind)); if (obj.name != null) result.put("name", serializeScalar(obj.name)); if (obj.target != null) result.put("target", serializeScalar(obj.target)); return result; 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 index 962797a51..01fd15fb9 100644 --- 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 @@ -8,6 +8,7 @@ import java.util.Map; public class RemoteConnection extends Connection { + public static final String SHORTHAND_PROPERTY = null; public String name = ""; public String endpoint = ""; @@ -27,11 +28,9 @@ public static RemoteConnection load(Object input, LoadContext context) { 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("kind") && map.get("kind") != null) { - result.kind = String.valueOf(map.get("kind")); - } if (map.containsKey("name") && map.get("name") != null) { result.name = String.valueOf(map.get("name")); } @@ -40,12 +39,10 @@ static void loadBaseInto(RemoteConnection result, Map map, LoadContext ctx } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; RemoteConnection obj = ctx.processObject(this); Map result = super.save(ctx); - if (obj.kind != null) result.put("kind", serializeScalar(obj.kind)); if (obj.name != null) result.put("name", serializeScalar(obj.name)); if (obj.endpoint != null) result.put("endpoint", serializeScalar(obj.endpoint)); return result; 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 index 749a4f146..62ddb46b3 100644 --- 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 @@ -36,6 +36,7 @@ public static ReplayJournalRecord load(Object input, LoadContext context) { 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"))); @@ -75,7 +76,6 @@ static void loadBaseInto(ReplayJournalRecord result, Map map, LoadContext } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; ReplayJournalRecord obj = ctx.processObject(this); 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 index d0de2ef19..1c59a8f84 100644 --- 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 @@ -28,6 +28,7 @@ public static ReplayMismatch load(Object input, LoadContext context) { 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")))); @@ -43,7 +44,6 @@ static void loadBaseInto(ReplayMismatch result, Map map, LoadContext ctx) } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; ReplayMismatch obj = ctx.processObject(this); 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 index 938b117d3..c6971b7c2 100644 --- 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 @@ -26,6 +26,7 @@ public static ReplayVerificationRequest load(Object input, LoadContext context) 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( @@ -37,7 +38,6 @@ static void loadBaseInto(ReplayVerificationRequest result, Map map, LoadCo } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; ReplayVerificationRequest obj = ctx.processObject(this); 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 index f00ff126d..dcdfa0b6b 100644 --- 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 @@ -28,6 +28,7 @@ public static ReplayVerificationResult load(Object input, LoadContext context) { 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"))); @@ -44,7 +45,6 @@ static void loadBaseInto(ReplayVerificationResult result, Map map, LoadCon } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; ReplayVerificationResult obj = ctx.processObject(this); 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 index 039d8dab7..e3f3d8ef7 100644 --- 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 @@ -29,6 +29,7 @@ public static ResumeContext load(Object input, LoadContext context) { 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); @@ -49,7 +50,6 @@ static void loadBaseInto(ResumeContext result, Map map, LoadContext ctx) { } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; ResumeContext obj = ctx.processObject(this); 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 index b7c542bdc..6f4520f4c 100644 --- 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 @@ -29,6 +29,7 @@ public static RetryPayload load(Object input, LoadContext context) { 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")); @@ -47,7 +48,6 @@ static void loadBaseInto(RetryPayload result, Map map, LoadContext ctx) { } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; RetryPayload obj = ctx.processObject(this); 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 index 124c80b2e..27584c936 100644 --- 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 @@ -28,6 +28,7 @@ public static RetryPolicyRequest load(Object input, LoadContext context) { 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")))); @@ -43,7 +44,6 @@ static void loadBaseInto(RetryPolicyRequest result, Map map, LoadContext c } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; RetryPolicyRequest obj = ctx.processObject(this); 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 index 3068b0c54..ced626ae0 100644 --- 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 @@ -28,6 +28,7 @@ public static RunTurnRequest load(Object input, LoadContext context) { 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")); @@ -45,7 +46,6 @@ static void loadBaseInto(RunTurnRequest result, Map map, LoadContext ctx) } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; RunTurnRequest obj = ctx.processObject(this); 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 index ae78fdf8c..3ac2b6c4a 100644 --- 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 @@ -31,6 +31,7 @@ public static RunTurnResult load(Object input, LoadContext context) { 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")); @@ -57,7 +58,6 @@ static void loadBaseInto(RunTurnResult result, Map map, LoadContext ctx) { } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; RunTurnResult obj = ctx.processObject(this); 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 index accc59cac..88c2519e8 100644 --- 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 @@ -28,6 +28,7 @@ public static SessionEndPayload load(Object input, LoadContext context) { 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")); @@ -43,7 +44,6 @@ static void loadBaseInto(SessionEndPayload result, Map map, LoadContext ct } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; SessionEndPayload obj = ctx.processObject(this); 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 index f9a7a1329..4cedd78a0 100644 --- 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 @@ -33,6 +33,7 @@ public static SessionEvent load(Object input, LoadContext context) { 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")); @@ -65,7 +66,6 @@ static void loadBaseInto(SessionEvent result, Map map, LoadContext ctx) { } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; SessionEvent obj = ctx.processObject(this); 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 index 984dab176..e82b2d64b 100644 --- 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 @@ -29,6 +29,7 @@ public static SessionFileRef load(Object input, LoadContext context) { 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")); @@ -47,7 +48,6 @@ static void loadBaseInto(SessionFileRef result, Map map, LoadContext ctx) } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; SessionFileRef obj = ctx.processObject(this); 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 index a8a9bffa1..0472b6d90 100644 --- 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 @@ -29,6 +29,7 @@ public static SessionRef load(Object input, LoadContext context) { 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")); @@ -47,7 +48,6 @@ static void loadBaseInto(SessionRef result, Map map, LoadContext ctx) { } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; SessionRef obj = ctx.processObject(this); 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 index 97c32c227..fcf95ab91 100644 --- 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 @@ -33,6 +33,7 @@ public static SessionStartPayload load(Object input, LoadContext context) { 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")); @@ -63,7 +64,6 @@ static void loadBaseInto(SessionStartPayload result, Map map, LoadContext } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; SessionStartPayload obj = ctx.processObject(this); 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 index 0f1933201..958e254d7 100644 --- 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 @@ -30,6 +30,7 @@ public static SessionSummary load(Object input, LoadContext context) { 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")); @@ -51,7 +52,6 @@ static void loadBaseInto(SessionSummary result, Map map, LoadContext ctx) } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; SessionSummary obj = ctx.processObject(this); 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 index 293ad8ec0..8ce1a27e8 100644 --- 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 @@ -35,6 +35,7 @@ public static SessionTrace load(Object input, LoadContext context) { 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")); @@ -77,7 +78,6 @@ static void loadBaseInto(SessionTrace result, Map map, LoadContext ctx) { } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; SessionTrace obj = ctx.processObject(this); 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 index f773b9723..f84c4e5bb 100644 --- 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 @@ -27,6 +27,7 @@ public static SessionWarningPayload load(Object input, LoadContext context) { 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")); @@ -41,7 +42,6 @@ static void loadBaseInto(SessionWarningPayload result, Map map, LoadContex } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; SessionWarningPayload obj = ctx.processObject(this); 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 index 52867fb49..166fb7b5b 100644 --- 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 @@ -25,13 +25,13 @@ public static StatusEventPayload load(Object input, LoadContext context) { 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); 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 index 264f4de38..1b94a1fd6 100644 --- 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 @@ -37,15 +37,15 @@ public static StreamChunk load(Object input, LoadContext context) { } } } - throw new IllegalArgumentException("Missing StreamChunk discriminator property: 'kind'"); + 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); 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 index 5354d8914..6d7da008e 100644 --- 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 @@ -25,13 +25,13 @@ public static StreamOptions load(Object input, LoadContext context) { 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); 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 index 8d249ee7f..02bb74938 100644 --- 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 @@ -27,6 +27,7 @@ public static SubscriptionInfo load(Object input, LoadContext context) { 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")); @@ -39,7 +40,6 @@ static void loadBaseInto(SubscriptionInfo result, Map map, LoadContext ctx } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; SubscriptionInfo obj = ctx.processObject(this); 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 index c44b1ac2f..8aa225294 100644 --- 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 @@ -26,6 +26,7 @@ public static Template load(Object input, LoadContext context) { 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); @@ -35,7 +36,6 @@ static void loadBaseInto(Template result, Map map, LoadContext ctx) { } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; Template obj = ctx.processObject(this); 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 index 8576e5cc0..efc90bb1d 100644 --- 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 @@ -8,6 +8,7 @@ import java.util.Map; public class TextChunk extends StreamChunk { + public static final String SHORTHAND_PROPERTY = null; public String value = ""; @@ -26,22 +27,18 @@ public static TextChunk load(Object input, LoadContext context) { 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("kind") && map.get("kind") != null) { - result.kind = String.valueOf(map.get("kind")); - } 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.kind != null) result.put("kind", serializeScalar(obj.kind)); if (obj.value != null) result.put("value", serializeScalar(obj.value)); return result; } 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 index a9a8e9d62..95a6454e4 100644 --- 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 @@ -8,6 +8,7 @@ import java.util.Map; public class TextPart extends ContentPart { + public static final String SHORTHAND_PROPERTY = null; public String value = ""; @@ -26,22 +27,18 @@ public static TextPart load(Object input, LoadContext context) { 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("kind") && map.get("kind") != null) { - result.kind = String.valueOf(map.get("kind")); - } 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.kind != null) result.put("kind", serializeScalar(obj.kind)); if (obj.value != null) result.put("value", serializeScalar(obj.value)); return result; } 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 index 0f423751c..fc1c1ccb6 100644 --- 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 @@ -8,6 +8,7 @@ import java.util.Map; public class ThinkingChunk extends StreamChunk { + public static final String SHORTHAND_PROPERTY = null; public String value = ""; @@ -26,22 +27,18 @@ public static ThinkingChunk load(Object input, LoadContext context) { 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("kind") && map.get("kind") != null) { - result.kind = String.valueOf(map.get("kind")); - } 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.kind != null) result.put("kind", serializeScalar(obj.kind)); if (obj.value != null) result.put("value", serializeScalar(obj.value)); return result; } 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 index 53d8e9bb5..7ac4abd30 100644 --- 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 @@ -25,13 +25,13 @@ public static ThinkingEventPayload load(Object input, LoadContext context) { 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); 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 index 41f7bb380..b247d3289 100644 --- 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 @@ -26,6 +26,7 @@ public static ThreadMarker load(Object input, LoadContext context) { 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")); @@ -35,7 +36,6 @@ static void loadBaseInto(ThreadMarker result, Map map, LoadContext ctx) { } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; ThreadMarker obj = ctx.processObject(this); 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 index e3361e864..4f2568d48 100644 --- 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 @@ -25,13 +25,13 @@ public static TokenEventPayload load(Object input, LoadContext context) { 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); 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 index f3d43ddf0..62974fda8 100644 --- 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 @@ -27,6 +27,7 @@ public static TokenUsage load(Object input, LoadContext context) { 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")))); @@ -39,7 +40,6 @@ static void loadBaseInto(TokenUsage result, Map map, LoadContext ctx) { } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; TokenUsage obj = ctx.processObject(this); 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 index 601ea914d..554cf94c6 100644 --- 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 @@ -38,8 +38,9 @@ public static Tool load(Object input, LoadContext context) { } } } - throw new IllegalArgumentException("Missing Tool discriminator property: 'kind'"); + 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")); @@ -56,7 +57,6 @@ static void loadBaseInto(Tool result, Map map, LoadContext ctx) { } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; Tool obj = ctx.processObject(this); 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 index 91b42b3a7..43c85cd71 100644 --- 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 @@ -27,6 +27,7 @@ public static ToolCall load(Object input, LoadContext context) { 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")); @@ -39,7 +40,6 @@ static void loadBaseInto(ToolCall result, Map map, LoadContext ctx) { } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; ToolCall obj = ctx.processObject(this); 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 index 6fb395a87..4ef9a14f1 100644 --- 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 @@ -30,6 +30,7 @@ public static ToolCallCompletePayload load(Object input, LoadContext context) { 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")); @@ -51,7 +52,6 @@ static void loadBaseInto(ToolCallCompletePayload result, Map map, LoadCont } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; ToolCallCompletePayload obj = ctx.processObject(this); 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 index 131946534..c6de661b5 100644 --- 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 @@ -27,6 +27,7 @@ public static ToolCallStartPayload load(Object input, LoadContext context) { 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")); @@ -39,7 +40,6 @@ static void loadBaseInto(ToolCallStartPayload result, Map map, LoadContext } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; ToolCallStartPayload obj = ctx.processObject(this); 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 index c2e777c1e..0324d9c55 100644 --- 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 @@ -8,6 +8,7 @@ import java.util.Map; public class ToolChunk extends StreamChunk { + public static final String SHORTHAND_PROPERTY = null; public ToolCall toolCall = null; @@ -26,22 +27,18 @@ public static ToolChunk load(Object input, LoadContext context) { 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("kind") && map.get("kind") != null) { - result.kind = String.valueOf(map.get("kind")); - } 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.kind != null) result.put("kind", serializeScalar(obj.kind)); if (obj.toolCall != null) result.put("toolCall", obj.toolCall.save(ctx)); return result; } 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 index b5ef2dcb3..59585a8f1 100644 --- 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 @@ -26,6 +26,7 @@ public static ToolContext load(Object input, LoadContext context) { 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( @@ -38,7 +39,6 @@ static void loadBaseInto(ToolContext result, Map map, LoadContext ctx) { } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; ToolContext obj = ctx.processObject(this); 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 index e9d643265..99c856557 100644 --- 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 @@ -27,6 +27,7 @@ public static ToolDispatchResult load(Object input, LoadContext context) { 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")); @@ -39,7 +40,6 @@ static void loadBaseInto(ToolDispatchResult result, Map map, LoadContext c } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; ToolDispatchResult obj = ctx.processObject(this); 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 index c275f4835..98c12cb4f 100644 --- 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 @@ -34,6 +34,7 @@ public static ToolExecutionCompletePayload load(Object input, LoadContext contex 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")); @@ -69,7 +70,6 @@ static void loadBaseInto(ToolExecutionCompletePayload result, Map map, Loa } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; ToolExecutionCompletePayload obj = ctx.processObject(this); 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 index af500fa39..ae3db40f0 100644 --- 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 @@ -30,6 +30,7 @@ public static ToolExecutionStartPayload load(Object input, LoadContext context) 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")); @@ -53,7 +54,6 @@ static void loadBaseInto(ToolExecutionStartPayload result, Map map, LoadCo } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; ToolExecutionStartPayload obj = ctx.processObject(this); 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 index 9bd4c1f46..8d309019a 100644 --- 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 @@ -29,6 +29,7 @@ public static ToolResult load(Object input, LoadContext context) { 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( @@ -48,7 +49,6 @@ static void loadBaseInto(ToolResult result, Map map, LoadContext ctx) { } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; ToolResult obj = ctx.processObject(this); @@ -94,7 +94,7 @@ public static ToolResult text(String value) { } public String text() { - throw new UnsupportedOperationException("Implement this helper outside generated code."); + return ToolResultMethods.text(this); } private static Map copyMap(Map source) { 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 index 09249f440..f13c0a803 100644 --- 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 @@ -26,6 +26,7 @@ public static ToolResultPayload load(Object input, LoadContext context) { 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")); @@ -35,7 +36,6 @@ static void loadBaseInto(ToolResultPayload result, Map map, LoadContext ct } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; ToolResultPayload obj = ctx.processObject(this); 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 index 19469cc2f..9392c961c 100644 --- 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 @@ -27,6 +27,7 @@ public static TraceFile load(Object input, LoadContext context) { 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")); @@ -39,7 +40,6 @@ static void loadBaseInto(TraceFile result, Map map, LoadContext ctx) { } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; TraceFile obj = ctx.processObject(this); 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 index 077db8260..d560aa8d8 100644 --- 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 @@ -33,6 +33,7 @@ public static TraceSpan load(Object input, LoadContext context) { 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")); @@ -72,7 +73,6 @@ static void loadBaseInto(TraceSpan result, Map map, LoadContext ctx) { } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; TraceSpan obj = ctx.processObject(this); 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 index 74d5c31e9..c5a302478 100644 --- 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 @@ -12,7 +12,7 @@ public class TraceTime { public String start = ""; public String end = ""; - public Double duration = 0.0; + public Double duration = 0.0d; public TraceTime() { } @@ -27,6 +27,7 @@ public static TraceTime load(Object input, LoadContext context) { 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")); @@ -39,7 +40,6 @@ static void loadBaseInto(TraceTime result, Map map, LoadContext ctx) { } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; TraceTime obj = ctx.processObject(this); 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 index 12c893ae0..a9a1c1122 100644 --- 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 @@ -33,6 +33,7 @@ public static TrajectoryEvent load(Object input, LoadContext context) { 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")); @@ -65,7 +66,6 @@ static void loadBaseInto(TrajectoryEvent result, Map map, LoadContext ctx) } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; TrajectoryEvent obj = ctx.processObject(this); 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 index ac66849e1..60f4e4f97 100644 --- 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 @@ -33,6 +33,7 @@ public static TurnCommit load(Object input, LoadContext context) { 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")); @@ -64,7 +65,6 @@ static void loadBaseInto(TurnCommit result, Map map, LoadContext ctx) { } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; TurnCommit obj = ctx.processObject(this); 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 index 19edc0ddb..3db654ed4 100644 --- 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 @@ -28,6 +28,7 @@ public static TurnEndPayload load(Object input, LoadContext context) { 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")))); @@ -43,7 +44,6 @@ static void loadBaseInto(TurnEndPayload result, Map map, LoadContext ctx) } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; TurnEndPayload obj = ctx.processObject(this); 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 index 6169e58ea..cffd52bcd 100644 --- 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 @@ -28,6 +28,7 @@ public static TurnEngineResult load(Object input, LoadContext context) { 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); @@ -45,7 +46,6 @@ static void loadBaseInto(TurnEngineResult result, Map map, LoadContext ctx } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; TurnEngineResult obj = ctx.processObject(this); 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 index e74c76f3a..35679b631 100644 --- 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 @@ -32,6 +32,7 @@ public static TurnEvent load(Object input, LoadContext context) { 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")); @@ -61,7 +62,6 @@ static void loadBaseInto(TurnEvent result, Map map, LoadContext ctx) { } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; TurnEvent obj = ctx.processObject(this); 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 index 189b7b3eb..274d59b3f 100644 --- 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 @@ -30,6 +30,7 @@ public static TurnModelRequest load(Object input, LoadContext context) { 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")); @@ -54,7 +55,6 @@ static void loadBaseInto(TurnModelRequest result, Map map, LoadContext ctx } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; TurnModelRequest obj = ctx.processObject(this); 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 index 96625676d..a52dd0211 100644 --- 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 @@ -28,6 +28,7 @@ public static TurnModelResponse load(Object input, LoadContext context) { 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"); @@ -46,7 +47,6 @@ static void loadBaseInto(TurnModelResponse result, Map map, LoadContext ct } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; TurnModelResponse obj = ctx.processObject(this); 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 index ed97978c3..0e7f324b2 100644 --- 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 @@ -31,6 +31,7 @@ public static TurnOptions load(Object input, LoadContext context) { 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")))); @@ -55,7 +56,6 @@ static void loadBaseInto(TurnOptions result, Map map, LoadContext ctx) { } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; TurnOptions obj = ctx.processObject(this); 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 index 93de61d1c..a2da211a3 100644 --- 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 @@ -27,6 +27,7 @@ public static TurnStartPayload load(Object input, LoadContext context) { 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")); @@ -41,7 +42,6 @@ static void loadBaseInto(TurnStartPayload result, Map map, LoadContext ctx } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; TurnStartPayload obj = ctx.processObject(this); 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 index 703cdaba8..0cd24696f 100644 --- 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 @@ -32,6 +32,7 @@ public static TurnSummary load(Object input, LoadContext context) { 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")); @@ -59,7 +60,6 @@ static void loadBaseInto(TurnSummary result, Map map, LoadContext ctx) { } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; TurnSummary obj = ctx.processObject(this); 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 index 3cad84b04..bfa6b2a04 100644 --- 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 @@ -29,6 +29,7 @@ public static TurnTrace load(Object input, LoadContext context) { 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")); @@ -48,7 +49,6 @@ static void loadBaseInto(TurnTrace result, Map map, LoadContext ctx) { } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; TurnTrace obj = ctx.processObject(this); 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 index 71439d66b..8e2699497 100644 --- 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 @@ -8,6 +8,7 @@ import java.util.Map; public class UnionProperty extends Property { + public static final String SHORTHAND_PROPERTY = null; public List oneOf = null; public List anyOf = null; @@ -27,11 +28,9 @@ public static UnionProperty load(Object input, LoadContext context) { 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("kind") && map.get("kind") != null) { - result.kind = String.valueOf(map.get("kind")); - } if (map.containsKey("oneOf") && map.get("oneOf") != null) { result.oneOf = ModelCollections.loadList( map.get("oneOf"), "oneOf", Property.SHORTHAND_PROPERTY, Property::load, ctx); @@ -42,12 +41,10 @@ static void loadBaseInto(UnionProperty result, Map map, LoadContext 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.kind != null) result.put("kind", serializeScalar(obj.kind)); if (obj.oneOf != null) { List items = new ArrayList<>(); for (Property item : obj.oneOf) items.add(item.save(ctx)); 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 index 84c226198..72a43cd86 100644 --- 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 @@ -8,6 +8,7 @@ import java.util.Map; public class UsageChunk extends StreamChunk { + public static final String SHORTHAND_PROPERTY = null; public InvocationUsage usage = null; @@ -26,22 +27,18 @@ public static UsageChunk load(Object input, LoadContext context) { 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("kind") && map.get("kind") != null) { - result.kind = String.valueOf(map.get("kind")); - } 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.kind != null) result.put("kind", serializeScalar(obj.kind)); if (obj.usage != null) result.put("usage", obj.usage.save(ctx)); return result; } 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 index a6d46bb81..c8ef58b7b 100644 --- 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 @@ -27,6 +27,7 @@ public static ValidationError load(Object input, LoadContext context) { 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")); @@ -39,7 +40,6 @@ static void loadBaseInto(ValidationError result, Map map, LoadContext ctx) } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; ValidationError obj = ctx.processObject(this); 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 index 9b72b14d9..2871d2a9c 100644 --- 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 @@ -26,6 +26,7 @@ public static ValidationResult load(Object input, LoadContext context) { 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")))); @@ -36,7 +37,6 @@ static void loadBaseInto(ValidationResult result, Map map, LoadContext ctx } } - public Map save(SaveContext context) { SaveContext ctx = context == null ? new SaveContext() : context; ValidationResult obj = ctx.processObject(this); 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/GeneratedModelExamplesTest.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/GeneratedModelExamplesTest.java deleted file mode 100644 index 4f31719ea..000000000 --- a/runtime/java/prompty/src/test/java/com/microsoft/prompty/GeneratedModelExamplesTest.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.microsoft.prompty; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.DynamicTest.dynamicTest; - -import com.microsoft.prompty.model.GeneratedModelExamples; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import org.junit.jupiter.api.DynamicTest; -import org.junit.jupiter.api.TestFactory; - -/** - * Executes every example suite emitted alongside the Typra-generated model. - * - *

The generated suites are plain {@code run()} entry points rather than JUnit - * methods, so the generation pipeline emits {@link GeneratedModelExamples} as a - * deterministic registry and this factory turns each entry into its own test. - */ -final class GeneratedModelExamplesTest { - - @TestFactory - List generatedExamples() { - Map cases = GeneratedModelExamples.all(); - assertFalse(cases.isEmpty(), "No generated model examples were registered"); - - List tests = new ArrayList<>(cases.size()); - for (Map.Entry entry : cases.entrySet()) { - tests.add(dynamicTest(entry.getKey(), entry.getValue()::run)); - } - return tests; - } -} diff --git a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/GeneratedModelExamples.java b/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/GeneratedModelExamples.java deleted file mode 100644 index 33ffbb55e..000000000 --- a/runtime/java/prompty/src/test/java/com/microsoft/prompty/model/GeneratedModelExamples.java +++ /dev/null @@ -1,162 +0,0 @@ -// -// Code generated by Typra emitter; DO NOT EDIT. -package com.microsoft.prompty.model; - -import java.util.LinkedHashMap; -import java.util.Map; - -public final class GeneratedModelExamples { - private GeneratedModelExamples() { } - - public static Map all() { - Map cases = new LinkedHashMap<>(); - cases.put("AiResourceInfoGeneratedTest", AiResourceInfoGeneratedTest::run); - cases.put("AnonymousConnectionGeneratedTest", AnonymousConnectionGeneratedTest::run); - cases.put("AnthropicImageBlockGeneratedTest", AnthropicImageBlockGeneratedTest::run); - cases.put("AnthropicImageSourceGeneratedTest", AnthropicImageSourceGeneratedTest::run); - cases.put("AnthropicMessagesRequestGeneratedTest", AnthropicMessagesRequestGeneratedTest::run); - cases.put("AnthropicMessagesResponseGeneratedTest", AnthropicMessagesResponseGeneratedTest::run); - cases.put("AnthropicTextBlockGeneratedTest", AnthropicTextBlockGeneratedTest::run); - cases.put("AnthropicToolDefinitionGeneratedTest", AnthropicToolDefinitionGeneratedTest::run); - cases.put("AnthropicToolResultBlockGeneratedTest", AnthropicToolResultBlockGeneratedTest::run); - cases.put("AnthropicToolUseBlockGeneratedTest", AnthropicToolUseBlockGeneratedTest::run); - cases.put("AnthropicUsageGeneratedTest", AnthropicUsageGeneratedTest::run); - cases.put("AnthropicWireMessageGeneratedTest", AnthropicWireMessageGeneratedTest::run); - cases.put("ApiKeyConnectionGeneratedTest", ApiKeyConnectionGeneratedTest::run); - cases.put("ArrayPropertyGeneratedTest", ArrayPropertyGeneratedTest::run); - cases.put("AudioPartGeneratedTest", AudioPartGeneratedTest::run); - cases.put("AuthorizationCodeFlowGeneratedTest", AuthorizationCodeFlowGeneratedTest::run); - cases.put("BindingGeneratedTest", BindingGeneratedTest::run); - cases.put("CheckpointGeneratedTest", CheckpointGeneratedTest::run); - cases.put("CompactionCompletePayloadGeneratedTest", CompactionCompletePayloadGeneratedTest::run); - cases.put("CompactionConfigGeneratedTest", CompactionConfigGeneratedTest::run); - cases.put("CompactionFailedPayloadGeneratedTest", CompactionFailedPayloadGeneratedTest::run); - cases.put("CompactionStartPayloadGeneratedTest", CompactionStartPayloadGeneratedTest::run); - cases.put("ConnectionGeneratedTest", ConnectionGeneratedTest::run); - cases.put("ContentPartGeneratedTest", ContentPartGeneratedTest::run); - cases.put("ContextCandidateGeneratedTest", ContextCandidateGeneratedTest::run); - cases.put("ContextRequestGeneratedTest", ContextRequestGeneratedTest::run); - cases.put("CustomToolGeneratedTest", CustomToolGeneratedTest::run); - cases.put("DelegatedStateReferenceGeneratedTest", DelegatedStateReferenceGeneratedTest::run); - cases.put("DeviceAuthorizationGeneratedTest", DeviceAuthorizationGeneratedTest::run); - cases.put("DoneEventPayloadGeneratedTest", DoneEventPayloadGeneratedTest::run); - cases.put("EngineCheckpointGeneratedTest", EngineCheckpointGeneratedTest::run); - cases.put("EngineEventGeneratedTest", EngineEventGeneratedTest::run); - cases.put("EnginePermissionDecisionGeneratedTest", EnginePermissionDecisionGeneratedTest::run); - cases.put("ErrorChunkGeneratedTest", ErrorChunkGeneratedTest::run); - cases.put("ErrorEventPayloadGeneratedTest", ErrorEventPayloadGeneratedTest::run); - cases.put("FileNotFoundErrorGeneratedTest", FileNotFoundErrorGeneratedTest::run); - cases.put("FilePartGeneratedTest", FilePartGeneratedTest::run); - cases.put("FinalOutputPolicyRequestGeneratedTest", FinalOutputPolicyRequestGeneratedTest::run); - cases.put("FinalOutputPolicyResultGeneratedTest", FinalOutputPolicyResultGeneratedTest::run); - cases.put("FormatConfigGeneratedTest", FormatConfigGeneratedTest::run); - cases.put("FoundryConnectionGeneratedTest", FoundryConnectionGeneratedTest::run); - cases.put("FunctionToolGeneratedTest", FunctionToolGeneratedTest::run); - cases.put("GuardrailResultGeneratedTest", GuardrailResultGeneratedTest::run); - cases.put("HarnessContextGeneratedTest", HarnessContextGeneratedTest::run); - cases.put("HookEndPayloadGeneratedTest", HookEndPayloadGeneratedTest::run); - cases.put("HookStartPayloadGeneratedTest", HookStartPayloadGeneratedTest::run); - cases.put("HostPolicyRequestGeneratedTest", HostPolicyRequestGeneratedTest::run); - cases.put("HostPolicyResultGeneratedTest", HostPolicyResultGeneratedTest::run); - cases.put("HostToolRequestGeneratedTest", HostToolRequestGeneratedTest::run); - cases.put("HostToolResultGeneratedTest", HostToolResultGeneratedTest::run); - cases.put("ImagePartGeneratedTest", ImagePartGeneratedTest::run); - cases.put("InvocationContextDecisionGeneratedTest", InvocationContextDecisionGeneratedTest::run); - cases.put("InvocationContextStateGeneratedTest", InvocationContextStateGeneratedTest::run); - cases.put("InvocationUsageGeneratedTest", InvocationUsageGeneratedTest::run); - cases.put("InvokerErrorGeneratedTest", InvokerErrorGeneratedTest::run); - cases.put("LlmCompletePayloadGeneratedTest", LlmCompletePayloadGeneratedTest::run); - cases.put("LlmStartPayloadGeneratedTest", LlmStartPayloadGeneratedTest::run); - cases.put("McpApprovalModeGeneratedTest", McpApprovalModeGeneratedTest::run); - cases.put("McpToolGeneratedTest", McpToolGeneratedTest::run); - cases.put("MemoryEntryGeneratedTest", MemoryEntryGeneratedTest::run); - cases.put("MemoryStoreGeneratedTest", MemoryStoreGeneratedTest::run); - cases.put("MessageGeneratedTest", MessageGeneratedTest::run); - cases.put("MessagesUpdatedPayloadGeneratedTest", MessagesUpdatedPayloadGeneratedTest::run); - cases.put("ModelGeneratedTest", ModelGeneratedTest::run); - cases.put("ModelInfoGeneratedTest", ModelInfoGeneratedTest::run); - cases.put("ModelInvocationContextSnapshotGeneratedTest", ModelInvocationContextSnapshotGeneratedTest::run); - cases.put("ModelInvocationRequestGeneratedTest", ModelInvocationRequestGeneratedTest::run); - cases.put("ModelInvocationResponseGeneratedTest", ModelInvocationResponseGeneratedTest::run); - cases.put("ModelOptionsGeneratedTest", ModelOptionsGeneratedTest::run); - cases.put("ModelReconciliationStateGeneratedTest", ModelReconciliationStateGeneratedTest::run); - cases.put("ModelToolRequestGeneratedTest", ModelToolRequestGeneratedTest::run); - cases.put("ModelToolResultGeneratedTest", ModelToolResultGeneratedTest::run); - cases.put("OAuthConnectionGeneratedTest", OAuthConnectionGeneratedTest::run); - cases.put("OAuthTokenGeneratedTest", OAuthTokenGeneratedTest::run); - cases.put("ObjectPropertyGeneratedTest", ObjectPropertyGeneratedTest::run); - cases.put("OpenApiToolGeneratedTest", OpenApiToolGeneratedTest::run); - cases.put("ParserConfigGeneratedTest", ParserConfigGeneratedTest::run); - cases.put("PermissionCompletedPayloadGeneratedTest", PermissionCompletedPayloadGeneratedTest::run); - cases.put("PermissionDecisionGeneratedTest", PermissionDecisionGeneratedTest::run); - cases.put("PermissionRequestGeneratedTest", PermissionRequestGeneratedTest::run); - cases.put("PermissionRequestedPayloadGeneratedTest", PermissionRequestedPayloadGeneratedTest::run); - cases.put("ProjectInfoGeneratedTest", ProjectInfoGeneratedTest::run); - cases.put("PromptyGeneratedTest", PromptyGeneratedTest::run); - cases.put("PromptyToolGeneratedTest", PromptyToolGeneratedTest::run); - cases.put("PropertyGeneratedTest", PropertyGeneratedTest::run); - cases.put("RedactedFieldGeneratedTest", RedactedFieldGeneratedTest::run); - cases.put("RedactionMetadataGeneratedTest", RedactionMetadataGeneratedTest::run); - cases.put("ReferenceConnectionGeneratedTest", ReferenceConnectionGeneratedTest::run); - cases.put("RemoteConnectionGeneratedTest", RemoteConnectionGeneratedTest::run); - cases.put("ReplayJournalRecordGeneratedTest", ReplayJournalRecordGeneratedTest::run); - cases.put("ReplayMismatchGeneratedTest", ReplayMismatchGeneratedTest::run); - cases.put("ReplayVerificationRequestGeneratedTest", ReplayVerificationRequestGeneratedTest::run); - cases.put("ReplayVerificationResultGeneratedTest", ReplayVerificationResultGeneratedTest::run); - cases.put("ResumeContextGeneratedTest", ResumeContextGeneratedTest::run); - cases.put("RetryPayloadGeneratedTest", RetryPayloadGeneratedTest::run); - cases.put("RetryPolicyRequestGeneratedTest", RetryPolicyRequestGeneratedTest::run); - cases.put("RunTurnRequestGeneratedTest", RunTurnRequestGeneratedTest::run); - cases.put("RunTurnResultGeneratedTest", RunTurnResultGeneratedTest::run); - cases.put("SessionEndPayloadGeneratedTest", SessionEndPayloadGeneratedTest::run); - cases.put("SessionEventGeneratedTest", SessionEventGeneratedTest::run); - cases.put("SessionFileRefGeneratedTest", SessionFileRefGeneratedTest::run); - cases.put("SessionRefGeneratedTest", SessionRefGeneratedTest::run); - cases.put("SessionStartPayloadGeneratedTest", SessionStartPayloadGeneratedTest::run); - cases.put("SessionSummaryGeneratedTest", SessionSummaryGeneratedTest::run); - cases.put("SessionTraceGeneratedTest", SessionTraceGeneratedTest::run); - cases.put("SessionWarningPayloadGeneratedTest", SessionWarningPayloadGeneratedTest::run); - cases.put("StatusEventPayloadGeneratedTest", StatusEventPayloadGeneratedTest::run); - cases.put("StreamChunkGeneratedTest", StreamChunkGeneratedTest::run); - cases.put("StreamOptionsGeneratedTest", StreamOptionsGeneratedTest::run); - cases.put("SubscriptionInfoGeneratedTest", SubscriptionInfoGeneratedTest::run); - cases.put("TemplateGeneratedTest", TemplateGeneratedTest::run); - cases.put("TextChunkGeneratedTest", TextChunkGeneratedTest::run); - cases.put("TextPartGeneratedTest", TextPartGeneratedTest::run); - cases.put("ThinkingChunkGeneratedTest", ThinkingChunkGeneratedTest::run); - cases.put("ThinkingEventPayloadGeneratedTest", ThinkingEventPayloadGeneratedTest::run); - cases.put("ThreadMarkerGeneratedTest", ThreadMarkerGeneratedTest::run); - cases.put("TokenEventPayloadGeneratedTest", TokenEventPayloadGeneratedTest::run); - cases.put("TokenUsageGeneratedTest", TokenUsageGeneratedTest::run); - cases.put("ToolCallCompletePayloadGeneratedTest", ToolCallCompletePayloadGeneratedTest::run); - cases.put("ToolCallGeneratedTest", ToolCallGeneratedTest::run); - cases.put("ToolCallStartPayloadGeneratedTest", ToolCallStartPayloadGeneratedTest::run); - cases.put("ToolChunkGeneratedTest", ToolChunkGeneratedTest::run); - cases.put("ToolContextGeneratedTest", ToolContextGeneratedTest::run); - cases.put("ToolDispatchResultGeneratedTest", ToolDispatchResultGeneratedTest::run); - cases.put("ToolExecutionCompletePayloadGeneratedTest", ToolExecutionCompletePayloadGeneratedTest::run); - cases.put("ToolExecutionStartPayloadGeneratedTest", ToolExecutionStartPayloadGeneratedTest::run); - cases.put("ToolGeneratedTest", ToolGeneratedTest::run); - cases.put("ToolResultGeneratedTest", ToolResultGeneratedTest::run); - cases.put("ToolResultPayloadGeneratedTest", ToolResultPayloadGeneratedTest::run); - cases.put("TraceFileGeneratedTest", TraceFileGeneratedTest::run); - cases.put("TraceSpanGeneratedTest", TraceSpanGeneratedTest::run); - cases.put("TraceTimeGeneratedTest", TraceTimeGeneratedTest::run); - cases.put("TrajectoryEventGeneratedTest", TrajectoryEventGeneratedTest::run); - cases.put("TurnCommitGeneratedTest", TurnCommitGeneratedTest::run); - cases.put("TurnEndPayloadGeneratedTest", TurnEndPayloadGeneratedTest::run); - cases.put("TurnEngineResultGeneratedTest", TurnEngineResultGeneratedTest::run); - cases.put("TurnEventGeneratedTest", TurnEventGeneratedTest::run); - cases.put("TurnModelRequestGeneratedTest", TurnModelRequestGeneratedTest::run); - cases.put("TurnModelResponseGeneratedTest", TurnModelResponseGeneratedTest::run); - cases.put("TurnOptionsGeneratedTest", TurnOptionsGeneratedTest::run); - cases.put("TurnStartPayloadGeneratedTest", TurnStartPayloadGeneratedTest::run); - cases.put("TurnSummaryGeneratedTest", TurnSummaryGeneratedTest::run); - cases.put("TurnTraceGeneratedTest", TurnTraceGeneratedTest::run); - cases.put("UnionPropertyGeneratedTest", UnionPropertyGeneratedTest::run); - cases.put("UsageChunkGeneratedTest", UsageChunkGeneratedTest::run); - cases.put("ValidationErrorGeneratedTest", ValidationErrorGeneratedTest::run); - cases.put("ValidationResultGeneratedTest", ValidationResultGeneratedTest::run); - return cases; - } -} 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 ffc42fe33..887bc75b7 100644 --- a/schema/package.json +++ b/schema/package.json @@ -8,11 +8,12 @@ "format:rust": "cargo fmt --all --manifest-path ../runtime/rust/prompty/Cargo.toml", "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/normalize-java-output.mjs b/schema/scripts/normalize-java-output.mjs index a9c88a2cc..657d61e38 100644 --- a/schema/scripts/normalize-java-output.mjs +++ b/schema/scripts/normalize-java-output.mjs @@ -2,59 +2,66 @@ // // TEMPORARY SHIM — remove once @typra/emitter's Java backend is fixed. // -// The Java language backend of @typra/emitter@0.4.2 emits source that does not -// compile and 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). +// 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). // -// Defects addressed (reported upstream): +// Pinned emitter: @typra/emitter@0.4.3. +// +// Residual defects still present in 0.4.3 (reported upstream): // -// J1 `default` is a Java reserved word but is emitted verbatim as a field -// name on `Property`. -// J2 `@abstract` models are emitted as `abstract class` yet their own -// `load()` fallback calls `new X()`; the `Tool` dispatch also omits the -// wildcard `*` catch-all subtype. -// J3 Enum-typed fields are assigned raw `String` values in `@factory` -// bodies and in shorthand `load()` paths. -// J4 `Long`/`Double`/`Float` fields are initialized with `int` literals. -// J6 Enum type names are emitted in lowerCamelCase. -// J7 Enums are emitted package-private alongside a public class in the same -// compilation unit, which is not legal Java. -// J8 Derived-class `load()` never populates base-class properties, and -// re-declares the discriminator field so it shadows the base field. // J9 Named collections are not normalized between their dictionary and list -// forms on load, and `collectionFormat` is not honoured on save. -// J10 Generated tests dot into `List<>` fields as if they were maps. -// J11 Generated tests double-escape expected string literals. -// J12 Generated tests compare object-valued fields against scalar wire values. -// J13 `Property.load` emits two consecutive `instanceof Number` branches, so -// the integer branch is unreachable. -// J14 Derived `save()` runs the `postSave` hook again after the base class has -// already run it. +// 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. +// 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. // -// J5 — `@method` stubs are emitted without an extension seam (Rust emits a -// trait, C# emits partial `*Helpers` classes) — is deliberately NOT addressed -// here. Those helpers are hand-written in the runtime package instead, so the -// generated model layer stays untouched. +// 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, unlinkSync } from "node:fs"; +import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; -/** Subtypes selected when a discriminated union sees an unknown discriminator. */ -const WILDCARD_SUBTYPE = { Tool: "CustomTool" }; - export function normalizeJavaOutput(root, specRoot) { if (!existsSync(root)) { return; @@ -69,9 +76,6 @@ export function normalizeJavaOutput(root, specRoot) { const optional = readOptionalProperties(specRoot); const namedCollections = readNamedCollectionFields(specRoot); - const enumRenames = extractEnums(root, units); - applyRenames(units, enumRenames); - const enumNames = collectEnumNames(units); const enumDefaults = collectEnumDefaults(units); const classes = indexClasses(units); @@ -81,22 +85,15 @@ export function normalizeJavaOutput(root, specRoot) { namedDictionarySave: 0, derivedSave: 0, scalarShorthand: 0, - loadBodies: 0, optionalDefaults: 0, requiredEnums: 0, - wildcard: 0, - misses: [], problems: [], }; for (const [name, unit] of units) { let text = unit.text; - text = fixReservedFieldNames(text); - text = fixNumericDefaults(text); text = clearOptionalDefaults(text, optional.get(name), stats); text = fixRequiredEnumDefaults(text, optional.get(name), enumDefaults, stats); - text = fixDiscriminatorDispatch(text, name, stats); - text = fixEnumAssignments(text, classes.get(name), enumNames); text = fixScalarShorthandDispatch(text, stats); const named = new Map(); text = fixNamedDictionaryLoads(text, units, stats, named); @@ -108,14 +105,13 @@ export function normalizeJavaOutput(root, specRoot) { extendSaveContext(units, stats); writeSupportClasses(root, units); - reshapeLoadMethods(units, classes, stats); assertRewritesApplied(stats, classes); for (const unit of units.values()) { writeFileSync(unit.path, unit.text); } - return { renames: enumRenames, classes: described }; + return { classes: described }; } /** @@ -275,68 +271,6 @@ function describeClasses(classes) { return described; } -// --------------------------------------------------------------------------- -// J6 + J7 — hoist enums into their own public compilation units -// --------------------------------------------------------------------------- - -function extractEnums(root, units) { - const renames = new Map(); - const header = "// \n// Code generated by Typra emitter; DO NOT EDIT.\n"; - - for (const unit of units.values()) { - const lines = unit.text.split("\n"); - const kept = []; - let index = 0; - while (index < lines.length) { - const match = /^enum (\w+) \{$/u.exec(lines[index]); - if (!match) { - kept.push(lines[index]); - index += 1; - continue; - } - const end = lines.indexOf("}", index); - const original = match[1]; - const renamed = original[0].toUpperCase() + original.slice(1); - if (renamed !== original) { - renames.set(original, renamed); - } - const body = lines.slice(index + 1, end).join("\n"); - const source = `${header}package com.microsoft.prompty.model;\n\npublic enum ${renamed} {\n${body}\n}\n`; - units.set(renamed, { path: join(root, `${renamed}.java`), text: source }); - // Drop the enum block plus the blank line that follows it. - index = end + 1; - if (lines[index] === "") { - index += 1; - } - } - unit.text = kept.join("\n"); - } - - return renames; -} - -function applyRenames(units, renames) { - if (renames.size === 0) { - return; - } - for (const unit of units.values()) { - let text = unit.text; - for (const [from, to] of renames) { - text = withLiteralsMasked(text, (code) => code.replace(new RegExp(`\\b${from}\\b`, "gu"), to)); - } - unit.text = text; - } -} - -function collectEnumNames(units) { - const names = new Set(); - for (const [name, unit] of units) { - if (/^public enum (\w+) \{$/mu.test(unit.text)) { - names.add(name); - } - } - return names; -} /** * Maps each generated enum to its first declared constant, which is the value @@ -389,108 +323,6 @@ function fixRequiredEnumDefaults(text, optional, enumDefaults, stats) { ); } -// --------------------------------------------------------------------------- -// J1 — reserved-word field names -// --------------------------------------------------------------------------- - -const RESERVED_FIELDS = { default: "defaultValue" }; - -function fixReservedFieldNames(text) { - return withLiteralsMasked(text, (code) => { - let result = code; - for (const [reserved, replacement] of Object.entries(RESERVED_FIELDS)) { - // Declaration: `public Object default = null;` - result = result.replace( - new RegExp(`^(\\s*public\\s+[\\w<>,\\[\\]. ]+?\\s+)${reserved}(\\s*=)`, "gmu"), - `$1${replacement}$2`, - ); - // Field access: `result.default`, `obj.default`, `this.default` - result = result.replace(new RegExp(`\\.${reserved}\\b(?!\\s*:)`, "gu"), `.${replacement}`); - } - return result; - }); -} - -/** - * Runs `transform` over the source with every string literal, text block, char - * literal and comment replaced by an opaque placeholder, then restores them. - * - * Rewrites that target Java identifiers must never see literal or comment text: - * `default` is both a reserved field name and a substring of real payload data - * (for example the OAuth scope `https://cognitiveservices.azure.com/.default`). - */ -function withLiteralsMasked(text, transform) { - const stash = []; - const masked = text.replace( - /"""[\s\S]*?"""|"(?:[^"\\\n]|\\.)*"|'(?:[^'\\]|\\.)*'|\/\*[\s\S]*?\*\/|\/\/[^\n]*/gu, - (match) => { - stash.push(match); - return `\u0000MASK${stash.length - 1}\u0000`; - }, - ); - return transform(masked).replace(/\u0000MASK(\d+)\u0000/gu, (_whole, index) => stash[Number(index)]); -} - -// --------------------------------------------------------------------------- -// J4 — boxed numeric literal defaults -// --------------------------------------------------------------------------- - -function fixNumericDefaults(text) { - return text - .replace(/^(\s*public Long \w+ = -?\d+)(;)$/gmu, "$1L$2") - .replace(/^(\s*public Double \w+ = -?\d+)(;)$/gmu, "$1.0$2") - .replace(/^(\s*public Float \w+ = -?\d+)(;)$/gmu, "$1f$2"); -} - -// --------------------------------------------------------------------------- -// J2 — discriminated-union dispatch semantics -// --------------------------------------------------------------------------- - -function fixDiscriminatorDispatch(text, className, stats) { - let result = text.replace( - /switch \(String\.valueOf\(discriminator\)\) \{/gu, - "switch (String.valueOf(discriminator).toLowerCase(java.util.Locale.ROOT)) {", - ); - - const wildcard = WILDCARD_SUBTYPE[className]; - if (wildcard && result.includes("Object discriminator = dispatchMap.get(")) { - result = result.replace( - /(\n\s*)default:\n\s*break;\n/u, - (whole, prefix) => { - stats.wildcard += 1; - return `${prefix}default:\n return ${wildcard}.load(data, ctx);\n`; - }, - ); - } - return result; -} - -// --------------------------------------------------------------------------- -// J3 — enum-typed fields assigned raw strings -// --------------------------------------------------------------------------- - -function fixEnumAssignments(text, info, enumNames) { - if (!info || info.enumFields.size === 0) { - return text; - } - let result = text; - for (const [field, type] of info.enumFields) { - if (!enumNames.has(type)) { - continue; - } - // Shorthand load path: `result.kind = String.valueOf(data);` - result = result.replace( - new RegExp(`(\\bresult\\.${field} = )String\\.valueOf\\(data\\);`, "gu"), - `$1${type}.fromValue(String.valueOf(data));`, - ); - // Factory bodies: `this.role = "assistant";` - result = result.replace( - new RegExp(`(\\bthis\\.${field} = )"([^"]*)";`, "gu"), - `$1${type}.fromValue("$2");`, - ); - } - return result; -} // --------------------------------------------------------------------------- // J9 — named-dictionary properties are not normalized into lists @@ -792,11 +624,6 @@ final class ModelCollections { */ function assertRewritesApplied(stats, classes) { const problems = []; - if (stats.misses.length > 0) { - problems.push( - `these classes declare load() but their field-population block was not recognised: ${stats.misses.join(", ")}`, - ); - } problems.push(...stats.problems); for (const [key, minimum] of Object.entries(EXPECTED_MINIMUMS)) { if (stats[key] < minimum) { @@ -835,17 +662,8 @@ const EXPECTED_MINIMUMS = { // ReplayVerificationResult.status, RunTurnResult.status, SessionEvent.type, // TurnCommit.status and TurnEvent.type. requiredEnums: 11, - wildcard: 1, - loadBodies: 100, }; -// --------------------------------------------------------------------------- -// J8 — base-property population and shadowed discriminator fields -// --------------------------------------------------------------------------- - -const LOAD_BODY_RE = - /(\n {4}if \(!\(data instanceof Map<\?, \?> map\)\) \{\n {6}return ctx\.processOutput\(new (\w+)\(\)\);\n {4}\}\n {4}\2 result = new \2\(\);\n)([\s\S]*?)(\n {4}return ctx\.processOutput\(result\);\n {2}\})/u; - function indexClasses(units) { const classes = new Map(); for (const [name, unit] of units) { @@ -873,78 +691,6 @@ function indexClasses(units) { return classes; } -function reshapeLoadMethods(units, classes, stats) { - // Pass 1 — split each class's field-population block into `loadBaseInto`. - for (const [name, info] of classes) { - const unit = units.get(name); - const match = LOAD_BODY_RE.exec(unit.text); - if (!match) { - if (unit.text.includes(`public static ${name} load(Object input, LoadContext context) {`)) { - stats.misses.push(name); - } - continue; - } - const [whole, prologue, , body, epilogue] = match; - info.hasLoadBody = true; - stats.loadBodies += 1; - - const chain = info.base ? ` ${info.base}.loadBaseInto(result, map, ctx);\n` : ""; - - const helper = - `\n static void loadBaseInto(${name} result, Map map, LoadContext ctx) {\n` + - `${chain}${body.replace(/^\n/u, "")}\n }\n`; - - let replacement; - if (info.isAbstract) { - replacement = - `\n throw new IllegalArgumentException(` + - `"Missing ${name} discriminator property: 'kind'");\n }`; - } else { - replacement = - `${prologue} ${name}.loadBaseInto(result, map, ctx);${epilogue}`; - } - - unit.text = unit.text.replace(whole, `${replacement}${helper}`); - } - - // Pass 2 — remove shadowed field declarations, seeding defaults in the ctor. - for (const [name, info] of classes) { - if (!info.base) { - continue; - } - const baseInfo = classes.get(info.base); - if (!baseInfo) { - continue; - } - const unit = units.get(name); - let text = unit.text; - const seeds = []; - for (const [field, { type, initializer }] of info.fields) { - const baseField = baseInfo.fields.get(field); - if (!baseField || baseField.type !== type) { - continue; - } - text = text.replace( - new RegExp(`^ {2}public ${escapeRe(type)} ${field} = .+;\\n`, "mu"), - "", - ); - if (initializer !== "null") { - seeds.push(` this.${field} = ${initializer};`); - } - } - if (seeds.length > 0) { - text = text.replace( - new RegExp(`^ {2}public ${name}\\(\\) \\{ \\}$`, "mu"), - ` public ${name}() {\n${seeds.join("\n")}\n }`, - ); - } - unit.text = text; - } -} - -function escapeRe(value) { - return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"); -} // --------------------------------------------------------------------------- // Generated tests @@ -961,11 +707,9 @@ export function normalizeJavaTests(root, model) { if (!existsSync(root)) { return; } - const renames = model?.renames ?? new Map(); const classes = model?.classes ?? new Map(); - const stats = { doubleEscaped: 0 }; + const stats = { doubleEscaped: 0, enumAssertions: 0 }; - const runnable = []; for (const name of readdirSync(root)) { if (!name.endsWith("GeneratedTest.java")) { continue; @@ -973,43 +717,13 @@ export function normalizeJavaTests(root, model) { const path = join(root, name); let text = readFileSync(path, "utf8"); - for (const [from, to] of renames) { - text = withLiteralsMasked(text, (code) => code.replace(new RegExp(`\\b${from}\\b`, "gu"), to)); - } - text = fixReservedFieldNames(text); - text = relaxEnumAssertions(text); + text = relaxEnumAssertions(text, stats); text = fixDoubleEscapedExpectations(text, stats); text = dropStructuredAssertions(text, classes, name.replace(/GeneratedTest\.java$/u, "")); - const body = /\n {2}static void run\(\) \{\n([\s\S]*?)\n {2}\}\n/u.exec(text); - if (!body || body[1].trim() === "") { - unlinkSync(path); - continue; - } - writeFileSync(path, text); - runnable.push(name.replace(/\.java$/u, "")); } - runnable.sort(); - const entries = runnable.map((name) => ` cases.put("${name}", ${name}::run);`).join("\n"); - writeFileSync( - join(root, `${REGISTRY_CLASS}.java`), - "// \n" + - "// Code generated by Typra emitter; DO NOT EDIT.\n" + - "package com.microsoft.prompty.model;\n\n" + - "import java.util.LinkedHashMap;\n" + - "import java.util.Map;\n\n" + - `public final class ${REGISTRY_CLASS} {\n` + - ` private ${REGISTRY_CLASS}() { }\n\n` + - " public static Map all() {\n" + - " Map cases = new LinkedHashMap<>();\n" + - `${entries}\n` + - " return cases;\n" + - " }\n" + - "}\n", - ); - // 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) { @@ -1019,11 +733,19 @@ export function normalizeJavaTests(root, model) { ` 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; -const REGISTRY_CLASS = "GeneratedModelExamples"; +// 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 @@ -1098,6 +820,10 @@ function javaEscape(value) { * 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); @@ -1152,12 +878,17 @@ function isScalarPath(classes, className, path) { * Generated examples compare enum-typed fields against their raw wire strings. * Teach the emitted `assertEquals` helper to unwrap enum constants first. */ -function relaxEnumAssertions(text) { +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( - /( {2}private static void assertEquals\(Object expected, Object actual, String message\) \{\n)/u, + anchor, "$1 expected = unwrapEnum(expected);\n actual = unwrapEnum(actual);\n", ).replace( - /( {2}private static void assertEquals\(Object expected, Object actual, String message\) \{\n)/u, + anchor, " private static Object unwrapEnum(Object value) {\n" + " if (value == null || !value.getClass().isEnum()) return value;\n" + " try {\n" + diff --git a/schema/tsp-output/.typra-generated/export-surfaces.json b/schema/tsp-output/.typra-generated/export-surfaces.json index 847e6812c..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 } ] diff --git a/schema/tsp-output/.typra-generated/manifest.json b/schema/tsp-output/.typra-generated/manifest.json index a70745394..aca97fdf5 100644 --- a/schema/tsp-output/.typra-generated/manifest.json +++ b/schema/tsp-output/.typra-generated/manifest.json @@ -3128,6 +3128,11 @@ "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", @@ -3218,11 +3223,21 @@ "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", @@ -3298,11 +3313,21 @@ "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", @@ -3338,6 +3363,16 @@ "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", @@ -3373,11 +3408,21 @@ "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", @@ -3438,6 +3483,11 @@ "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", @@ -3538,6 +3588,11 @@ "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", @@ -3563,6 +3618,16 @@ "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", @@ -3573,6 +3638,11 @@ "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", @@ -3588,6 +3658,11 @@ "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", @@ -3598,6 +3673,11 @@ "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", @@ -3608,11 +3688,21 @@ "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", @@ -3633,6 +3723,11 @@ "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", @@ -3758,6 +3853,11 @@ "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", @@ -3798,6 +3898,11 @@ "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", @@ -3818,6 +3923,11 @@ "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", From c36d37bafe781b43dc746225c79b0ddbfc9305c3 Mon Sep 17 00:00:00 2001 From: Seth Juarez Date: Tue, 4 Aug 2026 08:37:10 -0700 Subject: [PATCH 20/29] Cover the extension seams with tests Message.text(), Message.toTextContent() and ToolResult.text() delegate to hand-written seam classes, and the emitter creates those classes as throwing stubs when they are missing. The generated suites never call them, so a stubbed seam compiles, passes every test, and throws only in front of a provider - which is exactly how both seams stayed unimplemented. Pin the behaviour directly against the Rust reference, including the two cases Rust cannot express or does not exercise: an empty part list, which vacuously satisfies "every part is text" and must stay a string rather than becoming an empty array, and a null part list, which a Rust Vec cannot represent but a Java field can. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../microsoft/prompty/ExtensionSeamTest.java | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 runtime/java/prompty/src/test/java/com/microsoft/prompty/ExtensionSeamTest.java 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()); + } +} From bea126c5316320215828498dbc8f6eaf2f0519e7 Mon Sep 17 00:00:00 2001 From: Seth Juarez Date: Tue, 4 Aug 2026 08:37:11 -0700 Subject: [PATCH 21/29] Test the generated-output cleaner and guard generation in CI The cleaner deletes stale generated Java before each emit. It decided what to delete from the auto-generated marker, but nothing tested that decision, and the extension seams it must preserve are exactly the files whose loss would be silent - the emitter recreates them as throwing stubs. Extract cleanJavaOutput() so it can be driven directly and cover the branches that matter: seams are preserved, an unmarked hand-written file is refused rather than removed, and the pass is idempotent. One case is deliberately awkward - Typra's toPascalCase keeps the underscore before a digit, so a seam for type Foo_1 lands in Foo_1Methods.java and any file-name pattern would miss it. Marker-based matching is what makes that safe. Wire two jobs into the Java workflow. generation-scripts runs those tests. generation-drift regenerates and asserts runtime/java is unchanged, which catches a hand-edited generated file, a stale checked-in tree, and a non-deterministic emit. It is also the only place the shim's EXPECTED_MINIMUMS floors execute, so an upstream fix that turns a retained rewrite into a no-op now fails a build instead of rotting. It diffs only runtime/java on purpose: npm run generate omits the Rust and TypeSpec formatters that npm run build applies, so the other runtimes come out unformatted and would report a difference that means nothing. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/prompty-java-check.yml | 62 +++++++++++++++ schema/scripts/clean-java-output.mjs | 61 ++++++++++++--- schema/scripts/clean-java-output.test.mjs | 93 +++++++++++++++++++++++ 3 files changed, 204 insertions(+), 12 deletions(-) create mode 100644 schema/scripts/clean-java-output.test.mjs diff --git a/.github/workflows/prompty-java-check.yml b/.github/workflows/prompty-java-check.yml index c7b41eb8c..3b1258313 100644 --- a/.github/workflows/prompty-java-check.yml +++ b/.github/workflows/prompty-java-check.yml @@ -3,11 +3,73 @@ 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 }} diff --git a/schema/scripts/clean-java-output.mjs b/schema/scripts/clean-java-output.mjs index 3cfa08be1..862df974d 100644 --- a/schema/scripts/clean-java-output.mjs +++ b/schema/scripts/clean-java-output.mjs @@ -9,32 +9,69 @@ // // 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 the 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. +// +// 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"; -const MARKER = "// "; +export const MARKER = "// "; +export const SEAM_MARKER = "// Typra extension seam."; -const ROOTS = [ +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"), ]; -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}`); +/** + * 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; } - if (!readFileSync(path, "utf8").startsWith(MARKER)) { + 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 (text.startsWith(SEAM_MARKER)) { + 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.", ); } - rmSync(path); } + + 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..2ecec4546 --- /dev/null +++ b/schema/scripts/clean-java-output.test.mjs @@ -0,0 +1,93 @@ +// 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 } 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("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: [] }); +}); From 2355dbe1bd66d7dad5d030dc47cd5e81b8689eca Mon Sep 17 00:00:00 2001 From: Seth Juarez Date: Tue, 4 Aug 2026 09:02:07 -0700 Subject: [PATCH 22/29] fix(java): require every nested key in strict OpenAI schemas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OpenAI's strict JSON Schema dialect wants every property named in `required` at every nesting level, with semantic optionality carried by a null branch rather than by omission. Java widened `required` at the top level but not inside nested objects, so any prompt declaring a nested optional field produced a request the API rejects outright. The asymmetry was inherited from Rust, the behavioural reference, which has the same top-level-only widening. Python, TypeScript and C# already widen at every depth, and C# pins it with a dedicated test, so the majority and the API agree that recursion is correct. Rust still needs the mirror-image fix; this commit covers the Java slice only. Nested optional properties were already being given their null branch, so they were emitted as ["string", "null"] while still missing from `required` — an internally inconsistent shape. Listing them completes it rather than changing how optionality is expressed. One line does the whole job because all four strict-schema entry points — chat tool parameters, chat structured output, and the Responses API tool and text.format builders — funnel nested objects through the same recursive property walk. Objects reached through array items and union branches recurse back into it too. Non-strict output is untouched: the new condition collapses to the old one when strict is false, and a guard test pins that. Shared vectors needed no change; they cover only flat schemas, so the nested case was previously ungraded. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../com/microsoft/prompty/openai/Wire.java | 6 +- .../prompty/openai/WireSchemaTest.java | 116 +++++++++++++++++- 2 files changed, 115 insertions(+), 7 deletions(-) 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 index 7e1545747..b868f129c 100644 --- 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 @@ -396,7 +396,11 @@ private static Map propertySchema(Property property, boolean str } boolean isRequired = Boolean.TRUE.equals(child.required); nested.put(child.name, propertySchema(child, !isRequired, strict)); - if (isRequired) { + // 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); } } 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 index a8e0034e2..79a52e9d2 100644 --- 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 @@ -45,6 +45,45 @@ private static Map functionParametersSchema(List paramet 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) { + 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()); + Object schema = Streams.pointer(request, "response_format", "json_schema", "schema"); + return castMap(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); @@ -64,6 +103,71 @@ 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 schema = outputsSchema(requiredColorOptionalBorder()); + + assertEquals(List.of("row"), at(schema, "required")); + assertEquals(List.of("color", "border"), at(schema, "properties", "row", "required")); + assertEquals( + List.of("string", "null"), at(schema, "properties", "row", "properties", "border", "type")); + assertNoEmptyType(schema); + } + + /** 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 = @@ -117,11 +221,10 @@ void nestedUnionsAndOptionalityWiden() { false))))))))); assertEquals("object", at(schema, "properties", "row", "type")); - // Nested objects list only their genuinely required members, matching every other runtime. - // OpenAI's strict mode actually wants every key listed at every level, so a nested optional - // yields a schema the API rejects — but that rule lives in the shared spec, so it is reported - // upstream rather than fixed in one runtime, which would break cross-runtime agreement. - assertEquals(List.of("color", "fill"), at(schema, "properties", "row", "required")); + // 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")); @@ -135,8 +238,9 @@ void nestedUnionsAndOptionalityWiden() { 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"), + List.of("theme", "tint"), at(schema, "properties", "row", "properties", "fill", "anyOf", 1, "required")); assertNoEmptyType(schema); From da5a3f6efb1bf4477166fa4de12bbc3869d4bb9a Mon Sep 17 00:00:00 2001 From: Seth Juarez Date: Tue, 4 Aug 2026 09:06:04 -0700 Subject: [PATCH 23/29] test(java): prove nested strict schemas against the live API The offline tests assert what the wire builder emits; only a real request proves the service accepts it. Structured output was covered live, but with a flat schema, so the nested rule the previous commit implemented was ungraded end to end. This sends a prompt whose optional nested field is genuinely absent, so a faithful schema has to let the model answer null instead of inventing a value. It comes back as {city=London, postcode=null}. Reverting the widening turns this test into an HTTP 400 in which OpenAI states the rule itself: "In context=('properties', 'address'), 'required' is required to be supplied and to be an array including every key in properties. Missing 'postcode'." That is the clearest available evidence that the omission was a real defect rather than a stylistic difference. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../prompty/openai/OpenAILiveTest.java | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) 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 index 3cc0fba11..7f44420a6 100644 --- 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 @@ -237,6 +237,54 @@ void structuredOutputParsesIntoDeclaredFields() { 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 From 383717893295a0a31ce9647c138653b974e74308 Mon Sep 17 00:00:00 2001 From: Seth Juarez Date: Tue, 4 Aug 2026 10:41:16 -0700 Subject: [PATCH 24/29] test(java): accept an empty collection where a vector states null The shared vectors write `null` to mean "this field carries nothing", and the Java harness read that as "the saved value must be null exactly". That made Java the strict outlier: the generated models materialize optional collections, so a `tools` the wire never supplied arrives as an empty list and saves as `[]` rather than disappearing, and both reference runtimes emit it that way -- Rust inserts the saved collection unconditionally, and C# guards only on non-null, which an empty list passes. The other runtimes reconcile the two spellings at the comparison seam rather than in the model. Rust's vector test asserts `as_tools().is_none()`, where that helper reports `None` for an empty vector; Python checks length rather than identity, and says so in its failure message. Do the same here. The allowance is deliberately narrow. A collection carrying entries is still a real difference, an empty string is a value rather than an absence, and the key-set check is untouched, so a vector that omits a key entirely still rejects a runtime that emits it as an empty collection. Only an explicit `null` in a vector opts in. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../microsoft/prompty/SpecVectorsTest.java | 50 +++++++++++++++++++ .../com/microsoft/prompty/SpecVectors.java | 48 +++++++++++++++--- 2 files changed, 92 insertions(+), 6 deletions(-) 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 index f1db036b7..d7ee46fc8 100644 --- a/runtime/java/prompty/src/test/java/com/microsoft/prompty/SpecVectorsTest.java +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/SpecVectorsTest.java @@ -109,4 +109,54 @@ void theSubsetMatchStillIgnoresFieldsTheVectorDoesNotMention() { // 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/testFixtures/java/com/microsoft/prompty/SpecVectors.java b/runtime/java/prompty/src/testFixtures/java/com/microsoft/prompty/SpecVectors.java index 404040dc6..8484e4f25 100644 --- a/runtime/java/prompty/src/testFixtures/java/com/microsoft/prompty/SpecVectors.java +++ b/runtime/java/prompty/src/testFixtures/java/com/microsoft/prompty/SpecVectors.java @@ -11,6 +11,7 @@ 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; @@ -110,8 +111,11 @@ public static String string(Map value, String key) { * 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 is absent or null, which is how a vector says "this - * field should not be populated" without having to describe the whole surrounding object. + * 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 @@ -133,10 +137,13 @@ public static void assertMatches(String label, Object expected, Object actual) { * 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 one deliberate relaxation: numbers + *

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. Every other difference - * fails here exactly as it would there. + * 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); @@ -190,7 +197,14 @@ private static void assertMatches(String label, String path, Object expected, Ob String where = label + (path.isEmpty() ? "" : " at " + path); if (expected == null) { - assertTrue(actual == null, where + ": expected absent, got " + actual); + // 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; } @@ -279,6 +293,28 @@ 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 /** From 676d0929f5673f6327419dc11cc9fa637cd2b0ce Mon Sep 17 00:00:00 2001 From: Seth Juarez Date: Tue, 4 Aug 2026 11:06:20 -0700 Subject: [PATCH 25/29] fix(java): let a test state that an environment variable is absent A JVM cannot remove a name from its own environment, so `Environment` could only ever add a value on top of the ambient one. That left a whole family of tests -- "a missing credential is reported rather than guessed" -- silently at the mercy of the machine running them: green on a clean checkout, red the moment a developer or CI exported the same variable. Exporting a real Azure token to run the live Foundry suite was enough to turn one of them red, and `FoundryLiveTest` already carried a comment admitting a green result there was not evidence of anything. `Environment.mask` closes the gap. It is production API rather than test scaffolding because a host has the same need: running a prompt without inheriting an ambient key was previously inexpressible. Overrides and masks share one map so that each of set/mask/clear is a single mutation, and a concurrent lookup can never observe a gap in which the ambient value shows through. Seven tests that asserted absence now say so explicitly, including the load vectors, which mask any `${env:NAME}` they reference but do not supply. The live Foundry test drops its caveat and asserts unconditionally. clean env ./gradlew build 703 tests, 0 failures hostile env ./gradlew test 703 tests, 0 failures (hostile = AZURE_INFERENCE_CREDENTIAL, AZURE_OPENAI_{ENDPOINT, API_KEY,DEPLOYMENT}, ANTHROPIC_API_KEY, OPENAI_API_KEY, PROMPTY_TEST_VALUE, MISSING_VAR, NONEXISTENT all exported) Before this change the hostile run failed. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../anthropic/AnthropicExecutorTest.java | 3 + .../prompty/foundry/FoundryExecutorTest.java | 7 ++ .../prompty/foundry/FoundryLiveTest.java | 22 ++--- .../foundry/FoundryModelListerTest.java | 4 + .../com/microsoft/prompty/Environment.java | 53 ++++++++--- .../microsoft/prompty/LoadVectorsTest.java | 22 ++++- .../com/microsoft/prompty/RuntimeTest.java | 89 ++++++++++++++++++- 7 files changed, 165 insertions(+), 35 deletions(-) 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 index 6b02a8fb0..32c13f0f2 100644 --- 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 @@ -107,6 +107,9 @@ void theEnvironmentSuppliesTheKeyWhenTheConnectionDoesNot() { @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")); 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 index 298eef15f..68b29acba 100644 --- 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 @@ -148,6 +148,7 @@ void theDeploymentFallsBackToTheEnvironment() { @Test void aMissingEndpointIsReportedRatherThanGuessed() { + Environment.mask("AZURE_OPENAI_ENDPOINT"); Map model = new LinkedHashMap<>(Map.of("id", "gpt-4o-mini")); InvokerException error = assertThrows( @@ -157,6 +158,7 @@ void aMissingEndpointIsReportedRatherThanGuessed() { @Test void aMissingDeploymentIsReportedRatherThanGuessed() { + Environment.mask("AZURE_OPENAI_DEPLOYMENT"); Map model = new LinkedHashMap<>(keyModel()); model.remove("id"); InvokerException error = @@ -184,6 +186,7 @@ void theApiKeyFallsBackToTheEnvironment() { @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")); @@ -202,6 +205,10 @@ void aFoundryConnectionUsesABearerToken() { @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()))); 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 index 71e3d9a7f..3ba02ce69 100644 --- 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 @@ -119,33 +119,21 @@ void foundryProjectChatCompletionReturnsText() { void aMissingFoundryCredentialFailsBeforeAnyRequestIsSent() { LiveEnv.require("FOUNDRY_PROJECT_ENDPOINT"); - // The credential is restored afterwards because `.env` supplies it as an override, and clearing - // one without putting it back would make a later test in this class skip for want of a - // credential the machine actually has. - String saved = com.microsoft.prompty.Environment.lookup("AZURE_INFERENCE_CREDENTIAL").orElse(null); + // 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; - String credential; try { - com.microsoft.prompty.Environment.clear("AZURE_INFERENCE_CREDENTIAL"); - credential = LiveEnv.get("AZURE_INFERENCE_CREDENTIAL", ""); + 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 { - if (saved != null) { - com.microsoft.prompty.Environment.set("AZURE_INFERENCE_CREDENTIAL", saved); - } + com.microsoft.prompty.Environment.clear("AZURE_INFERENCE_CREDENTIAL"); } - if (!credential.isBlank()) { - // The credential lives in the real process environment, which `clear` cannot remove, so the - // request was legitimately authenticated and there is no absence to assert on. This test only - // does real work when the credential arrives as an override -- a green result here is not - // evidence that the missing-credential path behaves correctly. - return; - } assertNotNull(failure, "a missing credential should fail rather than send an anonymous request"); String message = String.valueOf(failure.getMessage()); assertTrue( 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 index 2ff4da9d2..7ac057e5f 100644 --- 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 @@ -165,6 +165,9 @@ void aConnectionApiVersionOverridesTheDefault() { @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())); } } @@ -204,6 +207,7 @@ void aBlankEnvironmentKeyDoesNotSatisfyTheRequirement() { @Test void aMissingDeploymentTokenNamesTheWayToGetOne() { + com.microsoft.prompty.Environment.mask("AZURE_INFERENCE_CREDENTIAL"); InvokerException error = assertThrows( InvokerException.class, () -> FoundryModelLister.deploymentToken(Map.of())); 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 index 278490096..283c424a8 100644 --- a/runtime/java/prompty/src/main/java/com/microsoft/prompty/Environment.java +++ b/runtime/java/prompty/src/main/java/com/microsoft/prompty/Environment.java @@ -15,37 +15,66 @@ * *

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 { - private static final Map OVERRIDES = new ConcurrentHashMap<>(); + /** + * 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. */ + /** + * 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) { - OVERRIDES.remove(name); + DECISIONS.remove(name); } else { - OVERRIDES.put(name, value); + DECISIONS.put(name, Optional.of(value)); } } - /** Remove a value previously supplied through {@link #set}. */ + /** + * 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) { - OVERRIDES.remove(name); + DECISIONS.remove(name); } - /** Remove every value previously supplied through {@link #set}. */ + /** Remove every value supplied through {@link #set} and every mask applied by {@link #mask}. */ public static void clearAll() { - OVERRIDES.clear(); + DECISIONS.clear(); } - /** Look up {@code name}, or an empty optional if it is set nowhere. */ + /** Look up {@code name}, or an empty optional if it is set nowhere or has been masked. */ public static Optional lookup(String name) { - String override = OVERRIDES.get(name); - if (override != null) { - return Optional.of(override); + Optional decision = DECISIONS.get(name); + if (decision != null) { + return decision; } String property = System.getProperty(name); if (property != null) { 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 index e9e019044..66ee03630 100644 --- a/runtime/java/prompty/src/test/java/com/microsoft/prompty/LoadVectorsTest.java +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/LoadVectorsTest.java @@ -14,6 +14,8 @@ 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; @@ -28,6 +30,9 @@ */ 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<>(); @@ -43,7 +48,7 @@ private void runCase(String name, Map testCase) { Map expected = SpecVectors.map(testCase, "expected"); Map env = SpecVectors.map(input, "env"); - List applied = setEnv(env); + List applied = setEnv(input, env); try { if (expected.containsKey("error")) { runErrorCase(name, input, expected); @@ -183,12 +188,25 @@ private static void write(Path target, String content) { // ---------------------------------------------------------------- environment - private static List setEnv(Map env) { + 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; } 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 index 84cd57a60..f33183335 100644 --- a/runtime/java/prompty/src/test/java/com/microsoft/prompty/RuntimeTest.java +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/RuntimeTest.java @@ -5,6 +5,7 @@ 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; @@ -36,6 +37,13 @@ */ 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 @@ -163,13 +171,86 @@ void builtInRenderersAndParserAreAvailable() { @Test void explicitValuesOutrankTheAmbientEnvironment() { - Environment.set("PROMPTY_TEST_VALUE", "explicit"); + // 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 { - assertEquals("explicit", Environment.lookup("PROMPTY_TEST_VALUE").orElseThrow()); + 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("PROMPTY_TEST_VALUE"); + Environment.clear(AMBIENT); + System.clearProperty(AMBIENT); } - assertTrue(Environment.lookup("PROMPTY_TEST_VALUE").isEmpty(), "clearing should remove the value"); } // ---------------------------------------------------------------- structured results From bb695dab5519775cbbe6134bf052983c66490bae Mon Sep 17 00:00:00 2001 From: Seth Juarez Date: Tue, 4 Aug 2026 11:44:47 -0700 Subject: [PATCH 26/29] docs(schema): record how the Java normalization shim is retired The shim header explained why each rewrite exists but never said what has to happen for the file to go away, so a reviewer could not tell whether it was a permanent fixture or a tracked temporary one. Retirement is not deleting one file. The shim is wired into normalize-typra-output.mjs and referenced from clean-java-output.mjs, the Java README, ModelNormalizationTest and the workflow path filter, so record the whole set together with the regeneration and full-suite run that has to pass afterwards. Also record the two emitter defects that currently block that evaluation, confirmed against emitter commit 7595113. Both are stated as behavioural requirements rather than as a prescribed implementation. The enumValues note calls out that only the field default is wrong and that suppressing empty lists on save would be incorrect, since that would stop an explicitly supplied [] from round-tripping. Comment-only: regeneration output is byte-identical and the Java suite still passes 703 tests across 77 files with no generated-file drift. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- schema/scripts/normalize-java-output.mjs | 31 ++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/schema/scripts/normalize-java-output.mjs b/schema/scripts/normalize-java-output.mjs index 657d61e38..aaf48325e 100644 --- a/schema/scripts/normalize-java-output.mjs +++ b/schema/scripts/normalize-java-output.mjs @@ -55,6 +55,37 @@ // 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. From 666091ed67109eaf36759007eb46e2561b260812 Mon Sep 17 00:00:00 2001 From: Seth Juarez Date: Tue, 4 Aug 2026 12:15:06 -0700 Subject: [PATCH 27/29] test(java): grade structured output against the full strict-widening contract Recursive strict widening was already implemented for both schema paths, but only the tool-parameter test graded the whole contract. The structured-output test checked the required list and the nullable branch while leaving nested and top-level additionalProperties unasserted, so that path could have regressed independently of the tools path and still gone green. Assert the remaining terms the canonical vector pins down, and add the one the schema alone cannot express: the response_format envelope has to declare strict. Widening every nested key into required is only correct under strict mode, so if the envelope ever stopped declaring it the request would be wrong while every schema assertion carried on passing. Behaviour is unchanged; this is assertion coverage only. Both mutants are killed: dropping the nested `strict || isRequired` fails the tool-parameter and structured-output tests together, and flipping the envelope to `strict: false` fails the new assertion. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../prompty/openai/WireSchemaTest.java | 35 ++++++++++++++++--- 1 file changed, 30 insertions(+), 5 deletions(-) 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 index 79a52e9d2..45be18247 100644 --- 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 @@ -51,6 +51,16 @@ private static Map functionParametersSchema(List paramet * 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"); @@ -60,8 +70,7 @@ private static Map outputsSchema(List outputs) { Prompty agent = Prompty.load(data, new LoadContext()); Map request = Wire.buildChatArgs(agent, List.of()); - Object schema = Streams.pointer(request, "response_format", "json_schema", "schema"); - return castMap(schema); + return castMap(Streams.pointer(request, "response_format", "json_schema")); } /** @@ -131,13 +140,29 @@ void strictNestedObjectRequiresEveryKeyAndNullsTheOptionalOnes() { */ @Test void structuredOutputAppliesTheSameNestedRuleAsTools() { - Map schema = outputsSchema(requiredColorOptionalBorder()); + 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")); assertEquals( - List.of("string", "null"), at(schema, "properties", "row", "properties", "border", "type")); + 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. */ From bda4c379168c7aacfe8b8044559fb3589a18263b Mon Sep 17 00:00:00 2001 From: Seth Juarez Date: Tue, 4 Aug 2026 13:38:31 -0700 Subject: [PATCH 28/29] Accept every known Typra seam marker when cleaning Java output `cleanJavaOutput` is fail-closed: a file under the generated roots that carries neither the generated marker nor a seam marker aborts the build rather than being deleted. That is the right default, but it made the seam marker a single hard-coded string, so the first emitter release that changes the marker would stop the build with "refusing to clean" instead of a useful diagnostic -- and the pinned 0.4.3 prose marker, the `` tag, and the `` tag are all in play across in-flight emitter work. Accept all three as an explicit allow-list so hand-written seam bodies survive that transition. The exposure is bounded deliberately: an unrecognised marker still aborts, generated files open with the generated marker and are deleted before the seam check runs, and the tag spellings must match the opening line exactly so a marker cannot be smuggled in as a prefix of unrelated content. Only the pinned prose marker matches by prefix, because its documented form continues with a trailing sentence. Verified byte-neutral: `npm run generate` at the current 0.4.3 pin leaves every generated Java file unchanged, and both real seam files (MessageMethods, ToolResultMethods) are still preserved. schema/scripts: 11/11 node tests pass. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- schema/scripts/clean-java-output.mjs | 42 ++++++++++++++++++++-- schema/scripts/clean-java-output.test.mjs | 43 ++++++++++++++++++++++- 2 files changed, 82 insertions(+), 3 deletions(-) diff --git a/schema/scripts/clean-java-output.mjs b/schema/scripts/clean-java-output.mjs index 862df974d..4f6c84bf7 100644 --- a/schema/scripts/clean-java-output.mjs +++ b/schema/scripts/clean-java-output.mjs @@ -13,12 +13,29 @@ // 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 the seam marker instead of the generated marker; deleting them would +// 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. @@ -30,6 +47,27 @@ 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"), @@ -58,7 +96,7 @@ export function cleanJavaOutput(roots = ROOTS) { removed.push(path); continue; } - if (text.startsWith(SEAM_MARKER)) { + if (hasSeamMarker(text)) { preserved.push(path); continue; } diff --git a/schema/scripts/clean-java-output.test.mjs b/schema/scripts/clean-java-output.test.mjs index 2ecec4546..18472d882 100644 --- a/schema/scripts/clean-java-output.test.mjs +++ b/schema/scripts/clean-java-output.test.mjs @@ -11,7 +11,7 @@ import { mkdtempSync, mkdirSync, writeFileSync, existsSync, rmSync } from "node: import { tmpdir } from "node:os"; import { join } from "node:path"; -import { cleanJavaOutput, MARKER, SEAM_MARKER } from "./clean-java-output.mjs"; +import { cleanJavaOutput, MARKER, SEAM_MARKER, SEAM_MARKERS } from "./clean-java-output.mjs"; function withRoot(files, run) { const dir = mkdtempSync(join(tmpdir(), "clean-java-")); @@ -57,6 +57,47 @@ test("preserves a seam whose name defeats a file-name pattern", () => { }); }); +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); From 31f4962d2d1d1513acbff75a1ecfa38c1276387a Mon Sep 17 00:00:00 2001 From: Seth Juarez Date: Tue, 4 Aug 2026 13:38:45 -0700 Subject: [PATCH 29/29] Grade both canonical named-collection contracts in the Java gate PR #447 (head 31ff17de) settled the two cross-runtime contracts this runtime had left open. Both are now graded directly. `recursive-array-valued-entry-rejection` requires an immediate array value inside a name-keyed object to be rejected at every recursive named-collection boundary, with a diagnostic carrying the path and the category. Probing the built classes shows Java already conforms at all three reachable boundaries, so this is pinned rather than fixed: * top level inputs.firstName * inside a list element tools[].parameters.toolArg * through a subclass inputs.cfg (ObjectProperty) .properties.nestedField Each assertion names the collection, the offending entry, and the word `array`, using a distinct entry name per boundary so a degraded generic diagnostic cannot satisfy all three. The pre-existing top-level test only checked the path, so it gains the category assertion the contract wants. `named-collection-lossless-fallback` requires object encoding only when every name is non-empty and exactly unique, and the whole ordered array otherwise, with duplicate detection before the map is built. `duplicateNamesCollapseOnSave` records that Java does not yet do this: two duplicate-named entries load as two entries but save as a single key, silently discarding the earlier payload. The collapse happens in emitter-generated save code, so it cannot be fixed from this PR without hand-editing generated output; the test is a characterization that fails loudly at its `assertInstanceOf(Map.class, ...)` the moment the lossless array fallback ships, at which point it should be inverted. runtime/java: 724 tests, 0 failures, 0 errors, 2 known skips, 80 files (live providers enabled). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../prompty/ModelNormalizationTest.java | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) 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 index 0b1a1532a..fe6ab8556 100644 --- a/runtime/java/prompty/src/test/java/com/microsoft/prompty/ModelNormalizationTest.java +++ b/runtime/java/prompty/src/test/java/com/microsoft/prompty/ModelNormalizationTest.java @@ -103,6 +103,49 @@ void rejectsNestedArrayValues() { 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 @@ -308,6 +351,38 @@ void unnamedEntriesFallBackToArray() { 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() {