[MAFWorkflow框架揭秘-06]人机交互(HITL)的实现

对于一个复杂的Workflow系统来说,除了需要有强大的编排能力和灵活的执行机制之外,还需要有良好的人机交互(HITL)支持,以便在Workflow执行过程中能够及时地获取用户的输入和反馈,从而更好地指导Workflow的执行和优化。MAF的Workflow框架利用RequestPort来实现了人机交互的功能,我们先通过一个简单的实例来体验以下Workflow下的人机交互功能。

1. 在Workflow中引入审批流程

下面提供的实例将演示如何创建一个Workflow实现银行转账功能,并在转账之前引入一个审批流程来让用户确认转账信息。如下是涉及的两个数据类型,TransferTransaction表示转账交易的相关信息,TransferInput表示转账交易的输入数据,IsApproved属性表示用户是否批准了这笔转账交易。

public record TransferTransaction(string From,string To, decimal Ammount);
public record TransferInput(TransferTransaction Transaction, bool IsApproved);

我们定义了如下两个Executor来实现转账交易的审批和执行逻辑。RiskCheckExecutor负责对转账交易进行风险检查,如果转账金额小于1000元则直接批准,否则需要用户审批;TransferExecutor负责根据审批结果来执行转账操作,如果用户批准了转账则输出转账信息,否则输出拒绝信息。

public partial class RiskCheckExecutor():Executor("RiskCheck")
{
    [MessageHandler(Send = [typeof(TransferInput), typeof(TransferTransaction)])]
    public ValueTask CheckAsync(TransferTransaction transaction, IWorkflowContext context)
    {
        if (transaction.Ammount < 1000)
        { 
            return context.SendMessageAsync(new TransferInput(transaction, true));
        }
        else
        {
            return context.SendMessageAsync(transaction);
        }
    }
}

public partial class TransferExecutor() : Executor("MoneyTransfer")
{
    [MessageHandler(Yield = [typeof(string)])]
    public string Transfer(TransferInput input, IWorkflowContext context)
    {
        var transaction = input.Transaction;
        return input.IsApproved
            ? $"Transfer {transaction.Ammount} RMB Yuan from {transaction.From} to {transaction.To}"
            : "Transfer is rejected because not be approved.";
    }
}

值得一提的,定义在RiskCheckExecutor中用于风险评估的CheckAsync方法会根据转账金额的不同决定传递给下游节点的不同类型的消息:如果转账金额小于1000元则直接传递一个TransferInput对象(其中IsApproved属性为true),否则传递一个TransferTransaction对象(表示需要用户审批)。由于该方法的返回类型为ValueTask,所以需要利用标注的MessageHandlerAttribute显式声明这两个Send类型。

Workflow的编排和调用体现在下面的代码中。整个Workflow由RiskCheckExecutorTransferExecutorRequestPort三个节点组成。RiskCheckExecutor作为Workflow的入口节点,根据转账金额的不同将数据发送到TransferExecutor或者RequestPort节点;RequestPort节点用于向用户发出审批请求并等待用户的响应;TransferExecutor节点根据审批结果来执行转账操作并产出结果。整个Workflow的输出来自于TransferExecutor节点。

using Microsoft.Agents.AI.Workflows;

var riskCheckExecutor = new RiskCheckExecutor();
var transferExecutor = new TransferExecutor();
var requestPort = RequestPort.Create<TransferTransaction, TransferInput>("Approval");

var workflow = new WorkflowBuilder(riskCheckExecutor)
    .AddEdge(
        source: riskCheckExecutor, 
        target: transferExecutor, 
        condition: (object? input) =>input is TransferInput,
        label:"Ammount<1000")
    .AddEdge(
        source: riskCheckExecutor,
        target: requestPort, 
        condition: (object? input)=>input is TransferTransaction,
        label:"Ammount>=1000")
    .AddEdge(source:requestPort, target:transferExecutor)
    .WithOutputFrom(transferExecutor)
    .Build();

await Utilities.GenerateAndShowPngImageAsync(workflow);

var transaction = new TransferTransaction("6222020200088888888", "6222020200099999999", 2000);
var run = await InProcessExecution.Default.RunStreamingAsync(workflow, transaction);
await foreach (var @event in run.WatchStreamAsync())
{
    if (@event is RequestInfoEvent  requestInfoEvent && requestInfoEvent.Request.IsDataOfType<TransferTransaction>())
    {
        var transferTransaction = requestInfoEvent.Request.Data.As<TransferTransaction>()!;
        Console.WriteLine($"""
            有如下一笔转账交易需要审批:
            转出账号:{transferTransaction.From}
            转入账号:{transferTransaction.To}
            转账金额:{transferTransaction.Ammount}

            请审批是否通过?(y/n)""");
        var input = Console.ReadLine();
        var transferInput = new TransferInput(transferTransaction, input?.ToLower() == "y");
        var response = requestInfoEvent.Request.CreateResponse(transferInput);
        await run.SendResponseAsync(response);
    }

    if (@event is WorkflowOutputEvent outputEvent)
    {
        Console.WriteLine(outputEvent.Data);
    }
}

我们以流的方式调用Workflow,并在事件流中监听RequestInfoEvent事件捕获通过RequestPort发出的对外请求。我们从RequestInfoEventRequest属性得到表示对外请求的ExternalRequest对象,并进一步从其Data中提取表示荷载内容的TransferTransaction对象。在控制台输出转账交易的相关信息后,我们等待用户的输入来决定是否批准这笔转账交易。根据用户的输入我们创建一个TransferInput对象,并通过调用ExternalRequestCreateResponse方法将其封装成一个ExternalResponse对象,最后通过StreamingRunSendResponseAsync方法将响应发送回Workflow。上面的程序还调用了之前定义的Utilities.GenerateAndShowPngImageAsync方法来生成Workflow的可视化图像,效果如下所示:

Alternative Text

运行上面的程序,我们可以看到当转账金额大于等于1000元时,Workflow会向用户发出审批请求并等待用户的输入。当用户输入y表示批准转账时,Workflow会继续执行并输出转账成功的信息;当用户输入n表示拒绝转账时,Workflow会输出转账被拒绝的信息。如下所示的就是两端对应的输出:


有如下一笔转账交易需要审批:
转出账号:6222020200088888888
转入账号:6222020200099999999
转账金额:2000

请审批是否通过?(y/n):
y
Transfer 2000 RMB Yuan from 6222020200088888888 to 6222020200099999999
有如下一笔转账交易需要审批:
转出账号:6222020200088888888
转入账号:6222020200099999999
转账金额:2000

请审批是否通过?(y/n):
n
Transfer is rejected because not be approved.

2. RequestPort、RequestInfoExecutor & RequestPortBinding

演示实例创建的Workflow共包含三个节点,引入人机交互的我们创建的一个RequestPort对象。通过前面的系列文章,我们知道作为Workflow功能节点的必须是一个ExecutorBinding对象,后者会提供用来执行业务功能的Executor对象。这里纳入Workflow作为功能节点的并非这个 RequestPort对象,而是一个RequestPortBinding对象。RequestPortBinding会对应的RequestInfoExecutor发送审批请求和处理审批响应。RequestInfoExecutor是对一个RequestPort对象的封装,并使用后者作为Workflow与外界交互的端口。

2.1 RequestPort

作为Workflow与外界交互的端口,RequestPort的类型定义非常简单。一个RequestPort本质是就是一个具有唯一标识(对应Id属性)并绑定了请求和响应数据类型的对象。RequestPort的定义如下所示:

public record RequestPort(string Id, Type Request, Type Response)
{
	public static RequestPort<TRequest, TResponse> Create<TRequest, TResponse>(string id)
	=> new RequestPort<TRequest, TResponse>(id, typeof(TRequest), typeof(TResponse));
}
public record class RequestPortInfo(TypeId RequestType, TypeId ResponseType, string PortId);

另一个RequestPortInfo可以视为RequestPort的可移植类型,它将RequestPort中的Type属性转换成了TypeId类型,并且提供了一个PortId属性来对应RequestPortId属性。RequestPortInfo的设计使得它可以在Workflow的执行环境中被安全地传递和使用,而不需要担心类型信息丢失或者不兼容的问题。

2.2 RequestInfoExecutor

RequestInfoExecutor旨在为实现人机交互完成如下两项核心操作:

  • 将承载交互请求的输入(类型对应RequestPortRequest属性)转换成标准的外部请求(类型为ExternalRequest)发出去,执行环境会将其转化成一个RequestInfoEvent事件来通知外界有一个交互请求需要处理;
  • 提取用户的交互响应(类型为ExternalResponse)中的数据(类型对应RequestPortResponse属性),通过调用IWorkflowContext.SendMessageAsync方法将其发送到Workflow中供后续节点使用。

但是外部请求发往何处呢?这里就涉及如下这个名为IExternalRequestSink的内部接口。该接口是对外部请求发送目的地的抽象,是执行环境提供给RequestInfoExecutor用于倾倒外部请求的要给水槽(Sink)。如何接收并处理外部请求取决于执行环境的实现,这不应该是RequestInfoExecutor关心的事情,它只需要调用IExternalRequestSink.PostAsync方法将外部请求发出去就行了。

internal interface IExternalRequestSink
{
	ValueTask PostAsync(ExternalRequest request);
}

当我们调用RequestInfoExecutor的构造函数时需要传入一个RequestPort对象,它的ID会作为自身的ID。如果allowWrapped参数为true(默认值),RequestInfoExecutor除了注册用于处理原始外部请求(针对RequestPortRequest属性对应的类型)之外,还会额外注册一个针对ExternalRequest类型的消息处理器,以支持处理被包装成ExternalRequest的外部请求。这种设计使得RequestInfoExecutor能够同时支持两种不同格式的外部请求,从而提高了它的适用性和灵活性。

internal sealed class RequestInfoExecutor : Executor
{
	private readonly bool _allowWrapped;
	private RequestPort Port { get; }
	private IExternalRequestSink? RequestSink { get; set; }

	private static ExecutorOptions DefaultOptions => new ExecutorOptions
	{
		AutoSendMessageHandlerResultObject = false,
		AutoYieldOutputHandlerResultObject = false
	};

	public RequestInfoExecutor(RequestPort port, bool allowWrapped = true)
		: base(port.Id, DefaultOptions)
	{
		Port = port;
		_allowWrapped = allowWrapped;
	}
}

它定义RequestSink属性用来设置上述的IExternalRequestSink对象。值得一提的是:指定的ExecutorOptions中的AutoSendMessageHandlerResultObjectAutoYieldOutputHandlerResultObject都被设置为false。在如下这个简化版的定义中,我们可以看出RequestInfoExecutor的实现原理。

internal sealed class RequestInfoExecutor : Executor
{
    private readonly Dictionary<string, ExternalRequest> _wrappedRequests = [];
    protected override ProtocolBuilder ConfigureProtocol(ProtocolBuilder protocolBuilder)
    {
        return protocolBuilder
            .ConfigureRoutes(ConfigureRoutes)
            .SendsMessage<ExternalRequest>()
            .SendsMessageType(Port.Response);

        void ConfigureRoutes(RouteBuilder routeBuilder)
        {
            routeBuilder = routeBuilder
                .AddHandlerUntyped(Port.Request, HandleAsync)
                .AddCatchAll(HandleCatchAllAsync);
            if (_allowWrapped)
            {
                routeBuilder = routeBuilder.AddHandler<ExternalRequest, ExternalRequest>(HandleAsync);
            }
            routeBuilder = routeBuilder.AddHandler<ExternalResponse, ExternalResponse?>(HandleAsync);
        }
    }

    public async ValueTask<ExternalRequest?> HandleCatchAllAsync(
        PortableValue message, 
        IWorkflowContext context, 
        CancellationToken cancellationToken)
    {
        object? maybeRequest = message.AsType(this.Port.Request);
        if (maybeRequest != null)
        {
            var request = ExternalRequest.Create(this.Port, maybeRequest!);
            await this.RequestSink!.PostAsync(request).ConfigureAwait(false);
            return request;
        }
        else if (message.Is(out ExternalRequest? request))
        {
            return await this.HandleAsync(request, context, cancellationToken).ConfigureAwait(false);
        }
        return null;
    }

    public async ValueTask<ExternalRequest> HandleAsync(
        ExternalRequest message, 
        IWorkflowContext context, 
        CancellationToken cancellationToken = default)
    {
        var request = ExternalRequest.Create(this.Port, requestData, message.RequestId);
        _wrappedRequests.Add(message.RequestId, message);
        await RequestSink!.PostAsync(request).ConfigureAwait(false);
        return request;
    }

    public async ValueTask<ExternalRequest> HandleAsync(
        object message, 
        IWorkflowContext context, 
        CancellationToken cancellationToken = default)
    {
        var request = ExternalRequest.Create(this.Port, message);
        await RequestSink!.PostAsync(request).ConfigureAwait(false);
        return request;
    }

    public async ValueTask<ExternalResponse?> HandleAsync(
        ExternalResponse message, 
        IWorkflowContext context, 
        CancellationToken cancellationToken = default)
    {        
        if (_allowWrapped && _wrappedRequests.TryGetValue(message.RequestId, out ExternalRequest? originalRequest))
        {
            await context.SendMessageAsync(
                originalRequest.RewrapResponse(message), 
                cancellationToken: cancellationToken)
                .ConfigureAwait(false);
            _wrappedRequests.Remove(message.RequestId);
        }
        else
        {
            await context
                .SendMessageAsync(message, cancellationToken: cancellationToken)
                .ConfigureAwait(false);
            await context
               .SendMessageAsync(data, cancellationToken: cancellationToken)
               .ConfigureAwait(false);
        }
        return message;
    }
}

RequestInfoExecutor的核心在于四个注册的路由处理方法(三个重载的HandleAsync方法和一个HandleCatchAllAsync方法):

  • 处理原始请求(message类型为object):将原始请求转换成ExternalRequest对象,并通过RequestSink.PostAsync方法将其发送出去;
  • 处理ExternalRequest:克隆这个ExternalRequest对象(以防止后续处理中它被修改),并通过RequestSink.PostAsync方法将其发送出去,同时缓存原始请求;
  • 兜底处理函数:注册的HandleCatchAllAsync方法作为兜底的请求处理函数;
  • 处理ExternalResponse
    • 如果允许处理被包装的请求(_allowWrapped=true)并且缓存中存在与这个响应对应的原始请求,则将这个ExternalResponse对象重新包装成一个与原始请求对应的响应对象,并通过IWorkflowContext.SendMessageAsync方法发送给下游节点,同时从缓存中移除这个请求;
    • 否则直接将这个ExternalResponse对象发送回Workflow中供下游节点使用。

2.3 RequestPortBinding

RequestPortBinding除了提供用于创建RequestInfoExecutor实例的工厂方法之外,并无特别之处。

public record RequestPortBinding(RequestPort Port, bool AllowWrapped = true): ExecutorBinding(
    Port.Id,
    _ => new ValueTask<Executor>(new RequestInfoExecutor(Port, AllowWrapped)),
    typeof(RequestInfoExecutor),
    Port)
{
    public override bool IsSharedInstance => false;
    public override bool SupportsConcurrentSharedExecution => true;
    public override bool SupportsResetting => false;
}

RequestPort提供了BindAsExecutor扩展方法来方便地将一个RequestPort对象转换成一个RequestPortBinding对象。ExecutorBinding同时提供了针对RequestPort类型的隐式转换操作符,使得一个RequestPort对象可以直接被用在需要ExecutorBinding对象的地方,这样就可以更方便地将RequestPort纳入Workflow的编排中。

public abstract record ExecutorBinding
{
    public static implicit operator ExecutorBinding(RequestPort port)
	    => port.BindAsExecutor();
}

public static ExecutorBinding BindAsExecutor(this RequestPort port, bool allowWrappedRequests = true)
    => new RequestPortBinding(port, allowWrappedRequests);

针对RequestPortBinding的DirectEdge可以通过如下这个AddExternalCall扩展方法来创建。AddExternalCall方法会在指定的源节点和RequestPortBinding之间添加两条边,分别用于发送请求和接收响应,从而实现Workflow与外界的交互。

public static WorkflowBuilder AddExternalCall<TRequest, TResponse>(
    this WorkflowBuilder builder, 
    ExecutorBinding source, 
    string portId)
{
    var requestPort = new RequestPort(portId, typeof(TRequest), typeof(TResponse));
    return builder.AddEdge(source, requestPort).AddEdge(requestPort, source);
}

3. ExternalRequest、ExternalResponse和RequestInfoEvent

RequestInfoExecutor封装原始请求生成的标准外部请求类型为ExternalRequest,它包含了请求的相关信息(RequestPortInfo PortInfo属性)以及请求数据(Data属性)。ExternalRequest提供了一系列的方法来支持请求数据的类型检查和转换,以及创建响应对象等操作。如下所示:

public record ExternalRequest(RequestPortInfo PortInfo, string RequestId, PortableValue Data)
{
    public bool IsDataOfType<TValue>();

    public bool TryGetDataAs<TValue>([NotNullWhen(true)] out TValue? value);
    public bool TryGetDataAs(Type targetType, [NotNullWhen(true)] out object? value);
    
    public static ExternalRequest Create(RequestPort port, [NotNull] object data, string? requestId = null);    
    public static ExternalRequest Create<T>(RequestPort port, T data, string? requestId = null);
   
    public ExternalResponse CreateResponse(object data);    
    public ExternalResponse CreateResponse<T>(T data);
}

四组方法说明如下:

  • IsDataOfType:用于检查请求数据是否可以被转换成指定的类型TValue
  • TryGetDataAs:用于尝试将请求数据转换成指定的类型,如果转换成功则返回true并通过out参数返回转换后的值,否则返回false
  • Create:用于创建一个ExternalRequest对象,参数包括RequestPort对象、请求数据以及可选的请求ID;
  • CreateResponse:用于创建一个与这个请求对应的ExternalResponse对象,参数为响应数据。ExternalResponseExternalRequest类似,也包含了响应的相关信息(PortInfo属性)以及响应数据(Data属性)。它提供了一个RewrapResponse方法来将这个响应重新包装成一个与原始请求对应的响应对象,这在RequestInfoExecutor处理被包装的请求时非常有用。

与之对应的表示外部响应的ExternalResponse类型定义如下。 我们可以从中提取对应外部请求的Id、RequestPort的相关信息(PortInfo属性)以及响应数据(Data属性),它也提供了IsDataOfTypeTryGetDataAs方法来支持响应数据的类型检查和转换。

public record ExternalResponse(RequestPortInfo PortInfo, string RequestId, PortableValue Data)
{    
    public bool IsDataOfType<TValue>() => this.Data.Is<TValue>();
    public bool TryGetDataAs<TValue>(out TValue? value);
    public bool TryGetDataAs(Type targetType, out object? value);
}

ExternalRequest最终被封装成一个RequestInfoEvent事件来通知外界有一个交互请求需要处理。RequestInfoEvent包含了一个ExternalRequest对象作为事件数据。Workflow在执行过程中会将这个事件发送到事件流中,外界可以通过监听这个事件来获取交互请求的信息并进行相应的处理。

public sealed class RequestInfoEvent(ExternalRequest request) : WorkflowEvent(request)
{
	public ExternalRequest Request => request;
}

4. 完整的实现细节

RequestPort对象自身并无特殊之处,但是为什么Workflow可以利用它来实现人机交互的功能呢?外部请求如何转换成RequestInfoEvent事件?针对RequestPort的外部响应又如何回到对应的RequestInfoExecutor中呢?这就涉及到很多内部的实现细节了。我个人觉得针对这块的实现有太多弯弯绕绕,其设计还有很多待改进的地方。接下来我们就将相关代码拎出来,尽量把整个实现原理讲清楚。

我们首先要明确一点:我们构建的Workflow纯粹是对执行流程的静态表达,与具体的执行环境无关。当我们在指定的InProcessExecutionEnvironment中执行某个Workflow的时候,Workflow中的很多组件需要有一个绑定到对应的运行时对象上才能真正发挥作用,这有点类似于延迟绑定的意思。其中涉及延时绑定的方面就是针对RequestPort的延时注册。MAF将针对RequestPort的注册抽象成了如下这个IExternalRequestContext接口,它定义了唯一的方法RegisterPort用来注册一个RequestPort对象,并返回一个对应的IExternalRequestSink对象。

internal interface IExternalRequestContext
{
	IExternalRequestSink RegisterPort(RequestPort port);
}

上述的延迟绑定体现在DelayedExternalRequestContext的定义上。这个针对IExternalRequestContext接口的实现,本质是_targetContext属性返回的另一个IExternalRequestContext对象的一个代理,被代理的对象可以在构造函数中指定,也可以通过ApplyPortRegistrationsApplyPortRegistrations方法延后指定。在实现的RegisterPort方法中,它将RequestPort的注册暂存在_requestPorts字典中。注册信息提供的DelayRegisteredSink也是对另一个IExternalRequestSink对象的代理,实现的PostAsync方法会将请求转发给这个被代理的对象。

internal sealed class DelayedExternalRequestContext : IExternalRequestContext
{
	private sealed class DelayRegisteredSink : IExternalRequestSink
	{
		internal IExternalRequestSink? TargetSink { get; set; }
		public ValueTask PostAsync(ExternalRequest request)
            =>TargetSink.PostAsync(request);
	}

	private readonly Dictionary<string, (RequestPort Port, DelayRegisteredSink Sink)> _requestPorts 
        = new Dictionary<string, (RequestPort, DelayRegisteredSink)>();
	private IExternalRequestContext? _targetContext;

	public DelayedExternalRequestContext(IExternalRequestContext? targetContext = null)
	    => _targetContext = targetContext;

	public void ApplyPortRegistrations(IExternalRequestContext targetContext)
	{
		_targetContext = targetContext;
		foreach (var (port, delayRegisteredSink) in _requestPorts.Values)
		{
			if (delayRegisteredSink != null)
			{
				delayRegisteredSink.TargetSink = targetContext.RegisterPort(port);
			}
		}
	}

	public IExternalRequestSink RegisterPort(RequestPort port)
	{
		var delayRegisteredSink = new DelayRegisteredSink
		{
			TargetSink = _targetContext?.RegisterPort(port)
		};
		_requestPorts.Add(port.Id, (port, delayRegisteredSink));
		return delayRegisteredSink;
	}
}

作为基类的Executor定义的内部属性DelayedPortRegistrations返回的正是这个DelayedExternalRequestContext对象。由于调用ExecutorConfigureProtocol方法提供的ProtocolBuilder对象是根据这个DelayedExternalRequestContext对象创建的,当我们注册基于RequestPort的路由处理器时,会将指定的RequestPort对象注册到这个DelayedExternalRequestContext对象中。当Workflow真正被执行的时候,会创建一个真正的IExternalRequestContext对象,并通过调用AttachRequestContext方法将RequetPort注册应用到该对象上。

public abstract class Executor : IIdentified
{	
	private DelayedExternalRequestContext DelayedPortRegistrations { get; } 
        = new DelayedExternalRequestContext();
    internal void AttachRequestContext(IExternalRequestContext externalRequestContext)
        => DelayedPortRegistrations.ApplyPortRegistrations(externalRequestContext);
    internal ExecutorProtocol Protocol => filed ??= ConfigureProtocol(
        new ProtocolBuilder(DelayedPortRegistrations)).Build(Options);
}

由于RequestInfoExecutor总是将外部请求发送给RequestSink属性返回的IExternalRequestSink对象,所有这个对象必须由运行环境提供。对于InProcessExecutionEnvironment来说,对应的Runner会在通过InProcessRunnerContext表示的上下文中执行Workflow,它就是这个能够接纳并处理外部请求的Sink。RequestInfoExecutor在执行的时候,其RequestSink属性需要绑定到这个对象上。从如下所示的PostAsync方法可以看出,正式它将针对RequestPort注册的外部请求转换成了一个RequestInfoEvent事件。

internal sealed class InProcessRunnerContext : IRunnerContext, IExternalRequestSink, ISuperStepJoinContext
{
    public IExternalRequestSink RegisterPort(string executorId, RequestPort port);
    public ValueTask PostAsync(ExternalRequest request)
    {
        ...
        return this.AddEventAsync(new RequestInfoEvent(request));
    }
    public async ValueTask<Executor> EnsureExecutorAsync(string executorId, IStepTracer? tracer, CancellationToken cancellationToken = default)
    {        
        Task<Executor> executorTask = _executors.GetOrAdd(executorId, CreateExecutorAsync);
        async Task<Executor> CreateExecutorAsync(string id)
        {
            var executor = await registration.CreateInstanceAsync(_sessionId).ConfigureAwait(false);
            executor.AttachRequestContext(BindExternalRequestContext(executorId));
            if (executor is RequestInfoExecutor requestInputExecutor)
            {
                requestInputExecutor.AttachRequestSink(this);
            }
            return executor;
        }
        return await executorTask.ConfigureAwait(false);
    }

    public IExternalRequestContext BindExternalRequestContext(string executorId)
        => new BoundExternalRequestContext(this, executorId);

    private sealed class BoundExternalRequestContext(
        InProcessRunnerContext RunnerContext,
        string ExecutorId) : IExternalRequestContext
    {
        public IExternalRequestSink RegisterPort(RequestPort port)=> RunnerContext.RegisterPort(ExecutorId, port);
    }
}

当Workflow流经RequestPortBinding所在的节点时,定义在InProcessRunnerContext中的EnsureExecutorAsync方法被调用来创建或者获取一个RequestInfoExecutor实例。InProcessRunnerContext在这里做了两件事:

  • 把自己包装成一个IExternalRequestContext对象(对应类型为BoundExternalRequestContext),并作为参数调用RequestInfoExecutorAttachRequestContext方法对RequestPort进行延迟注册。此时实施注册的是InProcessRunnerContext自己的RegisterPort方法,它返回的IExternalRequestSink对象就是它自己。通过延迟注册,它拥有了RequestInfoExecutorExecutorIdRequestPort对象之间的映射关系;
  • RequestInfoExecutorAttachRequestSink方法将它的RequestSink属性替换成自己;

到此为止,由于RequestInfoExecutorRequestSink属性已经是具有外部请求接收和处理能力的InProcessRunnerContext对象了,将外部请求转换成RequestInfoEvent事件的问题就迎刃而解了。但是问题指解决了一半,当我们根据ExternalRequest将对应的ExternalResponse创建出来,并作为参数调用StreamingRunSendResponseAsync方法时,响应如何直接抵达对应的RequestInfoExecutor呢?

这个问题不难解决,由于RequestPort的延迟注册让RequestInfoExecutor的ID和RequestPort对象之间有了映射关系,而ExternalResponse自身携带RequestPort的相关信息(PortInfo属性),自然也就能准确定位到对应的RequestInfoExecutor了。

随着人类对生命健康需求的不断增长,新药研发面临着前所未有的挑战。传统的药物研发流程通常耗时长达十年以上,耗资数十亿美元,且最终成功率极低,这在制药界被称为“反摩尔定律”困境。近年来,人工智能技术的飞速发展,特别是深度学习和大数据分析的广泛应用,为新药发现带来了革命性的契机。人工智能能够从海量的化学和生物数据中挖掘潜在规律,显著加速药物靶点发现、先导化合物优化等关键环节。在此背景下,本研究旨在设计并实现一个基于人工智能的新药发现辅助系统,以期为传统药物研发流程提供高效的智能化辅助工具,从而有效缩短研发周期并大幅降低研发成本。本研究以Python作为主要开发语言,深度结合PyTorch和TensorFlow两大主流深度学习框架,并集成RDKit化学信息学工具包,构建了一个功能完善的新药发现辅助系统。系统的核心目标是利用先进的人工智能技术辅助新药分子的设计与活性评估。在研究方法上,本文创新性地提出了一种融合多模态数据的新药发现算法。该算法综合处理分子的多种表示形式,包括一维的SMILES序列、二维的分子图结构以及三维的空间构象数据。通过构建多通道神经网络,系统能够有效提取并融合不同模态的特征,从而全面捕捉分子的理化性质与生物学活性之间的复杂非线性关系。 【课程报告内容】 摘要 第1章 绪论 第2章 相关技术与理论 第3章 系统需求分析 第4章 系统总体设计 第5章 系统详细设计与实现 第6章 系统测试与分析 第7章 总结与展望 参考文献 附件-实现指南
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值