Skip to content
Merged

Dev #2579

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
# Runtime Prompt Templates

These files are the source of truth for runtime prompts.
`OpenAIRuntimeService` resolves templates from:
Runtime prompts are now resolved from the database-backed `AIPrompts` and `AIPromptVersions` records seeded by the AI module.
These files are retained as prompt asset references and seed inputs, not as the runtime source of truth.

- `AI/Prompts/Versions/<version>/<template>.txt`

Current templates:
Current prompt asset references:

- `application-analysis.system.txt`
- `application-analysis.user.txt`
Expand Down Expand Up @@ -45,10 +43,6 @@ Version selection:

Template loading is strict:

- Core templates are required for each version.
- Missing required templates fail fast at runtime with a configuration error.
- Fragment templates are required when the corresponding placeholder is present in the parent template.
- Fragment resolution is automatic using `<base>.<placeholder-lower>.txt` from the same version folder.
- Example: `application-analysis.user.txt` with `{{RULES}}` resolves `application-analysis.rules.txt`.
- `{{COMMON_*}}` placeholders resolve to `common.<suffix>.txt` where suffix is lower-cased and `_` becomes `.`.
- Example: `{{COMMON_RULES}}` resolves `common.rules.txt`.
- Core prompt records are required for each version.
- Missing required prompt records fail fast at runtime with a configuration error.
- Runtime prompt rendering resolves placeholders from the stored template text plus the version metadata sections.
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Unity.AI.Domain;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.MultiTenancy;

namespace Unity.AI.Runtime;

public class AIPromptTemplateProvider(
IRepository<AIPrompt, Guid> promptRepository,
IRepository<AIPromptVersion, Guid> promptVersionRepository,
ICurrentTenant currentTenant) : IAIPromptTemplateProvider, ITransientDependency
{
public async Task<AIPromptTemplateSnapshot> GetRequiredPromptAsync(
string promptType,
string promptVersion,
CancellationToken cancellationToken = default)
{
var normalizedPromptVersion = OpenAIPromptRenderer.ResolvePromptVersion(promptVersion);
var versionNumber = OpenAIPromptRenderer.ResolvePromptVersionNumber(normalizedPromptVersion);

using (currentTenant.Change(null))
{
var prompt = await promptRepository.FindAsync(p => p.Name == promptType);
if (prompt == null || !prompt.IsActive)
{
throw new InvalidOperationException($"AI prompt '{promptType}' is not configured.");
}

var version = await promptVersionRepository.FindAsync(
v => v.PromptId == prompt.Id && v.VersionNumber == versionNumber);
if (version == null || !version.IsPublished || version.IsDeprecated)
{
throw new InvalidOperationException(
$"AI prompt version '{normalizedPromptVersion}' for prompt '{promptType}' is not configured.");
}

return new AIPromptTemplateSnapshot(
normalizedPromptVersion,
version.SystemPrompt,
version.UserPromptTemplate,
version.MetadataJson);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;

namespace Unity.AI.Runtime;

public static class AIPromptTemplateRenderer
{
public static string BuildApplicationAnalysisUserPrompt(
string userPromptTemplate,
string schema,
string data,
string attachments,
string? metadataJson = null)
{
return RenderPromptTemplate(
userPromptTemplate,
metadataJson,
new Dictionary<string, string>
{
["SCHEMA"] = schema,
["DATA"] = data,
["ATTACHMENTS"] = attachments
});
}

public static string BuildAttachmentSummaryUserPrompt(
string userPromptTemplate,
string attachment,
string? metadataJson = null)
{
return RenderPromptTemplate(
userPromptTemplate,
metadataJson,
new Dictionary<string, string>
{
["ATTACHMENT"] = attachment
});
}

public static string BuildApplicationScoringUserPrompt(
string userPromptTemplate,
string data,
string attachments,
string section,
string response,
string? metadataJson = null)
{
return RenderPromptTemplate(
userPromptTemplate,
metadataJson,
new Dictionary<string, string>
{
["DATA"] = data,
["ATTACHMENTS"] = attachments,
["SECTION"] = section,
["RESPONSE"] = response
});
}

private static string RenderPromptTemplate(
string template,
string? metadataJson,
IReadOnlyDictionary<string, string> runtimeReplacements)
{
var placeholders = GetTemplatePlaceholders(template);
var replacements = new Dictionary<string, string>(runtimeReplacements, StringComparer.Ordinal);

foreach (var (key, value) in ExtractMetadataSections(metadataJson))
{
replacements.TryAdd(key, value);
}

if (!replacements.ContainsKey("RESPONSE") && replacements.TryGetValue("OUTPUT", out var outputTemplate))
{
replacements["RESPONSE"] = outputTemplate;
}
else if (!replacements.ContainsKey("OUTPUT") && replacements.TryGetValue("RESPONSE", out var responseTemplate))
{
replacements["OUTPUT"] = responseTemplate;
}

var unresolved = placeholders
.Where(placeholder => !replacements.ContainsKey(placeholder))
.OrderBy(placeholder => placeholder)
.ToList();
if (unresolved.Count > 0)
{
throw new InvalidOperationException(
$"Unresolved prompt placeholders: {string.Join(", ", unresolved)}");
}

var rendered = template;
foreach (var placeholder in placeholders)
{
rendered = rendered.Replace($"{{{{{placeholder}}}}}", replacements[placeholder] ?? string.Empty, StringComparison.Ordinal);
}

return rendered;
}

private static Dictionary<string, string> ExtractMetadataSections(string? metadataJson)
{
if (string.IsNullOrWhiteSpace(metadataJson))
{
return new Dictionary<string, string>(StringComparer.Ordinal);
}

try
{
using var doc = JsonDocument.Parse(metadataJson);
var root = doc.RootElement;
if (root.ValueKind != JsonValueKind.Object)
{
return new Dictionary<string, string>(StringComparer.Ordinal);
}

if (root.TryGetProperty("sections", out var sections) && sections.ValueKind == JsonValueKind.Object)
{
return ExtractStringProperties(sections);
}

return ExtractStringProperties(root);
}
catch (JsonException ex)
{
throw new InvalidOperationException("Invalid prompt metadata JSON.", ex);
}
}

private static Dictionary<string, string> ExtractStringProperties(JsonElement element)
{
var values = new Dictionary<string, string>(StringComparer.Ordinal);
foreach (var property in element.EnumerateObject())
{
if (property.Value.ValueKind == JsonValueKind.String)
{
values[property.Name] = property.Value.GetString() ?? string.Empty;
}
}

return values;
}

private static HashSet<string> GetTemplatePlaceholders(string template)
{
var placeholders = new HashSet<string>(StringComparer.Ordinal);
var invalidPlaceholders = new List<string>();
var searchIndex = 0;

while (searchIndex < template.Length)
{
var start = template.IndexOf("{{", searchIndex, StringComparison.Ordinal);
if (start < 0)
{
break;
}

var end = template.IndexOf("}}", start + 2, StringComparison.Ordinal);
if (end < 0)
{
break;
}

var placeholder = template.Substring(start + 2, end - start - 2).Trim();
if (IsPromptPlaceholder(placeholder))
{
placeholders.Add(placeholder);
}
else
{
invalidPlaceholders.Add(placeholder);
}

searchIndex = end + 2;
}

if (invalidPlaceholders.Count > 0)
{
throw new InvalidOperationException(
$"Invalid prompt placeholders: {string.Join(", ", invalidPlaceholders.OrderBy(item => item))}");
}

return placeholders;
}

private static bool IsPromptPlaceholder(string placeholder)
{
return !string.IsNullOrWhiteSpace(placeholder) &&
placeholder.All(character => char.IsUpper(character) || char.IsDigit(character) || character == '_');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Unity.AI.Runtime;

public sealed record AIPromptTemplateSnapshot(
string PromptVersion,
string SystemPrompt,
string UserPromptTemplate,
string? MetadataJson);
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.Threading;
using System.Threading.Tasks;

namespace Unity.AI.Runtime;

public interface IAIPromptTemplateProvider
{
Task<AIPromptTemplateSnapshot> GetRequiredPromptAsync(
string promptType,
string promptVersion,
CancellationToken cancellationToken = default);
}
Loading
Loading