Merge pull request #1173 from iceljc/master

fix template rendering by states
This commit is contained in:
iceljc 2025-09-25 16:12:04 -05:00 committed by GitHub
commit f4b669d229
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 122 additions and 100 deletions

View file

@ -29,19 +29,18 @@ public interface IAgentService
/// <returns></returns>
Task InheritAgent(Agent agent);
string RenderInstruction(Agent agent);
string RenderInstruction(Agent agent, Dictionary<string, object>? renderData = null);
string RenderTemplate(Agent agent, string templateName);
string RenderTemplate(Agent agent, string templateName, Dictionary<string, object>? renderData = null);
bool RenderFunction(Agent agent, FunctionDef def);
bool RenderFunction(Agent agent, FunctionDef def, Dictionary<string, object>? renderData = null);
FunctionParametersDef? RenderFunctionProperty(Agent agent, FunctionDef def);
FunctionParametersDef? RenderFunctionProperty(Agent agent, FunctionDef def, Dictionary<string, object>? renderData = null);
(string, IEnumerable<FunctionDef>) PrepareInstructionAndFunctions(Agent agent, StringComparer? comparer = null);
IEnumerable<FunctionDef> FilterFunctions(string instruction, Agent agent, StringComparer? comparer = null);
IEnumerable<FunctionDef> FilterFunctions(string instruction, IEnumerable<FunctionDef> functions, StringComparer? comparer = null);
(string, IEnumerable<FunctionDef>) PrepareInstructionAndFunctions(Agent agent, Dictionary<string, object>? renderData = null, StringComparer? comparer = null);
bool RenderVisibility(string? visibilityExpression, Dictionary<string, object> dict);
Dictionary<string, object> CollectRenderData(Agent agent);
/// <summary>

View file

@ -63,17 +63,18 @@ public class BasicAgentHook : AgentHookBase
var agentService = _services.GetRequiredService<IAgentService>();
var innerUtilities = utilities!.Where(x => !string.IsNullOrEmpty(x.Name) && !x.Disabled).ToList();
var renderDict = agentService.CollectRenderData(agent);
var functionNames = new List<string>();
var templateNames = new List<string>();
foreach (var utility in innerUtilities)
{
var isVisible = agentService.RenderVisibility(utility.VisibilityExpression, agent.TemplateDict);
var isVisible = agentService.RenderVisibility(utility.VisibilityExpression, renderDict);
if (!isVisible || utility.Items.IsNullOrEmpty()) continue;
foreach (var item in utility.Items)
{
isVisible = agentService.RenderVisibility(item.VisibilityExpression, agent.TemplateDict);
isVisible = agentService.RenderVisibility(item.VisibilityExpression, renderDict);
if (!isVisible) continue;
if (item.FunctionName?.StartsWith(UTIL_PREFIX) == true)

View file

@ -7,7 +7,7 @@ namespace BotSharp.Core.Agents.Services;
public partial class AgentService
{
public string RenderInstruction(Agent agent)
public string RenderInstruction(Agent agent, Dictionary<string, object>? renderData = null)
{
var render = _services.GetRequiredService<ITemplateRender>();
var conv = _services.GetRequiredService<IConversationService>();
@ -18,19 +18,16 @@ public partial class AgentService
instructions.AddRange(secondaryInstructions);
// update states
var renderDict = new Dictionary<string, object>(agent.TemplateDict);
foreach (var t in conv.States.GetStates())
{
renderDict[t.Key] = t.Value;
}
var renderDict = renderData != null ? new Dictionary<string, object>(renderData ?? []) : CollectRenderData(agent);
renderDict[TemplateRenderConstant.RENDER_AGENT] = agent;
var res = render.Render(string.Join("\r\n", instructions), renderDict);
return res;
}
public bool RenderFunction(Agent agent, FunctionDef def)
public bool RenderFunction(Agent agent, FunctionDef def, Dictionary<string, object>? renderData = null)
{
var renderDict = renderData ?? agent.TemplateDict;
var isRender = true;
var channels = def.Channels;
@ -48,18 +45,19 @@ public partial class AgentService
if (!string.IsNullOrWhiteSpace(def.VisibilityExpression))
{
isRender = RenderVisibility(def.VisibilityExpression, agent.TemplateDict);
isRender = RenderVisibility(def.VisibilityExpression, renderDict);
}
return isRender;
}
public FunctionParametersDef? RenderFunctionProperty(Agent agent, FunctionDef def)
public FunctionParametersDef? RenderFunctionProperty(Agent agent, FunctionDef def, Dictionary<string, object>? renderData = null)
{
var parameterDef = def?.Parameters;
var propertyDef = parameterDef?.Properties;
if (propertyDef == null) return null;
var renderDict = renderData ?? agent.TemplateDict;
var visibleExpress = "visibility_expression";
var root = propertyDef.RootElement;
var iterator = root.EnumerateObject();
@ -73,7 +71,7 @@ public partial class AgentService
if (node.TryGetProperty(visibleExpress, out var element))
{
var expression = element.GetString();
matched = RenderVisibility(expression, agent.TemplateDict);
matched = RenderVisibility(expression, renderDict);
}
if (matched)
@ -107,37 +105,19 @@ public partial class AgentService
return parameterDef;
}
public (string, IEnumerable<FunctionDef>) PrepareInstructionAndFunctions(Agent agent, StringComparer? comparer = null)
public (string, IEnumerable<FunctionDef>) PrepareInstructionAndFunctions(Agent agent, Dictionary<string, object>? renderData = null, StringComparer ? comparer = null)
{
var text = string.Empty;
if (!string.IsNullOrEmpty(agent.Instruction) || !agent.SecondaryInstructions.IsNullOrEmpty())
{
text = RenderInstruction(agent);
text = RenderInstruction(agent, renderData);
}
var functions = FilterFunctions(text, agent, comparer);
return (text, functions);
}
public IEnumerable<FunctionDef> FilterFunctions(string instruction, Agent agent, StringComparer? comparer = null)
{
var functions = agent.Functions.Concat(agent.SecondaryFunctions ?? []);
if (agent.FuncVisMode.IsEqualTo(AgentFuncVisMode.Auto) && !string.IsNullOrWhiteSpace(instruction))
{
functions = FilterFunctions(instruction, functions, comparer);
}
return functions;
}
public IEnumerable<FunctionDef> FilterFunctions(string instruction, IEnumerable<FunctionDef> functions, StringComparer? comparer = null)
{
comparer = comparer ?? StringComparer.OrdinalIgnoreCase;
var matches = Regex.Matches(instruction, @"\b[A-Za-z0-9_-]+\b");
var words = new HashSet<string>(matches.Select(m => m.Value), comparer);
return functions.Where(x => words.Contains(x.Name, comparer));
}
public string RenderTemplate(Agent agent, string templateName)
public string RenderTemplate(Agent agent, string templateName, Dictionary<string, object>? renderData = null)
{
var conv = _services.GetRequiredService<IConversationService>();
var render = _services.GetRequiredService<ITemplateRender>();
@ -145,14 +125,11 @@ public partial class AgentService
var template = agent.Templates.FirstOrDefault(x => x.Name == templateName)?.Content ?? string.Empty;
// update states
foreach (var t in conv.States.GetStates())
{
agent.TemplateDict[t.Key] = t.Value;
}
var renderDict = renderData != null ? new Dictionary<string, object>(renderData ?? []) : CollectRenderData(agent);
renderDict[TemplateRenderConstant.RENDER_AGENT] = agent;
// render liquid template
agent.TemplateDict[TemplateRenderConstant.RENDER_AGENT] = agent;
var content = render.Render(template, agent.TemplateDict);
var content = render.Render(template, renderDict);
HookEmitter.Emit<IContentGeneratingHook>(_services, async hook => await hook.OnRenderingTemplate(agent, templateName, content),
agent.Id).Wait();
@ -175,4 +152,37 @@ public partial class AgentService
return result.IsEqualTo("visible");
}
public Dictionary<string, object> CollectRenderData(Agent agent)
{
var state = _services.GetRequiredService<IConversationStateService>();
var renderDict = new Dictionary<string, object>(agent?.TemplateDict ?? []);
foreach (var t in state.GetStates())
{
renderDict[t.Key] = t.Value;
}
return renderDict;
}
#region Private methods
private IEnumerable<FunctionDef> FilterFunctions(string instruction, Agent agent, StringComparer? comparer = null)
{
var functions = agent.Functions.Concat(agent.SecondaryFunctions ?? []);
if (agent.FuncVisMode.IsEqualTo(AgentFuncVisMode.Auto) && !string.IsNullOrWhiteSpace(instruction))
{
functions = FilterFunctions(instruction, functions, comparer);
}
return functions;
}
private IEnumerable<FunctionDef> FilterFunctions(string instruction, IEnumerable<FunctionDef> functions, StringComparer? comparer = null)
{
comparer = comparer ?? StringComparer.OrdinalIgnoreCase;
var matches = Regex.Matches(instruction, @"\b[A-Za-z0-9_-]+\b");
var words = new HashSet<string>(matches.Select(m => m.Value), comparer);
return functions.Where(x => words.Contains(x.Name, comparer));
}
#endregion
}

View file

@ -108,7 +108,8 @@ public class ChatCompletionProvider : IChatCompletion
renderedInstructions = [];
// Prepare instruction and functions
var (instruction, functions) = agentService.PrepareInstructionAndFunctions(agent);
var renderData = agentService.CollectRenderData(agent);
var (instruction, functions) = agentService.PrepareInstructionAndFunctions(agent, renderData);
if (!string.IsNullOrWhiteSpace(instruction))
{
renderedInstructions.Add(instruction);

View file

@ -372,7 +372,8 @@ public class ChatCompletionProvider : IChatCompletion
var options = InitChatCompletionOption(agent);
// Prepare instruction and functions
var (instruction, functions) = agentService.PrepareInstructionAndFunctions(agent);
var renderData = agentService.CollectRenderData(agent);
var (instruction, functions) = agentService.PrepareInstructionAndFunctions(agent, renderData);
if (!string.IsNullOrWhiteSpace(instruction))
{
renderedInstructions.Add(instruction);
@ -384,9 +385,12 @@ public class ChatCompletionProvider : IChatCompletion
{
foreach (var function in functions)
{
if (!agentService.RenderFunction(agent, function)) continue;
if (!agentService.RenderFunction(agent, function, renderData))
{
continue;
}
var property = agentService.RenderFunctionProperty(agent, function);
var property = agentService.RenderFunctionProperty(agent, function, renderData);
options.Tools.Add(ChatTool.CreateFunctionTool(
functionName: function.Name,
@ -395,18 +399,6 @@ public class ChatCompletionProvider : IChatCompletion
}
}
foreach (var function in functions)
{
if (!agentService.RenderFunction(agent, function)) continue;
var property = agentService.RenderFunctionProperty(agent, function);
options.Tools.Add(ChatTool.CreateFunctionTool(
functionName: function.Name,
functionDescription: function.Description,
functionParameters: BinaryData.FromObjectAsJson(property)));
}
if (!string.IsNullOrEmpty(agent.Knowledges))
{
messages.Add(new SystemChatMessage(agent.Knowledges));

View file

@ -345,7 +345,8 @@ public class ChatCompletionProvider : IChatCompletion
var options = InitChatCompletionOption(agent);
// Prepare instruction and functions
var (instruction, functions) = agentService.PrepareInstructionAndFunctions(agent);
var renderData = agentService.CollectRenderData(agent);
var (instruction, functions) = agentService.PrepareInstructionAndFunctions(agent, renderData);
if (!string.IsNullOrWhiteSpace(instruction))
{
renderedInstructions.Add(instruction);
@ -357,9 +358,12 @@ public class ChatCompletionProvider : IChatCompletion
{
foreach (var function in functions)
{
if (!agentService.RenderFunction(agent, function)) continue;
if (!agentService.RenderFunction(agent, function, renderData))
{
continue;
}
var property = agentService.RenderFunctionProperty(agent, function);
var property = agentService.RenderFunctionProperty(agent, function, renderData);
options.Tools.Add(ChatTool.CreateFunctionTool(
functionName: function.Name,

View file

@ -198,7 +198,8 @@ public class GeminiChatCompletionProvider : IChatCompletion
var funcPrompts = new List<string>();
// Prepare instruction and functions
var (instruction, functions) = agentService.PrepareInstructionAndFunctions(agent);
var renderData = agentService.CollectRenderData(agent);
var (instruction, functions) = agentService.PrepareInstructionAndFunctions(agent, renderData);
if (!string.IsNullOrWhiteSpace(instruction))
{
renderedInstructions.Add(instruction);
@ -207,9 +208,12 @@ public class GeminiChatCompletionProvider : IChatCompletion
foreach (var function in functions)
{
if (!agentService.RenderFunction(agent, function)) continue;
if (!agentService.RenderFunction(agent, function, renderData))
{
continue;
}
var def = agentService.RenderFunctionProperty(agent, function);
var def = agentService.RenderFunctionProperty(agent, function, renderData);
var props = JsonSerializer.Serialize(def?.Properties);
var parameters = !string.IsNullOrWhiteSpace(props) && props != "{}" ? new Schema()
{

View file

@ -101,7 +101,8 @@ public class PalmChatCompletionProvider : IChatCompletion
var router = routing.Router;
// Prepare instruction and functions
var (prompt, functions) = agentService.PrepareInstructionAndFunctions(agent);
var renderData = agentService.CollectRenderData(agent);
var (prompt, functions) = agentService.PrepareInstructionAndFunctions(agent, renderData);
if (!string.IsNullOrWhiteSpace(prompt))
{
renderedInstructions.Add(prompt);

View file

@ -492,7 +492,8 @@ public class GoogleRealTimeProvider : IRealTimeCompletion
var funcPrompts = new List<string>();
// Prepare instruction and functions
var (instruction, functions) = agentService.PrepareInstructionAndFunctions(agent);
var renderData = agentService.CollectRenderData(agent);
var (instruction, functions) = agentService.PrepareInstructionAndFunctions(agent, renderData);
if (!string.IsNullOrWhiteSpace(instruction))
{
renderedInstructions.Add(instruction);
@ -501,9 +502,12 @@ public class GoogleRealTimeProvider : IRealTimeCompletion
foreach (var function in functions)
{
if (!agentService.RenderFunction(agent, function)) continue;
if (!agentService.RenderFunction(agent, function, renderData))
{
continue;
}
var def = agentService.RenderFunctionProperty(agent, function);
var def = agentService.RenderFunctionProperty(agent, function, renderData);
var props = JsonSerializer.Serialize(def?.Properties);
var parameters = !string.IsNullOrWhiteSpace(props) && props != "{}"
? new Schema()

View file

@ -94,7 +94,8 @@ public class ChatCompletionProvider : IChatCompletion
renderedInstructions = [];
// Prepare instruction and functions
var (instruction, functions) = agentService.PrepareInstructionAndFunctions(agent);
var renderData = agentService.CollectRenderData(agent);
var (instruction, functions) = agentService.PrepareInstructionAndFunctions(agent, renderData);
if (!string.IsNullOrWhiteSpace(instruction))
{
renderedInstructions.Add(instruction);

View file

@ -73,7 +73,8 @@ public sealed class MicrosoftExtensionsAIChatCompletionProvider : IChatCompletio
var agentService = _services.GetRequiredService<IAgentService>();
// Prepare instruction and functions
var (instruction, functions) = agentService.PrepareInstructionAndFunctions(agent);
var renderData = agentService.CollectRenderData(agent);
var (instruction, functions) = agentService.PrepareInstructionAndFunctions(agent, renderData);
if (!string.IsNullOrWhiteSpace(instruction))
{
renderedInstructions.Add(instruction);
@ -82,9 +83,9 @@ public sealed class MicrosoftExtensionsAIChatCompletionProvider : IChatCompletio
foreach (var function in functions)
{
if (agentService.RenderFunction(agent, function))
if (agentService.RenderFunction(agent, function, renderData))
{
var property = agentService.RenderFunctionProperty(agent, function);
var property = agentService.RenderFunctionProperty(agent, function, renderData);
(options.Tools ??= []).Add(new NopAIFunction(function.Name, function.Description, JsonSerializer.SerializeToElement(property)));
}
}

View file

@ -342,7 +342,8 @@ public class ChatCompletionProvider : IChatCompletion
var options = InitChatCompletionOption(agent);
// Prepare instruction and functions
var (instruction, functions) = agentService.PrepareInstructionAndFunctions(agent);
var renderData = agentService.CollectRenderData(agent);
var (instruction, functions) = agentService.PrepareInstructionAndFunctions(agent, renderData);
if (!string.IsNullOrWhiteSpace(instruction))
{
renderedInstructions.Add(instruction);
@ -354,9 +355,12 @@ public class ChatCompletionProvider : IChatCompletion
{
foreach (var function in functions)
{
if (!agentService.RenderFunction(agent, function)) continue;
if (!agentService.RenderFunction(agent, function, renderData))
{
continue;
}
var property = agentService.RenderFunctionProperty(agent, function);
var property = agentService.RenderFunctionProperty(agent, function, renderData);
options.Tools.Add(ChatTool.CreateFunctionTool(
functionName: function.Name,

View file

@ -568,7 +568,8 @@ public class RealTimeCompletionProvider : IRealTimeCompletion
};
// Prepare instruction and functions
var (instruction, functions) = agentService.PrepareInstructionAndFunctions(agent);
var renderData = agentService.CollectRenderData(agent);
var (instruction, functions) = agentService.PrepareInstructionAndFunctions(agent, renderData);
if (!string.IsNullOrWhiteSpace(instruction))
{
messages.Add(new SystemChatMessage(instruction));
@ -576,9 +577,12 @@ public class RealTimeCompletionProvider : IRealTimeCompletion
foreach (var function in functions)
{
if (!agentService.RenderFunction(agent, function)) continue;
if (!agentService.RenderFunction(agent, function, renderData))
{
continue;
}
var property = agentService.RenderFunctionProperty(agent, function);
var property = agentService.RenderFunctionProperty(agent, function, renderData);
options.Tools.Add(ChatTool.CreateFunctionTool(
functionName: function.Name,

View file

@ -235,7 +235,8 @@ public class ChatCompletionProvider : IChatCompletion
renderedInstructions = [];
// Prepare instruction and functions
var (instruction, agentFunctions) = agentService.PrepareInstructionAndFunctions(agent);
var renderData = agentService.CollectRenderData(agent);
var (instruction, agentFunctions) = agentService.PrepareInstructionAndFunctions(agent, renderData);
if (!string.IsNullOrWhiteSpace(instruction))
{
renderedInstructions.Add(instruction);

View file

@ -41,37 +41,27 @@ namespace BotSharp.Plugin.Google.Core
return Task.CompletedTask;
}
public string RenderInstruction(Agent agent)
public string RenderInstruction(Agent agent, Dictionary<string, object>? renderData = null)
{
return "Fake Instruction";
}
public string RenderTemplate(Agent agent, string templateName)
public string RenderTemplate(Agent agent, string templateName, Dictionary<string, object>? renderData = null)
{
return $"Rendered template for {templateName}";
}
public bool RenderFunction(Agent agent, FunctionDef def)
public bool RenderFunction(Agent agent, FunctionDef def, Dictionary<string, object>? renderData = null)
{
return true;
}
public (string, IEnumerable<FunctionDef>) PrepareInstructionAndFunctions(Agent agent, StringComparer? comparer = null)
public (string, IEnumerable<FunctionDef>) PrepareInstructionAndFunctions(Agent agent, Dictionary<string, object>? renderData = null, StringComparer? comparer = null)
{
return (string.Empty, []);
}
public IEnumerable<FunctionDef> FilterFunctions(string instruction, Agent agent, StringComparer? comparer = null)
{
return [];
}
public IEnumerable<FunctionDef> FilterFunctions(string instruction, IEnumerable<FunctionDef> functions, StringComparer? comparer = null)
{
return [];
}
public FunctionParametersDef? RenderFunctionProperty(Agent agent, FunctionDef def)
public FunctionParametersDef? RenderFunctionProperty(Agent agent, FunctionDef def, Dictionary<string, object>? renderData = null)
{
return def.Parameters;
}
@ -130,5 +120,10 @@ namespace BotSharp.Plugin.Google.Core
{
return Task.FromResult(Enumerable.Empty<AgentUtility>());
}
public Dictionary<string, object> CollectRenderData(Agent agent)
{
return [];
}
}
}

View file

@ -38,7 +38,7 @@ namespace BotSharp.Plugin.SemanticKernel.Tests
_servicesMock.Setup(x => x.GetService(typeof(IEnumerable<IContentGeneratingHook>)))
.Returns(new List<IContentGeneratingHook>());
var agentService = new Mock<IAgentService>();
agentService.Setup(x => x.RenderInstruction(agent)).Returns("How can I help you?");
agentService.Setup(x => x.RenderInstruction(agent, null)).Returns("How can I help you?");
_servicesMock.Setup(x => x.GetService(typeof(IAgentService)))
.Returns(agentService.Object);