From a8fb26b8c59bfb73a9aa0fc112a77d21efbeb8b2 Mon Sep 17 00:00:00 2001
From: Roman Kondrat'ev <62770895+RomeCore@users.noreply.github.com>
Date: Fri, 24 Apr 2026 22:03:34 +0500
Subject: [PATCH 1/2] Improve metadata creation, add advanced language fallback
system
---
.../TemplateDictionaryAccessor.cs | 5 ++
.../DataAccessors/TemplateObjectAccessor.cs | 5 ++
src/LLTSharp/ITemplate.cs | 7 +-
src/LLTSharp/ITemplateParser.cs | 6 +-
src/LLTSharp/LLTParser.cs | 48 +++++--------
.../HierarchicalLanguageFallbackScheme.cs | 71 +++++++++++++++++++
.../Locale/MajorLanguageFallbackScheme.cs | 2 +-
.../Factories/LanguageMetadataFactory.cs | 21 ++++++
.../TargetModelFamilyMetadataFactory.cs | 18 +++++
.../Factories/TargetModelMetadataFactory.cs | 18 +++++
.../Factories/VersionMetadataFactory.cs | 19 +++++
.../LanguageMetadataFallbackScheme.cs | 3 +-
src/LLTSharp/Metadata/MetadataFactory.cs | 18 +++++
.../Metadata/{ => Types}/LanguageMetadata.cs | 3 +-
.../{ => Types}/TargetModelFamilyMetadata.cs | 4 +-
.../{ => Types}/TargetModelMetadata.cs | 4 +-
.../Metadata/{ => Types}/VersionMetadata.cs | 15 +++-
src/LLTSharp/TemplateContextAccessor.cs | 18 +++--
src/LLTSharp/TemplateDataAccessor.cs | 7 ++
src/LLTSharp/TemplateLibrary.cs | 42 ++++++++---
.../BasicTemplateParsingTests.cs | 6 +-
tests/LLTSharp.Tests/TemplateLibraryTests.cs | 20 ++++--
22 files changed, 283 insertions(+), 77 deletions(-)
create mode 100644 src/LLTSharp/Locale/HierarchicalLanguageFallbackScheme.cs
create mode 100644 src/LLTSharp/Metadata/Factories/LanguageMetadataFactory.cs
create mode 100644 src/LLTSharp/Metadata/Factories/TargetModelFamilyMetadataFactory.cs
create mode 100644 src/LLTSharp/Metadata/Factories/TargetModelMetadataFactory.cs
create mode 100644 src/LLTSharp/Metadata/Factories/VersionMetadataFactory.cs
rename src/LLTSharp/Metadata/{ => FallbackSchemes}/LanguageMetadataFallbackScheme.cs (94%)
create mode 100644 src/LLTSharp/Metadata/MetadataFactory.cs
rename src/LLTSharp/Metadata/{ => Types}/LanguageMetadata.cs (96%)
rename src/LLTSharp/Metadata/{ => Types}/TargetModelFamilyMetadata.cs (94%)
rename src/LLTSharp/Metadata/{ => Types}/TargetModelMetadata.cs (94%)
rename src/LLTSharp/Metadata/{ => Types}/VersionMetadata.cs (65%)
diff --git a/src/LLTSharp/DataAccessors/TemplateDictionaryAccessor.cs b/src/LLTSharp/DataAccessors/TemplateDictionaryAccessor.cs
index 0e5124a..418bd41 100644
--- a/src/LLTSharp/DataAccessors/TemplateDictionaryAccessor.cs
+++ b/src/LLTSharp/DataAccessors/TemplateDictionaryAccessor.cs
@@ -46,6 +46,11 @@ public override TemplateDataAccessor Index(TemplateDataAccessor index)
return Property(index.ToString());
}
+ public override bool HasProperty(string name)
+ {
+ return _dictionary.ContainsKey(name);
+ }
+
public override TemplateDataAccessor Property(string key)
{
if (_dictionary.TryGetValue(key, out var accessor))
diff --git a/src/LLTSharp/DataAccessors/TemplateObjectAccessor.cs b/src/LLTSharp/DataAccessors/TemplateObjectAccessor.cs
index 3a06b6f..94f0c8f 100644
--- a/src/LLTSharp/DataAccessors/TemplateObjectAccessor.cs
+++ b/src/LLTSharp/DataAccessors/TemplateObjectAccessor.cs
@@ -54,6 +54,11 @@ public TemplateObjectAccessor(object target, DataAccessorCreationOptions options
return accessors;
}
+ public override bool HasProperty(string name)
+ {
+ return _propertyAccessors.ContainsKey(name);
+ }
+
public override TemplateDataAccessor Property(string key)
{
if (_propertyAccessors.TryGetValue(key, out var accessor))
diff --git a/src/LLTSharp/ITemplate.cs b/src/LLTSharp/ITemplate.cs
index d0ad4e5..60acee6 100644
--- a/src/LLTSharp/ITemplate.cs
+++ b/src/LLTSharp/ITemplate.cs
@@ -6,13 +6,8 @@ namespace LLTSharp
///
/// Interface for a template that can be used to generate contents.
///
- public interface ITemplate
+ public interface ITemplate : IMetadataProvider
{
- ///
- /// Gets the metadata associated with this template.
- ///
- public IMetadataCollection Metadata { get; }
-
///
/// Renders the template with the given context.
///
diff --git a/src/LLTSharp/ITemplateParser.cs b/src/LLTSharp/ITemplateParser.cs
index f26c3c0..2e74444 100644
--- a/src/LLTSharp/ITemplateParser.cs
+++ b/src/LLTSharp/ITemplateParser.cs
@@ -1,4 +1,5 @@
-using System;
+using LLTSharp.Metadata;
+using System;
using System.Collections.Generic;
using System.Text;
@@ -13,7 +14,8 @@ public interface ITemplateParser
/// Parses a prompt template string into a objects.
///
/// The prompt template string to parse.
+ /// Optional metadata factories to use for parsing.
/// A collection of objects representing the parsed templates.
- IEnumerable Parse(string templateString);
+ IEnumerable Parse(string templateString, IEnumerable? metadataFactories = null);
}
}
\ No newline at end of file
diff --git a/src/LLTSharp/LLTParser.cs b/src/LLTSharp/LLTParser.cs
index cbd76f5..435c7fc 100644
--- a/src/LLTSharp/LLTParser.cs
+++ b/src/LLTSharp/LLTParser.cs
@@ -20,6 +20,7 @@ public class LLTParser : ITemplateParser
private class LLTParsingContext
{
public TemplateLibrary LocalLibrary { get; set; }
+ public IEnumerable MetadataFactories { get; set; }
}
private static readonly Parser _parser;
@@ -697,30 +698,23 @@ private static void DeclareMainRules(ParserBuilder builder)
.Rule("constant_object")
.Transform(v =>
{
+ var ctx = v.GetParsingParameter();
+ var factories = ctx.MetadataFactories;
+
var obj = v.GetValue(2);
var metadata = new List();
var additionalMetadata = new AdditionalMetadata();
foreach (var pair in obj.Dictionary)
{
- switch (pair.Key)
- {
- case "lang":
- metadata.Add(new LanguageMetadata(new Locale.LanguageCode(pair.Value.ToString())));
- break;
- case "model":
- metadata.Add(new TargetModelMetadata(pair.Value.ToString()));
- break;
- case "model_family":
- metadata.Add(new TargetModelFamilyMetadata(pair.Value.ToString()));
+ IMetadata? result = null;
+ foreach (var factory in factories)
+ if (factory.TryCreateMetadata(pair.Key, pair.Value, out result))
break;
- case "version":
- metadata.Add(new VersionMetadata((int)((double)pair.Value.GetValue())));
- break;
- default:
- additionalMetadata.Set(pair.Key, pair.Value.GetValue());
- break;
- }
+ if (result != null)
+ metadata.Add(result);
+ else
+ additionalMetadata.Set(pair.Key, pair.Value.GetValue());
}
if (additionalMetadata.Count > 0)
@@ -770,21 +764,13 @@ static LLTParser()
_parser = builder.Build();
}
- public ParsedRuleResultBase ParseAST(string templateString)
+ public IEnumerable Parse(string templateString, IEnumerable? metadataFactories = null)
{
- var ctx = new LLTParsingContext { LocalLibrary = new TemplateLibrary() };
- return _parser.Parse(templateString, ctx);
- }
-
- public ParsedRuleResultBase ParseOptimizedAST(string templateString)
- {
- var ctx = new LLTParsingContext { LocalLibrary = new TemplateLibrary() };
- return _parser.Parse(templateString, ctx).Optimized(ParseTreeOptimization.Default);
- }
-
- public IEnumerable Parse(string templateString)
- {
- var ctx = new LLTParsingContext { LocalLibrary = new TemplateLibrary() };
+ var ctx = new LLTParsingContext
+ {
+ LocalLibrary = new TemplateLibrary(),
+ MetadataFactories = metadataFactories?.ToList() ?? Enumerable.Empty()
+ };
return _parser.Parse(templateString, ctx).GetValue>();
}
}
diff --git a/src/LLTSharp/Locale/HierarchicalLanguageFallbackScheme.cs b/src/LLTSharp/Locale/HierarchicalLanguageFallbackScheme.cs
new file mode 100644
index 0000000..bdf8e79
--- /dev/null
+++ b/src/LLTSharp/Locale/HierarchicalLanguageFallbackScheme.cs
@@ -0,0 +1,71 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace LLTSharp.Locale
+{
+ ///
+ /// Provides a hierarchical language fallback scheme that walks up the language tag hierarchy (e.g., en-US → en)
+ /// and then tries any available sibling variant of the same language root before falling back to a default or the first available language.
+ ///
+ public class HierarchicalLanguageFallbackScheme : ILanguageFallbackScheme
+ {
+ // Да, да, это нейрослоп
+
+ private readonly LanguageCode? _defaultLanguage;
+
+ ///
+ /// Creates a new instance of the scheme.
+ ///
+ ///
+ /// Optional default language code to be used if the target language and none of its linguistic relatives are available.
+ /// If , the first available language from the collection will be returned as a last resort.
+ ///
+ public HierarchicalLanguageFallbackScheme(LanguageCode? defaultLanguage = null)
+ {
+ _defaultLanguage = defaultLanguage;
+ }
+
+ public LanguageCode GetFallbackLanguage(LanguageCode targetLanguage, IEnumerable availableLanguages)
+ {
+ if (availableLanguages == null)
+ throw new ArgumentNullException(nameof(availableLanguages));
+
+ // Materialise to a list to avoid multiple enumeration.
+ List availableList = availableLanguages as List ?? availableLanguages.ToList();
+
+ if (availableList.Count == 0)
+ throw new ArgumentException("Available languages collection is empty.", nameof(availableLanguages));
+
+ // 1. Exact match is already the best choice.
+ if (availableList.Any(l => l == targetLanguage))
+ return targetLanguage;
+
+ // 2. Walk up the parent chain: e.g., zh-Hans-CN → zh-Hans → zh
+ LanguageCode current = targetLanguage;
+ while (true)
+ {
+ LanguageCode parent = current.GetSuperLanguage();
+ if (parent == current)
+ break;
+
+ if (availableList.Any(l => l == parent))
+ return parent;
+
+ current = parent;
+ }
+
+ // 3. Look for any sibling belonging to the same root language (e.g., fr-FR when fr-CA was requested)
+ var sibling = availableList.FirstOrDefault(l => l.IsSubLanguageOf(current));
+ if (sibling.FullCode != null)
+ return sibling;
+
+ // 4. Fall back to the explicitly configured default language (if available).
+ if (_defaultLanguage.HasValue && availableList.Any(l => l == _defaultLanguage.Value))
+ return _defaultLanguage.Value;
+
+ // 5. Ultimate fallback – return the first language from the available set.
+ return availableList[0];
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/LLTSharp/Locale/MajorLanguageFallbackScheme.cs b/src/LLTSharp/Locale/MajorLanguageFallbackScheme.cs
index b3bb6c2..2628573 100644
--- a/src/LLTSharp/Locale/MajorLanguageFallbackScheme.cs
+++ b/src/LLTSharp/Locale/MajorLanguageFallbackScheme.cs
@@ -22,7 +22,7 @@ public LanguageCode GetFallbackLanguage(LanguageCode targetLanguage, IEnumerable
var hashSet = new HashSet(availableLanguages);
hashSet.IntersectWith(LanguageGroup.MajorWorldLanguages);
- if (hashSet.Any())
+ if (hashSet.Count > 0)
return hashSet.First();
return availableLanguages.First();
diff --git a/src/LLTSharp/Metadata/Factories/LanguageMetadataFactory.cs b/src/LLTSharp/Metadata/Factories/LanguageMetadataFactory.cs
new file mode 100644
index 0000000..65370e0
--- /dev/null
+++ b/src/LLTSharp/Metadata/Factories/LanguageMetadataFactory.cs
@@ -0,0 +1,21 @@
+using LLTSharp.Metadata.Types;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace LLTSharp.Metadata.Factories
+{
+ public class LanguageMetadataFactory : MetadataFactory
+ {
+ public override bool TryCreateMetadata(string key, TemplateDataAccessor value, out IMetadata metadata)
+ {
+ if (key == "lang")
+ {
+ metadata = new LanguageMetadata(value.ToString());
+ return true;
+ }
+ metadata = null;
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/LLTSharp/Metadata/Factories/TargetModelFamilyMetadataFactory.cs b/src/LLTSharp/Metadata/Factories/TargetModelFamilyMetadataFactory.cs
new file mode 100644
index 0000000..cde1c9a
--- /dev/null
+++ b/src/LLTSharp/Metadata/Factories/TargetModelFamilyMetadataFactory.cs
@@ -0,0 +1,18 @@
+using LLTSharp.Metadata.Types;
+
+namespace LLTSharp.Metadata.Factories
+{
+ public class TargetModelFamilyMetadataFactory : MetadataFactory
+ {
+ public override bool TryCreateMetadata(string key, TemplateDataAccessor value, out IMetadata metadata)
+ {
+ if (key == "model_family")
+ {
+ metadata = new TargetModelFamilyMetadata(value.ToString());
+ return true;
+ }
+ metadata = null;
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/LLTSharp/Metadata/Factories/TargetModelMetadataFactory.cs b/src/LLTSharp/Metadata/Factories/TargetModelMetadataFactory.cs
new file mode 100644
index 0000000..f8111fb
--- /dev/null
+++ b/src/LLTSharp/Metadata/Factories/TargetModelMetadataFactory.cs
@@ -0,0 +1,18 @@
+using LLTSharp.Metadata.Types;
+
+namespace LLTSharp.Metadata.Factories
+{
+ public class TargetModelMetadataFactory : MetadataFactory
+ {
+ public override bool TryCreateMetadata(string key, TemplateDataAccessor value, out IMetadata metadata)
+ {
+ if (key == "model")
+ {
+ metadata = new TargetModelMetadata(value.ToString());
+ return true;
+ }
+ metadata = null;
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/LLTSharp/Metadata/Factories/VersionMetadataFactory.cs b/src/LLTSharp/Metadata/Factories/VersionMetadataFactory.cs
new file mode 100644
index 0000000..0b75535
--- /dev/null
+++ b/src/LLTSharp/Metadata/Factories/VersionMetadataFactory.cs
@@ -0,0 +1,19 @@
+using LLTSharp.Metadata.Types;
+using System;
+
+namespace LLTSharp.Metadata.Factories
+{
+ public class VersionMetadataFactory : MetadataFactory
+ {
+ public override bool TryCreateMetadata(string key, TemplateDataAccessor value, out IMetadata metadata)
+ {
+ if (key == "version")
+ {
+ metadata = new VersionMetadata(Version.Parse(value.ToString()));
+ return true;
+ }
+ metadata = null;
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/LLTSharp/Metadata/LanguageMetadataFallbackScheme.cs b/src/LLTSharp/Metadata/FallbackSchemes/LanguageMetadataFallbackScheme.cs
similarity index 94%
rename from src/LLTSharp/Metadata/LanguageMetadataFallbackScheme.cs
rename to src/LLTSharp/Metadata/FallbackSchemes/LanguageMetadataFallbackScheme.cs
index a09af0c..79ad261 100644
--- a/src/LLTSharp/Metadata/LanguageMetadataFallbackScheme.cs
+++ b/src/LLTSharp/Metadata/FallbackSchemes/LanguageMetadataFallbackScheme.cs
@@ -3,8 +3,9 @@
using System.Linq;
using System.Text;
using LLTSharp.Locale;
+using LLTSharp.Metadata.Types;
-namespace LLTSharp.Metadata
+namespace LLTSharp.Metadata.FallbackSchemes
{
///
/// A fallback scheme for handling language metadata when no specific information is available.
diff --git a/src/LLTSharp/Metadata/MetadataFactory.cs b/src/LLTSharp/Metadata/MetadataFactory.cs
new file mode 100644
index 0000000..9455182
--- /dev/null
+++ b/src/LLTSharp/Metadata/MetadataFactory.cs
@@ -0,0 +1,18 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace LLTSharp.Metadata
+{
+ public abstract class MetadataFactory
+ {
+ ///
+ /// Tries to create a new instance of based on the provided key and value.
+ ///
+ /// The key associated with the metadata.
+ /// The value associated with the metadata.
+ /// When this method returns, contains the newly created instance of if successful; otherwise, null. This parameter is passed uninitialized.
+ /// true if a new instance of was created successfully; otherwise, false.
+ public abstract bool TryCreateMetadata(string key, TemplateDataAccessor value, out IMetadata metadata);
+ }
+}
\ No newline at end of file
diff --git a/src/LLTSharp/Metadata/LanguageMetadata.cs b/src/LLTSharp/Metadata/Types/LanguageMetadata.cs
similarity index 96%
rename from src/LLTSharp/Metadata/LanguageMetadata.cs
rename to src/LLTSharp/Metadata/Types/LanguageMetadata.cs
index a5568d2..f765822 100644
--- a/src/LLTSharp/Metadata/LanguageMetadata.cs
+++ b/src/LLTSharp/Metadata/Types/LanguageMetadata.cs
@@ -2,9 +2,8 @@
using System.Collections.Generic;
using System.Text;
using LLTSharp.Locale;
-using LLTSharp.Metadata;
-namespace LLTSharp.Metadata
+namespace LLTSharp.Metadata.Types
{
///
/// The metadata for language-related information.
diff --git a/src/LLTSharp/Metadata/TargetModelFamilyMetadata.cs b/src/LLTSharp/Metadata/Types/TargetModelFamilyMetadata.cs
similarity index 94%
rename from src/LLTSharp/Metadata/TargetModelFamilyMetadata.cs
rename to src/LLTSharp/Metadata/Types/TargetModelFamilyMetadata.cs
index d3e094b..63fe221 100644
--- a/src/LLTSharp/Metadata/TargetModelFamilyMetadata.cs
+++ b/src/LLTSharp/Metadata/Types/TargetModelFamilyMetadata.cs
@@ -1,6 +1,4 @@
-using LLTSharp.Metadata;
-
-namespace LLTSharp.Metadata
+namespace LLTSharp.Metadata.Types
{
///
/// The metadata for target model family-related information.
diff --git a/src/LLTSharp/Metadata/TargetModelMetadata.cs b/src/LLTSharp/Metadata/Types/TargetModelMetadata.cs
similarity index 94%
rename from src/LLTSharp/Metadata/TargetModelMetadata.cs
rename to src/LLTSharp/Metadata/Types/TargetModelMetadata.cs
index 670e45b..6a8f769 100644
--- a/src/LLTSharp/Metadata/TargetModelMetadata.cs
+++ b/src/LLTSharp/Metadata/Types/TargetModelMetadata.cs
@@ -1,6 +1,4 @@
-using LLTSharp.Metadata;
-
-namespace LLTSharp.Metadata
+namespace LLTSharp.Metadata.Types
{
///
/// The metadata for target model-related information.
diff --git a/src/LLTSharp/Metadata/VersionMetadata.cs b/src/LLTSharp/Metadata/Types/VersionMetadata.cs
similarity index 65%
rename from src/LLTSharp/Metadata/VersionMetadata.cs
rename to src/LLTSharp/Metadata/Types/VersionMetadata.cs
index 8db8cab..e6145b0 100644
--- a/src/LLTSharp/Metadata/VersionMetadata.cs
+++ b/src/LLTSharp/Metadata/Types/VersionMetadata.cs
@@ -2,7 +2,7 @@
using System.Collections.Generic;
using System.Text;
-namespace LLTSharp.Metadata
+namespace LLTSharp.Metadata.Types
{
///
/// Represents the version metadata.
@@ -12,7 +12,7 @@ public class VersionMetadata : IMetadata
///
/// Gets the version code associated with this metadata.
///
- public int Version { get; }
+ public Version Version { get; }
///
/// Initializes a new instance of the class with the specified version code.
@@ -20,7 +20,16 @@ public class VersionMetadata : IMetadata
/// The version code associated with this metadata.
public VersionMetadata(int version)
{
- Version = version;
+ Version = new Version(version, 0);
+ }
+
+ ///
+ /// Initializes a new instance of the class with the specified version code.
+ ///
+ /// The version code associated with this metadata.
+ public VersionMetadata(Version version)
+ {
+ Version = version ?? throw new ArgumentNullException(nameof(version));
}
public override string ToString()
diff --git a/src/LLTSharp/TemplateContextAccessor.cs b/src/LLTSharp/TemplateContextAccessor.cs
index dc9110c..093aa01 100644
--- a/src/LLTSharp/TemplateContextAccessor.cs
+++ b/src/LLTSharp/TemplateContextAccessor.cs
@@ -209,16 +209,20 @@ public override string ToString(string? format = null)
/// Renders a template with the specified identifier and optional new context.
///
///
- /// Tries to find the specified template in the local library. If not found, tries to find it in the shared library.
+ /// Tries to find the specified template in the context variables, then local library. If not found, tries to find it in the shared library.
///
/// The identifier of the template to render.
/// The new context to use for rendering the template. If null, uses the current context.
/// The rendered template as a string.
public string RenderTemplate(string identifier, TemplateDataAccessor? newContext)
{
- var template = Library.TryRetrieve(identifier);
+ ITemplate? template = null;
+ if (Context.HasProperty(identifier) && Context.Property(identifier).GetValue() is ITemplate ctxValueTemplate)
+ template = ctxValueTemplate;
if (template == null)
- template = TemplateLibrary.Shared.TryRetrieve(identifier); // try to get from shared library
+ template = Library.TryRetrieve(identifier);
+ if (template == null)
+ template = TemplateLibrary.Shared.TryRetrieve(identifier);
if (template == null)
throw new TemplateRuntimeException($"Template '{identifier}' not found.");
@@ -233,14 +237,18 @@ public string RenderTemplate(string identifier, TemplateDataAccessor? newContext
/// Renders a messages template with the specified identifier and optional new context.
///
///
- /// Tries to find the specified template in the local library. If not found, tries to find it in the shared library.
+ /// Tries to find the specified template in the context variables, then local library. If not found, tries to find it in the shared library.
///
/// The identifier of the messages template to render.
/// The new context to use for rendering the template. If null, uses the current context.
/// The rendered template as a collection of messages.
public IEnumerable RenderMessagesTemplate(string identifier, TemplateDataAccessor? newContext)
{
- var template = Library.TryRetrieve(identifier);
+ ITemplate? template = null;
+ if (Context.HasProperty(identifier) && Context.Property(identifier).GetValue() is ITemplate ctxValueTemplate)
+ template = ctxValueTemplate;
+ if (template == null)
+ template = Library.TryRetrieve(identifier);
if (template == null)
template = TemplateLibrary.Shared.TryRetrieve(identifier);
if (template == null)
diff --git a/src/LLTSharp/TemplateDataAccessor.cs b/src/LLTSharp/TemplateDataAccessor.cs
index d8ddf91..f0e7afd 100644
--- a/src/LLTSharp/TemplateDataAccessor.cs
+++ b/src/LLTSharp/TemplateDataAccessor.cs
@@ -18,6 +18,13 @@ public abstract class TemplateDataAccessor : IDisposable
///
public virtual int Length => 0;
+ ///
+ /// Determines if a property exists in the data.
+ ///
+ /// The name of the property to check.
+ /// True if the property exists; otherwise, false.
+ public virtual bool HasProperty(string name) => false;
+
///
/// Gets the template data property associated with the specified property name.
///
diff --git a/src/LLTSharp/TemplateLibrary.cs b/src/LLTSharp/TemplateLibrary.cs
index eb921ea..57e5fae 100644
--- a/src/LLTSharp/TemplateLibrary.cs
+++ b/src/LLTSharp/TemplateLibrary.cs
@@ -9,6 +9,9 @@
using System.Threading.Tasks;
using LLTSharp.Locale;
using LLTSharp.Metadata;
+using LLTSharp.Metadata.Factories;
+using LLTSharp.Metadata.FallbackSchemes;
+using LLTSharp.Metadata.Types;
using RCParsing;
namespace LLTSharp
@@ -28,7 +31,7 @@ static TemplateLibrary()
///
/// Registers a template parser for the specified language.
///
- /// The language code of the template language.
+ /// The language code of the template language, e.g., "llt".
/// The template parser to register.
/// Thrown when a parser is already registered for the specified language code.
public static void RegisterParser(string languageCode, ITemplateParser parser)
@@ -46,14 +49,35 @@ public static void RegisterParser(string languageCode, ITemplateParser parser)
private readonly Dictionary _fallbackSchemes = new();
private readonly Dictionary> _fallbackMetadatas = new();
+ private readonly List _metadataFactories = new()
+ {
+ new VersionMetadataFactory(),
+ new LanguageMetadataFactory(),
+ new TargetModelMetadataFactory(),
+ new TargetModelFamilyMetadataFactory()
+ };
- private readonly object _lockObject = new object();
+ private readonly object _lockObject = new();
///
/// Gets the shared template library instance.
///
public static TemplateLibrary Shared { get; } = new();
+ ///
+ /// Gets the collection of metadata factories used by this template library.
+ /// These are used to construct metadata objects inside metadata collections.
+ ///
+ /// The default metadata factories are:
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public List MetadataFactories => _metadataFactories;
+
///
/// Initializes a new instance of the class.
///
@@ -276,7 +300,7 @@ public void ImportFromString(string templateContents, string languageCode = "llt
if (!_templateParsers.TryGetValue(languageCode, out var parser))
throw new ArgumentException($"No parser registered for language: '{languageCode}'.");
- var templates = parser.Parse(templateContents);
+ var templates = parser.Parse(templateContents, MetadataFactories);
foreach (var template in templates)
Add(template);
}
@@ -294,7 +318,7 @@ public void ImportFromReader(TextReader reader, string languageCode = "llt")
throw new ArgumentException($"No parser registered for language: '{languageCode}'.");
var templateContents = reader?.ReadToEnd() ?? throw new ArgumentNullException(nameof(reader));
- var templates = parser.Parse(templateContents);
+ var templates = parser.Parse(templateContents, MetadataFactories);
foreach (var template in templates)
Add(template);
}
@@ -313,7 +337,7 @@ public void ImportFromStream(Stream stream, string languageCode = "llt")
using var reader = new StreamReader(stream);
var templateContents = reader.ReadToEnd();
- var templates = parser.Parse(templateContents);
+ var templates = parser.Parse(templateContents, MetadataFactories);
foreach (var template in templates)
Add(template);
}
@@ -340,7 +364,7 @@ public void ImportFromFile(string filename)
throw new ArgumentException($"No parser registered for language: '{languageCode}'.");
var templateContents = File.ReadAllText(filename);
- var templates = parser.Parse(templateContents);
+ var templates = parser.Parse(templateContents, MetadataFactories);
AddRange(templates);
}
@@ -357,7 +381,7 @@ public void ImportFromFile(string filename, string languageCode)
throw new ArgumentException($"No parser registered for language: '{languageCode}'.");
var templateContents = File.ReadAllText(filename);
- var templates = parser.Parse(templateContents);
+ var templates = parser.Parse(templateContents, MetadataFactories);
AddRange(templates);
}
@@ -393,7 +417,7 @@ public IEnumerable ImportFromFolder(string folderPath, SearchO
try
{
- var templates = parser.Parse(File.ReadAllText(file));
+ var templates = parser.Parse(File.ReadAllText(file), MetadataFactories);
AddRange(templates);
}
catch (ParsingException ex)
@@ -438,7 +462,7 @@ public List ImportFromAssembly(Assembly assembly, string? fold
var stream = assembly.GetManifestResourceStream(resource);
using var reader = new StreamReader(stream);
var templateContents = reader.ReadToEnd();
- var templates = parser.Parse(templateContents);
+ var templates = parser.Parse(templateContents, MetadataFactories);
AddRange(templates);
}
catch (ParsingException ex)
diff --git a/tests/LLTSharp.Tests/BasicTemplateParsingTests.cs b/tests/LLTSharp.Tests/BasicTemplateParsingTests.cs
index 5c10d58..fc90775 100644
--- a/tests/LLTSharp.Tests/BasicTemplateParsingTests.cs
+++ b/tests/LLTSharp.Tests/BasicTemplateParsingTests.cs
@@ -174,11 +174,7 @@ public void MultipleInlineVariables()
""";
var parser = new LLTParser();
- var ast = parser.ParseAST(templateStr);
- var ctx = ast.Context;
- var value = ast.GetValue>();
-
- output.WriteLine(ctx.walkTrace.Render(400));
+ parser.Parse(templateStr);
}
}
}
\ No newline at end of file
diff --git a/tests/LLTSharp.Tests/TemplateLibraryTests.cs b/tests/LLTSharp.Tests/TemplateLibraryTests.cs
index de9b1b1..3f80c83 100644
--- a/tests/LLTSharp.Tests/TemplateLibraryTests.cs
+++ b/tests/LLTSharp.Tests/TemplateLibraryTests.cs
@@ -3,7 +3,9 @@
using System.Linq;
using System.Text;
using System.Threading.Tasks;
+using LLTSharp.Locale;
using LLTSharp.Metadata;
+using LLTSharp.Metadata.Types;
namespace LLTSharp.Tests
{
@@ -33,7 +35,7 @@ @template sample_template_another
{
@metadata
{
- lang: 'en_US',
+ lang: 'en-US',
model: 'gpt-3.5-turbo'
}
This is another template for GPT-3.5-Turbo model.
@@ -43,7 +45,7 @@ @template sample_template
{
@metadata
{
- lang: 'en_US',
+ lang: 'en-US',
model: 'gpt-3.5-turbo'
}
This is template for GPT-3.5-Turbo model.
@@ -53,22 +55,22 @@ @template sample_template
{
@metadata
{
- lang: 'en_US',
+ lang: 'en-US',
model: 'gpt-4'
}
This is template for GPT-4 model.
}
""");
- var template = lib.Retrieve("sample_template", new LanguageMetadata("en_US"), new TargetModelMetadata("gpt-3.5-turbo"));
+ var template = lib.Retrieve("sample_template", new LanguageMetadata("en-US"), new TargetModelMetadata("gpt-3.5-turbo"));
var rendered = template.Render();
Assert.Equal("This is template for GPT-3.5-Turbo model.", rendered);
- template = lib.Retrieve("sample_template", new LanguageMetadata("en_US"), new TargetModelMetadata("gpt-4"));
+ template = lib.Retrieve("sample_template", new LanguageMetadata("en-US"), new TargetModelMetadata("gpt-4"));
rendered = template.Render();
Assert.Equal("This is template for GPT-4 model.", rendered);
- Assert.Throws(() => lib.Retrieve("sample_template", new LanguageMetadata("fr_FR")));
+ Assert.Throws(() => lib.Retrieve("sample_template", new LanguageMetadata("fr-FR")));
}
[Fact]
@@ -76,6 +78,8 @@ public void TemplatePriorityByMetadataSpecificity()
{
var lib = new TemplateLibrary();
+ lib.SetLanguageFallbackScheme(new HierarchicalLanguageFallbackScheme());
+
lib.ImportFromString(
"""
@template greeting
@@ -112,6 +116,10 @@ @template greeting
var lessSpecific = lib.Retrieve("greeting", new LanguageMetadata("en"));
Assert.Equal("Hello!", lessSpecific.Render().ToString());
+ // Fallback to the most specific language-specific template
+ var lessSpecific2 = lib.RetrieveWithFallback("greeting", new LanguageMetadata("en-US"));
+ Assert.Equal("Hello!", lessSpecific2.Render().ToString());
+
// Most general template.
var generic = lib.Retrieve("greeting");
Assert.Equal("Generic greeting", generic.Render().ToString());
From 94633d6593d4b5ce194df8a652cc58b0a9544ce6 Mon Sep 17 00:00:00 2001
From: Roman Kondrat'ev <62770895+RomeCore@users.noreply.github.com>
Date: Fri, 24 Apr 2026 22:08:50 +0500
Subject: [PATCH 2/2] Change version to 1.2.3
---
src/LLTSharp/LLTSharp.csproj | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/LLTSharp/LLTSharp.csproj b/src/LLTSharp/LLTSharp.csproj
index 2bee7a1..85fdda4 100644
--- a/src/LLTSharp/LLTSharp.csproj
+++ b/src/LLTSharp/LLTSharp.csproj
@@ -8,7 +8,7 @@
LLTSharp
- 1.2.2
+ 1.2.3
Roman K.
RomeCore
LLTSharp