Merge pull request #943 from iceljc/features/call-dummy-func

call dummy func
This commit is contained in:
iceljc 2025-03-21 16:44:44 -05:00 committed by GitHub
commit 08a8c872e0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 323 additions and 64 deletions

View file

@ -9,7 +9,7 @@ public interface IConversationService
string ConversationId { get; }
Task<Conversation> NewConversation(Conversation conversation);
void SetConversationId(string conversationId, List<MessageState> states, bool isReadOnly = false);
Task<Conversation> GetConversation(string id);
Task<Conversation> GetConversation(string id, bool isLoadStates = false);
Task<PagedItems<Conversation>> GetConversations(ConversationFilter filter);
Task<Conversation> UpdateConversationTitle(string id, string title);
Task<Conversation> UpdateConversationTitleAlias(string id, string titleAlias);

View file

@ -25,6 +25,10 @@ public class FunctionDef
[JsonPropertyName("parameters")]
public FunctionParametersDef Parameters { get; set; } = new FunctionParametersDef();
[JsonPropertyName("output")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Output { get; set; }
public override string ToString()
{
return $"{Name}: {Description}";

View file

@ -0,0 +1,7 @@
namespace BotSharp.Abstraction.Instructs.Models;
public class ExecuteTemplateArgs
{
[JsonPropertyName("template_name")]
public string? TemplateName { get; set; }
}

View file

@ -27,6 +27,8 @@ public class ConversationFilter
public List<string>? Tags { get; set; }
public bool IsLoadLatestStates { get; set; }
public static ConversationFilter Empty()
{
return new ConversationFilter();

View file

@ -124,7 +124,7 @@ public interface IBotSharpRepository : IHaveServiceProvider
=> throw new NotImplementedException();
void UpdateConversationStatus(string conversationId, string status)
=> throw new NotImplementedException();
Conversation GetConversation(string conversationId)
Conversation GetConversation(string conversationId, bool isLoadStates = false)
=> throw new NotImplementedException();
PagedItems<Conversation> GetConversations(ConversationFilter filter)
=> throw new NotImplementedException();

View file

@ -94,6 +94,7 @@
<None Remove="data\agents\dfd9b46d-d00c-40af-8a75-3fbdc2b89869\templates\instruction.metrics.liquid" />
<None Remove="data\agents\dfd9b46d-d00c-40af-8a75-3fbdc2b89869\templates\instruction.reviewer.liquid" />
<None Remove="data\agents\dfd9b46d-d00c-40af-8a75-3fbdc2b89869\templates\instruction.simulator.liquid" />
<None Remove="data\agents\dfd9b46d-d00c-40af-8a75-3fbdc2b89869\templates\instruction.simulator.liquid" />
<None Remove="data\plugins\config.json" />
</ItemGroup>
@ -191,6 +192,12 @@
<Content Include="data\agents\dfd9b46d-d00c-40af-8a75-3fbdc2b89869\templates\instruction.metrics.liquid">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\templates\util-instruct-execute_template.fn.liquid">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\util-instruct-execute_template.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="data\plugins\config.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>

View file

@ -71,7 +71,7 @@ public partial class ConversationService : IConversationService
return db.UpdateConversationMessage(conversationId, request);
}
public async Task<Conversation> GetConversation(string id)
public async Task<Conversation> GetConversation(string id, bool isLoadStates = false)
{
var db = _services.GetRequiredService<IBotSharpRepository>();
var conversation = db.GetConversation(id);
@ -80,6 +80,11 @@ public partial class ConversationService : IConversationService
public async Task<PagedItems<Conversation>> GetConversations(ConversationFilter filter)
{
if (filter == null)
{
filter = ConversationFilter.Empty();
}
var db = _services.GetRequiredService<IBotSharpRepository>();
var conversations = db.GetConversations(filter);
return conversations;

View file

@ -69,23 +69,34 @@ public class ConversationStateService : IConversationStateService
return this;
}
var defaultRound = -1;
var preValue = string.Empty;
var currentValue = value.ToString();
var hooks = _services.GetServices<IConversationHook>();
var curActiveRounds = activeRounds > 0 ? activeRounds : -1;
int? preActiveRounds = null;
var curActive = true;
StateKeyValue? pair = null;
StateValue? prevLeafNode = null;
var curActiveRounds = activeRounds > 0 ? activeRounds : defaultRound;
if (ContainsState(name) && _curStates.TryGetValue(name, out var pair))
if (ContainsState(name) && _curStates.TryGetValue(name, out pair))
{
var leafNode = pair?.Values?.LastOrDefault();
preActiveRounds = leafNode?.ActiveRounds;
preValue = leafNode?.Data ?? string.Empty;
prevLeafNode = pair?.Values?.LastOrDefault();
preValue = prevLeafNode?.Data ?? string.Empty;
}
_logger.LogInformation($"[STATE] {name} = {value}");
var routingCtx = _services.GetRequiredService<IRoutingContext>();
if (!ContainsState(name) || preValue != currentValue || preActiveRounds != curActiveRounds)
var isNoChange = ContainsState(name)
&& preValue == currentValue
&& prevLeafNode?.ActiveRounds == curActiveRounds
&& curActiveRounds == defaultRound
&& prevLeafNode?.Source == source
&& prevLeafNode?.DataType == valueType
&& prevLeafNode?.Active == curActive
&& pair?.Readonly == readOnly;
var hooks = _services.GetServices<IConversationHook>();
if (!ContainsState(name) || preValue != currentValue || prevLeafNode?.ActiveRounds != curActiveRounds)
{
foreach (var hook in hooks)
{
@ -95,7 +106,7 @@ public class ConversationStateService : IConversationStateService
MessageId = routingCtx.MessageId,
Name = name,
BeforeValue = preValue,
BeforeActiveRounds = preActiveRounds,
BeforeActiveRounds = prevLeafNode?.ActiveRounds,
AfterValue = currentValue,
AfterActiveRounds = curActiveRounds,
DataType = valueType,
@ -116,7 +127,7 @@ public class ConversationStateService : IConversationStateService
{
Data = currentValue,
MessageId = routingCtx.MessageId,
Active = true,
Active = curActive,
ActiveRounds = curActiveRounds,
DataType = valueType,
Source = source,
@ -128,6 +139,10 @@ public class ConversationStateService : IConversationStateService
newPair.Values = new List<StateValue> { newValue };
_curStates[name] = newPair;
}
else if (isNoChange)
{
// do nothing
}
else
{
_curStates[name].Values.Add(newValue);
@ -415,14 +430,14 @@ public class ConversationStateService : IConversationStateService
{
var values = _curStates.Values.ToList();
var copy = JsonSerializer.Deserialize<List<StateKeyValue>>(JsonSerializer.Serialize(values));
return new ConversationState(copy ?? new());
return new ConversationState(copy ?? []);
}
public void SetCurrentState(ConversationState state)
{
var values = _curStates.Values.ToList();
var copy = JsonSerializer.Deserialize<List<StateKeyValue>>(JsonSerializer.Serialize(values));
_curStates = new ConversationState(copy ?? new());
_curStates = new ConversationState(copy ?? []);
}
public void ResetCurrentState()

View file

@ -0,0 +1,92 @@
using BotSharp.Abstraction.Functions;
using BotSharp.Abstraction.Instructs;
using BotSharp.Abstraction.Instructs.Models;
namespace BotSharp.Core.Instructs.Functions;
public class ExecuteTemplateFn : IFunctionCallback
{
public string Name => "util-instruct-execute_template";
private readonly IServiceProvider _services;
private readonly ILogger<ExecuteTemplateFn> _logger;
public ExecuteTemplateFn(
IServiceProvider services,
ILogger<ExecuteTemplateFn> logger)
{
_services = services;
_logger = logger;
}
public async Task<bool> Execute(RoleDialogModel message)
{
var args = JsonSerializer.Deserialize<ExecuteTemplateArgs>(message.FunctionArgs);
if (string.IsNullOrEmpty(args.TemplateName))
{
message.Content = $"Invalid template name.";
return false;
}
var agentService = _services.GetRequiredService<IAgentService>();
var agent = await agentService.GetAgent(message.CurrentAgentId);
var template = agent.Templates.FirstOrDefault(x => x.Name.IsEqualTo(args.TemplateName));
if (template == null)
{
message.Content = $"Cannot find template ({args.TemplateName}) in agent {agent.Name}";
return false;
}
var response = await GetAiResponse(agent, args.TemplateName);
message.Content = response;
return true;
}
private async Task<string> GetAiResponse(Agent agent, string templateName)
{
try
{
var agentService = _services.GetRequiredService<IAgentService>();
var text = agentService.RenderedTemplate(agent, templateName);
var completion = CompletionProvider.GetChatCompletion(_services, provider: agent.LlmConfig?.Provider, model: agent.LlmConfig?.Model);
var response = await completion.GetChatCompletions(new Agent()
{
Id = agent.Id
},
new List<RoleDialogModel>
{
new(AgentRole.User, text)
});
var hooks = _services.GetServices<IInstructHook>();
foreach (var hook in hooks)
{
if (!string.IsNullOrEmpty(hook.SelfId) && hook.SelfId != agent.Id)
{
continue;
}
await hook.OnResponseGenerated(new InstructResponseModel
{
AgentId = agent.Id,
TemplateName = templateName,
Provider = completion.Provider,
Model = completion.Model,
UserMessage = text,
CompletionText = response.Content
});
}
return response.Content;
}
catch (Exception ex)
{
var error = $"Error when getting agent {agent.Name} instruction response.";
_logger.LogWarning($"{error} {ex.Message}\r\n{ex.InnerException}");
return error;
}
}
}

View file

@ -0,0 +1,17 @@
namespace BotSharp.Core.Instructs.Hooks;
public class InstructUtilityHook : IAgentUtilityHook
{
private static string PREFIX = "util-instruct-";
private static string EXECUTE_TEMPLATE = $"{PREFIX}execute_template";
public void AddUtilities(List<AgentUtility> utilities)
{
utilities.Add(new AgentUtility
{
Name = "instruct.template",
Functions = [new($"{EXECUTE_TEMPLATE}")],
Templates = [new($"{EXECUTE_TEMPLATE}.fn")]
});
}
}

View file

@ -1,6 +1,7 @@
using BotSharp.Abstraction.Instructs.Settings;
using BotSharp.Abstraction.Plugins.Models;
using BotSharp.Abstraction.Settings;
using BotSharp.Core.Instructs.Hooks;
using Microsoft.Extensions.Configuration;
namespace BotSharp.Core.Instructs;
@ -18,6 +19,8 @@ public class InsturctionPlugin : IBotSharpPlugin
var settingService = provider.GetRequiredService<ISettingService>();
return settingService.Bind<InstructionSettings>("Instruction");
});
services.AddScoped<IAgentUtilityHook, InstructUtilityHook>();
}
public bool AttachMenu(List<PluginMenuDef> menu)

View file

@ -80,7 +80,7 @@ public class BotSharpDbContext : Database, IBotSharpRepository
public bool DeleteConversations(IEnumerable<string> conversationIds)
=> throw new NotImplementedException();
public Conversation GetConversation(string conversationId)
public Conversation GetConversation(string conversationId, bool isLoadStates = false)
=> throw new NotImplementedException();
public PagedItems<Conversation> GetConversations(ConversationFilter filter)

View file

@ -1,5 +1,6 @@
using BotSharp.Abstraction.Loggers.Models;
using BotSharp.Abstraction.Users.Models;
using System;
using System.IO;
namespace BotSharp.Core.Repository;
@ -346,7 +347,7 @@ public partial class FileRepository
}
}
public Conversation GetConversation(string conversationId)
public Conversation GetConversation(string conversationId, bool isLoadStates = false)
{
var convDir = FindConversationDirectory(conversationId);
if (string.IsNullOrEmpty(convDir)) return null;
@ -361,18 +362,20 @@ public partial class FileRepository
record.Dialogs = CollectDialogElements(dialogFile);
}
var stateFile = Path.Combine(convDir, STATE_FILE);
if (record != null)
if (isLoadStates)
{
var states = CollectConversationStates(stateFile);
var curStates = new Dictionary<string, string>();
states.ForEach(x =>
var latestStateFile = Path.Combine(convDir, CONV_LATEST_STATE_FILE);
if (record != null && File.Exists(latestStateFile))
{
curStates[x.Key] = x.Values?.LastOrDefault()?.Data ?? string.Empty;
});
record.States = curStates;
var stateJson = File.ReadAllText(latestStateFile);
var states = JsonSerializer.Deserialize<Dictionary<string, JsonDocument>>(stateJson, _options) ?? [];
record.States = states.ToDictionary(x => x.Key, x =>
{
var elem = x.Value.RootElement.GetProperty("data");
return elem.ValueKind != JsonValueKind.Null ? elem.ToString() : null;
});
}
}
return record;
}
@ -508,6 +511,21 @@ public partial class FileRepository
if (!matched) continue;
if (filter.IsLoadLatestStates)
{
var latestStateFile = Path.Combine(d, CONV_LATEST_STATE_FILE);
if (File.Exists(latestStateFile))
{
var stateJson = File.ReadAllText(latestStateFile);
var states = JsonSerializer.Deserialize<Dictionary<string, JsonDocument>>(stateJson, _options) ?? [];
record.States = states.ToDictionary(x => x.Key, x =>
{
var elem = x.Value.RootElement.GetProperty("data");
return elem.ValueKind != JsonValueKind.Null ? elem.ToString() : null;
});
}
}
records.Add(record);
}

View file

@ -8,13 +8,11 @@ public class RoutingUtilityHook : IAgentUtilityHook
public void AddUtilities(List<AgentUtility> utilities)
{
var utility = new AgentUtility
utilities.Add(new AgentUtility
{
Name = "routing.tools",
Functions = [new($"{REDIRECT_TO_AGENT}"), new($"{FALLBACK_TO_ROUTER}")],
Templates = [new($"{REDIRECT_TO_AGENT}.fn"), new($"{FALLBACK_TO_ROUTER}.fn")]
};
utilities.Add(utility);
});
}
}

View file

@ -101,8 +101,8 @@ public partial class RoutingService
Context.SetDialogs(dialogs);
// Send to Next LLM
var agentId = routing.Context.GetCurrentAgentId();
await InvokeAgent(agentId, dialogs);
var curAgentId = routing.Context.GetCurrentAgentId();
await InvokeAgent(curAgentId, dialogs);
}
}
else

View file

@ -1,4 +1,6 @@
using BotSharp.Abstraction.Functions;
using BotSharp.Abstraction.Templating;
namespace BotSharp.Core.Routing;
public partial class RoutingService
@ -6,12 +8,20 @@ public partial class RoutingService
public async Task<bool> InvokeFunction(string name, RoleDialogModel message)
{
var function = _services.GetServices<IFunctionCallback>().FirstOrDefault(x => x.Name == name);
var isFillDummyContent = false;
var dummyFuncResponse = string.Empty;
if (function == null)
{
message.StopCompletion = true;
message.Content = $"Can't find function implementation of {name}.";
_logger.LogError(message.Content);
return false;
dummyFuncResponse = await GetDummyFunctionOutput(name, message);
isFillDummyContent = !string.IsNullOrEmpty(dummyFuncResponse);
if (!isFillDummyContent)
{
message.StopCompletion = true;
message.Content = $"Can't find function implementation of {name}.";
_logger.LogError(message.Content);
return false;
}
}
// Clone message
@ -25,7 +35,15 @@ public partial class RoutingService
var progressService = _services.GetService<IConversationProgressService>();
// Before executing functions
clonedMessage.Indication = await function.GetIndication(message);
if (!isFillDummyContent)
{
clonedMessage.Indication = await function.GetIndication(message);
}
else
{
clonedMessage.Indication = "Running";
}
if (progressService?.OnFunctionExecuting != null)
{
await progressService.OnFunctionExecuting(clonedMessage);
@ -40,7 +58,15 @@ public partial class RoutingService
try
{
result = await function.Execute(clonedMessage);
if (!isFillDummyContent)
{
result = await function.Execute(clonedMessage);
}
else
{
clonedMessage.Content = dummyFuncResponse;
result = true;
}
// After functions have been executed
foreach (var hook in hooks)
@ -87,4 +113,32 @@ public partial class RoutingService
return result;
}
private async Task<string?> GetDummyFunctionOutput(string functionName, RoleDialogModel message)
{
if (string.IsNullOrEmpty(message.CurrentAgentId))
{
return null;
}
var agentService = _services.GetRequiredService<IAgentService>();
var agent = await agentService.GetAgent(message.CurrentAgentId);
var found = agent?.Functions?.FirstOrDefault(x => x.Name == functionName);
if (string.IsNullOrWhiteSpace(found?.Output))
{
return null;
}
var render = _services.GetRequiredService<ITemplateRender>();
var state = _services.GetRequiredService<IConversationStateService>();
var dict = new Dictionary<string, object>();
foreach (var item in state.GetStates())
{
dict[item.Key] = item.Value;
}
var text = render.Render(found.Output, dict);
return text;
}
}

View file

@ -0,0 +1,14 @@
{
"name": "util-instruct-execute_template",
"description": "Select a specific template that can handle the user's request.",
"parameters": {
"type": "object",
"properties": {
"template_name": {
"type": "string",
"description": "The template name that is selected for handling the request."
}
},
"required": [ "template_name" ]
}
}

View file

@ -0,0 +1,3 @@
please call function util-routing-execute_template if user wants to use a template to fulfill a specific task.
Please ensure each template is executed only once.
Please output the template response directly without changing anthything.

View file

@ -138,7 +138,7 @@ public class ConversationController : ControllerBase
}
[HttpGet("/conversation/{conversationId}")]
public async Task<ConversationViewModel?> GetConversation([FromRoute] string conversationId)
public async Task<ConversationViewModel?> GetConversation([FromRoute] string conversationId, [FromQuery] bool isLoadStates = false)
{
var service = _services.GetRequiredService<IConversationService>();
var userService = _services.GetRequiredService<IUserService>();
@ -151,7 +151,8 @@ public class ConversationController : ControllerBase
var filter = new ConversationFilter
{
Id = conversationId,
UserId = !isAdmin ? user.Id : null
UserId = !isAdmin ? user.Id : null,
IsLoadLatestStates = isLoadStates
};
var conversations = await service.GetConversations(filter);
if (conversations.Items.IsNullOrEmpty())
@ -161,7 +162,6 @@ public class ConversationController : ControllerBase
var result = ConversationViewModel.FromSession(conversations.Items.First());
var state = _services.GetRequiredService<IConversationStateService>();
result.States = state.Load(conversationId, isReadOnly: true);
user = await userService.GetUser(result.User.Id);
result.User = UserViewModel.FromUser(user);

View file

@ -31,7 +31,7 @@ public class ConversationViewModel
public string? TaskId { get; set; }
public string Status { get; set; }
public Dictionary<string, string> States { get; set; }
public Dictionary<string, string> States { get; set; } = [];
public List<string> Tags { get; set; } = new();
@ -55,7 +55,8 @@ public class ConversationViewModel
Channel = sess.Channel,
Status = sess.Status,
TaskId = sess.TaskId,
Tags = sess.Tags ?? new(),
Tags = sess.Tags ?? [],
States = sess.States ?? [],
CreatedTime = sess.CreatedTime,
UpdatedTime = sess.UpdatedTime
};

View file

@ -106,6 +106,7 @@ public class ChatHubConversationHook : ConversationHookBase
if (!AllowSendingMessage()) return;
var conv = _services.GetRequiredService<IConversationService>();
var state = _services.GetRequiredService<IConversationStateService>();
var json = JsonSerializer.Serialize(new ChatResponseModel()
{
ConversationId = conv.ConversationId,
@ -114,6 +115,7 @@ public class ChatHubConversationHook : ConversationHookBase
Function = message.FunctionName,
RichContent = message.SecondaryRichContent ?? message.RichContent,
Data = message.Data,
States = state.GetStates(),
Sender = new UserViewModel()
{
FirstName = "AI",

View file

@ -12,6 +12,7 @@ public class FunctionDefMongoElement
public string? VisibilityExpression { get; set; }
public string? Impact { get; set; }
public FunctionParametersDefMongoElement Parameters { get; set; } = new();
public string? Output { get; set; }
public static FunctionDefMongoElement ToMongoElement(FunctionDef function)
{
@ -27,7 +28,8 @@ public class FunctionDefMongoElement
Type = function.Parameters.Type,
Properties = JsonSerializer.Serialize(function.Parameters.Properties),
Required = function.Parameters.Required,
}
},
Output = function.Output
};
}
@ -45,7 +47,8 @@ public class FunctionDefMongoElement
Type = function.Parameters.Type,
Properties = JsonSerializer.Deserialize<JsonDocument>(function.Parameters.Properties.IfNullOrEmptyAs("{}")),
Required = function.Parameters.Required,
}
},
Output = function.Output
};
}
}

View file

@ -299,26 +299,25 @@ public partial class MongoRepository
_dc.Conversations.UpdateOne(filter, update);
}
public Conversation GetConversation(string conversationId)
public Conversation GetConversation(string conversationId, bool isLoadStates = false)
{
if (string.IsNullOrEmpty(conversationId)) return null;
var filterConv = Builders<ConversationDocument>.Filter.Eq(x => x.Id, conversationId);
var filterDialog = Builders<ConversationDialogDocument>.Filter.Eq(x => x.ConversationId, conversationId);
var filterState = Builders<ConversationStateDocument>.Filter.Eq(x => x.ConversationId, conversationId);
var conv = _dc.Conversations.Find(filterConv).FirstOrDefault();
var dialog = _dc.ConversationDialogs.Find(filterDialog).FirstOrDefault();
var states = _dc.ConversationStates.Find(filterState).FirstOrDefault();
if (conv == null) return null;
var dialogElements = dialog?.Dialogs?.Select(x => DialogMongoElement.ToDomainElement(x))?.ToList() ?? new List<DialogElement>();
var curStates = new Dictionary<string, string>();
states.States.ForEach(x =>
var curStates = conv.LatestStates?.ToDictionary(x => x.Key, x =>
{
curStates[x.Key] = x.Values?.LastOrDefault()?.Data ?? string.Empty;
});
var jsonDoc = JsonDocument.Parse(x.Value.ToJson());
var data = jsonDoc.RootElement.GetProperty("data");
return data.ValueKind != JsonValueKind.Null ? data.ToString() : null;
}) ?? [];
return new Conversation
{
@ -456,19 +455,34 @@ public partial class MongoRepository
var conversationDocs = _dc.Conversations.Find(filterDef).Sort(sortDef).Skip(pager.Offset).Limit(pager.Size).ToList();
var count = _dc.Conversations.CountDocuments(filterDef);
var conversations = conversationDocs.Select(x => new Conversation
var conversations = conversationDocs.Select(x =>
{
Id = x.Id.ToString(),
AgentId = x.AgentId.ToString(),
UserId = x.UserId.ToString(),
TaskId = x.TaskId,
Title = x.Title,
Channel = x.Channel,
Status = x.Status,
DialogCount = x.DialogCount,
Tags = x.Tags ?? new(),
CreatedTime = x.CreatedTime,
UpdatedTime = x.UpdatedTime
var states = new Dictionary<string, string>();
if (filter.IsLoadLatestStates)
{
states = x.LatestStates.ToDictionary(p => p.Key, p =>
{
var jsonDoc = JsonDocument.Parse(p.Value.ToJson());
var data = jsonDoc.RootElement.GetProperty("data");
return data.ValueKind != JsonValueKind.Null ? data.ToString() : null;
});
}
return new Conversation
{
Id = x.Id.ToString(),
AgentId = x.AgentId.ToString(),
UserId = x.UserId.ToString(),
TaskId = x.TaskId,
Title = x.Title,
Channel = x.Channel,
Status = x.Status,
DialogCount = x.DialogCount,
Tags = x.Tags ?? [],
States = states,
CreatedTime = x.CreatedTime,
UpdatedTime = x.UpdatedTime
};
}).ToList();
return new PagedItems<Conversation>