Skip to content
Open
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
21 changes: 21 additions & 0 deletions aevatar-framework.sln
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aevatar.TestBase", "test\Ae
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aevatar.GAgents.Tests", "test\Aevatar.GAgents.Tests\Aevatar.GAgents.Tests.csproj", "{B06AEDD8-DCE4-4F23-B730-C353E627A8E4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aevatar.Plugins.Test", "test\Aevatar.Plugins.Test\Aevatar.Plugins.Test.csproj", "{8D9680A9-0BD0-46EC-AE4C-8FE666900C2E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aevatar.ProxyGAgent.Sdk", "test\Aevatar.ProxyGAgent.Sdk\Aevatar.ProxyGAgent.Sdk.csproj", "{41D14456-6FBB-4432-BBA0-C01DA8DABE2E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aevatar.ProxyGAgent", "src\Aevatar.ProxyGAgent\Aevatar.ProxyGAgent.csproj", "{DA353205-DE92-45CA-995B-D5C492D55528}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -34,6 +40,9 @@ Global
{6545F4B2-EB81-4CF6-8FEC-C5FF8E116C7C} = {96D7B45D-F041-44DD-BFCC-E17048911357}
{55EF840B-F16D-4D15-9770-B3006ADB6EFB} = {96D7B45D-F041-44DD-BFCC-E17048911357}
{B06AEDD8-DCE4-4F23-B730-C353E627A8E4} = {96D7B45D-F041-44DD-BFCC-E17048911357}
{8D9680A9-0BD0-46EC-AE4C-8FE666900C2E} = {96D7B45D-F041-44DD-BFCC-E17048911357}
{41D14456-6FBB-4432-BBA0-C01DA8DABE2E} = {94D0A422-54BA-48E4-9AAA-4C59BFF72F06}
{DA353205-DE92-45CA-995B-D5C492D55528} = {94D0A422-54BA-48E4-9AAA-4C59BFF72F06}
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{70485291-29F6-4FBA-BFE3-3F400184B3C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
Expand Down Expand Up @@ -68,5 +77,17 @@ Global
{B06AEDD8-DCE4-4F23-B730-C353E627A8E4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B06AEDD8-DCE4-4F23-B730-C353E627A8E4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B06AEDD8-DCE4-4F23-B730-C353E627A8E4}.Release|Any CPU.Build.0 = Release|Any CPU
{8D9680A9-0BD0-46EC-AE4C-8FE666900C2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8D9680A9-0BD0-46EC-AE4C-8FE666900C2E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8D9680A9-0BD0-46EC-AE4C-8FE666900C2E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8D9680A9-0BD0-46EC-AE4C-8FE666900C2E}.Release|Any CPU.Build.0 = Release|Any CPU
{41D14456-6FBB-4432-BBA0-C01DA8DABE2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{41D14456-6FBB-4432-BBA0-C01DA8DABE2E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{41D14456-6FBB-4432-BBA0-C01DA8DABE2E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{41D14456-6FBB-4432-BBA0-C01DA8DABE2E}.Release|Any CPU.Build.0 = Release|Any CPU
{DA353205-DE92-45CA-995B-D5C492D55528}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DA353205-DE92-45CA-995B-D5C492D55528}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DA353205-DE92-45CA-995B-D5C492D55528}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DA353205-DE92-45CA-995B-D5C492D55528}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Aevatar.Core.Abstractions.ProxyGAgent;

[GenerateSerializer]
public class ProxyGAgentInitialization : InitializationEventBase
{
[Id(0)] public byte[] PluginCode { get; set; }
}
8 changes: 8 additions & 0 deletions src/Aevatar.Core.Abstractions/ProxyGAgent/ProxyGAgentState.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Aevatar.Core.Abstractions.ProxyGAgent;

[GenerateSerializer]
public class ProxyGAgentState : StateBase
{
[Id(0)] public byte[]? PluginCode { get; set; }
[Id(1)] public Dictionary<string, object>? Database { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Aevatar.Core.Abstractions.ProxyGAgent;

[GenerateSerializer]
public class ProxyStateLogEvent : StateLogEventBase<ProxyStateLogEvent>
{
[Id(0)] public Dictionary<string, object> Data { get; set; }
}
16 changes: 16 additions & 0 deletions src/Aevatar.ProxyGAgent/Aevatar.ProxyGAgent.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>Aevatar.ProxyGAgent</RootNamespace>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\test\Aevatar.ProxyGAgent.Sdk\Aevatar.ProxyGAgent.Sdk.csproj" />
<ProjectReference Include="..\Aevatar.Core.Abstractions\Aevatar.Core.Abstractions.csproj" />
<ProjectReference Include="..\Aevatar.Core\Aevatar.Core.csproj" />
</ItemGroup>

</Project>
31 changes: 31 additions & 0 deletions src/Aevatar.ProxyGAgent/ProxyCodeLoadContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System.Reflection;
using System.Runtime.Loader;

namespace Aevatar.ProxyGAgent;

public class ProxyCodeLoadContext : AssemblyLoadContext
{
private readonly ISdkStreamManager _sdkStreamManager;

public ProxyCodeLoadContext(ISdkStreamManager sdkStreamManager) : base(true)
{
_sdkStreamManager = sdkStreamManager;
}

protected override Assembly Load(AssemblyName assemblyName)
{
return LoadFromFolderOrDefault(assemblyName);
}

private Assembly LoadFromFolderOrDefault(AssemblyName assemblyName)
{
if (assemblyName.Name.StartsWith("Aevatar.ProxyGAgent.Sdk"))
{
// Sdk assembly should NOT be shared
using var stream = _sdkStreamManager.GetStream(assemblyName);
return LoadFromStream(stream);
}

return null;
}
}
160 changes: 160 additions & 0 deletions src/Aevatar.ProxyGAgent/ProxyGAgent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
using System.Reflection;
using System.Runtime.Loader;
using Aevatar.Core;
using Aevatar.Core.Abstractions;
using Aevatar.Core.Abstractions.ProxyGAgent;
using Aevatar.ProxyGAgent.Sdk;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;

namespace Aevatar.ProxyGAgent;

[GAgent("proxy")]
public class ProxyGAgent : GAgentBase<ProxyGAgentState, ProxyStateLogEvent, ProxyGAgentEvent, ProxyGAgentInitialization>
{
public ProxyGAgent(ILogger<ProxyGAgent> logger) : base(logger)
{
AppDomain.CurrentDomain.AssemblyResolve += OnAssemblyResolve!;
}

private static Assembly? OnAssemblyResolve(object sender, ResolveEventArgs args)
{
var folderPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
if (folderPath == null) return null;
var assemblyPath = Path.Combine(folderPath, new AssemblyName(args.Name).Name + ".dll");
if (!File.Exists(assemblyPath)) return null;
var assembly = Assembly.LoadFrom(assemblyPath);
return assembly;
}

public override Task<string> GetDescriptionAsync()
{
return Task.FromResult("This is a proxy GAgent for executing C# code.");
}

public override async Task InitializeAsync(ProxyGAgentInitialization initializeDto)
{
RaiseEvent(new SetPluginCode
{
PluginCode = initializeDto.PluginCode
});
await ConfirmEvents();
}

[GenerateSerializer]
public class SetPluginCode : ProxyStateLogEvent
{
[Id(0)] public byte[] PluginCode { get; set; }
}

protected override void GAgentTransitionState(ProxyGAgentState state, StateLogEventBase<ProxyStateLogEvent> @event)
{
if (@event is SetPluginCode setPluginCode)
{
State.PluginCode = setPluginCode.PluginCode;
return;
}

if (State.PluginCode.IsNullOrEmpty())
{
return;
}

var assembly = Assembly.Load(State.PluginCode!);
var logEventConsistencyTypes = GetLogEventConsistencyTypes(assembly);
if (logEventConsistencyTypes == null) return;
foreach (var logEventConsistencyType in logEventConsistencyTypes)
{
var logEventConsistencyInstance = Activator.CreateInstance(logEventConsistencyType)!;
if (logEventConsistencyInstance is not ILogEventConsistency logEventConsistency) continue;
logEventConsistency.State = State;
logEventConsistency.Apply((ProxyStateLogEvent)@event);
}
}

[AllEventHandler]
public async Task ExecuteEventHandlersAsync(EventWrapperBase eventData)
{
if (State.PluginCode.IsNullOrEmpty())
{
return;
}

var assembly = Assembly.Load(State.PluginCode!);
var handlerTypes = GetHandlerTypes(assembly);

foreach (var handlerType in handlerTypes)
{
var interfaceType = GetHandlerInterfaceType(handlerType);
var eventType = interfaceType.GetGenericArguments()[0];

if (IsMatchingEventType(eventData, eventType))
{
var handlerInstance = Activator.CreateInstance(handlerType);
var handleMethod = interfaceType.GetMethod(nameof(IGAgentEventHandler<EventBase>.HandleEventAsync));

if (handleMethod != null)
{
await InvokeHandleMethodAsync(handleMethod, handlerInstance!, eventData, eventType);
}
}
}
}

private IEnumerable<Type> GetHandlerTypes(Assembly assembly)
{
var handlerInterfaceType = typeof(IGAgentEventHandler<>);
return assembly.GetTypes()
.Where(t => t.GetInterfaces()
.Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == handlerInterfaceType) &&
t is { IsInterface: false, IsAbstract: false });
}

private IEnumerable<Type>? GetLogEventConsistencyTypes(Assembly? assembly)
{
if (assembly == null) return null;
var handlerInterfaceType = typeof(ILogEventConsistency);
return assembly.GetTypes()
.Where(t => handlerInterfaceType.IsAssignableFrom(t)
&& t is { IsInterface: false, IsAbstract: false });
}

private Type GetHandlerInterfaceType(Type handlerType)
{
var handlerInterfaceType = typeof(IGAgentEventHandler<>);
return handlerType.GetInterfaces()
.First(i => i.IsGenericType && i.GetGenericTypeDefinition() == handlerInterfaceType);
}

private bool IsMatchingEventType(EventWrapperBase eventData, Type eventType)
{
return eventData.GetType().IsGenericType &&
eventData.GetType().GetGenericTypeDefinition() == typeof(EventWrapper<>) &&
((EventWrapper<EventBase>)eventData).Event.GetType().FullName == eventType.FullName;
}

private async Task InvokeHandleMethodAsync(MethodInfo handleMethod, object handlerInstance,
EventWrapperBase eventData, Type eventType)
{
dynamic eventWrapper = eventData;
var eventJson = JsonConvert.SerializeObject(eventWrapper.Event);
var eventObject = JsonConvert.DeserializeObject(eventJson, eventType);

var result = await (Task<EventHandleResult>)handleMethod.Invoke(handlerInstance, new object[] { eventObject })!;
if (!result.StateLogEventList.IsNullOrEmpty())
{
foreach (var stateLogEvent in result.StateLogEventList)
{
RaiseEvent(stateLogEvent);
}
}

if (!result.GAgentEventBase.IsNullOrEmpty())
{
foreach (var eventBase in result.GAgentEventBase)
{
await PublishAsync(eventBase);
}
}
}
}
36 changes: 36 additions & 0 deletions src/Aevatar.ProxyGAgent/SdkStreamManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System.Collections.Concurrent;
using System.Reflection;

namespace Aevatar.ProxyGAgent;

public interface ISdkStreamManager
{
Stream GetStream(AssemblyName assemblyName);
}

public class SdkStreamManager(string sdkDir) : ISdkStreamManager
{
private readonly ConcurrentDictionary<string, byte[]> _cachedSdkStreams = new();

public Stream GetStream(AssemblyName assemblyName)
{
var path = Path.Combine(sdkDir, assemblyName.Name + ".dll");
if (!File.Exists(path))
{
var assembly = Assembly.Load(assemblyName);

path = assembly.Location;
}

if (!_cachedSdkStreams.TryGetValue(path, out var buffer))
{
using var fs = new FileStream(path, FileMode.Open, FileAccess.Read);
var length = (int)fs.Length;
buffer = new byte[length];
fs.ReadExactly(buffer, 0, length);
_cachedSdkStreams.TryAdd(path, buffer);
}

return new MemoryStream(buffer);
}
}
7 changes: 4 additions & 3 deletions test/Aevatar.Core.Tests/TestGAgents/NaiveTestGAgent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ public class NaiveTestGAgentState : StateBase
[Id(0)] public List<string> Content { get; set; }
}

[GenerateSerializer]
public class NaiveTestStateLogEvent : StateLogEventBase<NaiveTestStateLogEvent>
{
[Id(0)] public Guid Id { get; set; }
}

[GAgent("naiveTest")]
public class NaiveTestGAgent : GAgentBase<NaiveTestGAgentState, NaiveTestStateLogEvent,EventBase, NaiveGAgentInitialize>
public class NaiveTestGAgent : GAgentBase<NaiveTestGAgentState, NaiveTestStateLogEvent,EventBase, NaiveGAgentInitializationEvent>
{
public NaiveTestGAgent(ILogger<NaiveTestGAgent> logger) : base(logger)
{
Expand All @@ -27,13 +28,13 @@ public override Task<string> GetDescriptionAsync()
return Task.FromResult("This is a naive test GAgent");
}

public override async Task InitializeAsync(NaiveGAgentInitialize initialize)
public override async Task InitializeAsync(NaiveGAgentInitializationEvent initializationEvent)
{
if (State.Content.IsNullOrEmpty())
{
State.Content = [];
}

State.Content.Add(initialize.InitialGreeting);
State.Content.Add(initializationEvent.InitialGreeting);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
namespace Aevatar.Core.Tests.TestInitializeDtos;

[GenerateSerializer]
public class NaiveGAgentInitialize : InitializationEventBase
public class NaiveGAgentInitializationEvent : InitializationEventBase
{
[Id(0)] public string InitialGreeting { get; set; }
}
6 changes: 6 additions & 0 deletions test/Aevatar.GAgents.Tests/Aevatar.GAgents.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,20 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Aevatar.ProxyGAgent\Aevatar.ProxyGAgent.csproj" />
<ProjectReference Include="..\Aevatar.Core.Tests\Aevatar.Core.Tests.csproj" />
<ProjectReference Include="..\Aevatar.Plugins.Test\Aevatar.Plugins.Test.csproj" />
<ProjectReference Include="..\Aevatar.TestBase\Aevatar.TestBase.csproj" />
</ItemGroup>

<ItemGroup>
<Content Include="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<None Remove="ProxyGAgentPlugins\*.dll" />
<Content Include="ProxyGAgentPlugins\*.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>

</Project>
Loading