在Python中支持CodeProject.AI Server模块中的长时间操作

目录

介绍

开始

创建适配器

长流程支持

代码

创建modulesettings.json文件

安装脚本

创建“CodeProject.AI测试”页(和资源管理器UI)

结论


介绍

CodeProject.AI模块中封装一些出色的AI代码非常简单,因为您的代码执行快速推理然后将结果返回给服务器。对于AI操作时间较长的情况(例如生成式AI),由于超时和用户体验普遍较差,此流程将不起作用。

本文将向您展示如何为CodeProject.AI Server创建一个模块,该模块包装了一些需要很长时间才能完成的代码。我们将只关注为我们的AI代码编写适配器所需的代码,而不是AI代码本身。为此,以及桌面上LLM的有趣示例,请阅读Matthew Dennis的后续文章 为CodeProject.AI Server创建LLM聊天模块

开始

我们假设你已阅读 CodeProject.AI模块创建:Python完整演练。我们将以完全相同的方式创建一个模块,但我们将展示如何处理长时间运行的进程。

首先,像往常一样,克隆 CodeProject.AI Server存储库,并在/src/modules文件夹中为您的模块创建一个新文件夹。我们称之为PythonLongProcess。对于我们这些普通人来说,这是一个简单的名字。

我们还将假设我们有一些想要通过CodeProject.AI Server公开的代码。我们将要包装的惊人代码如下:

import time

cancelled = False

def a_long_process(callback):

    result    = ""
    step      = 0
    cancelled = False

    for i in range(1, 11):
       if cancelled: break
       
       time.sleep(1)
       step   = 1 if not step else step + 1
       result = str(step) if not result else f"{result} {step}"
       callback(result, step)


def cancel_process():
    global cancelled
    cancelled = True

所有代码所做的就是逐步构建一个包含数字1-10的字符串。在每个步骤中,它都会检查进程是否已被取消,并调用回调以允许调用方检查进度。没什么令人兴奋的,但它将是一个很好的演示。

创建适配器

我们希望将这个长进程代码包装在CodeProject.AI Server模块中,因此我们将创建一个适配器、一个modulesettings.json文件、安装脚本和一个测试页。我们将从适配器开始。

我们的适配器将非常简陋。我们不需要从调用方获取值,没有太多的错误检查,也不会记录任何信息。

我们需要创建一个ModuleRunner派生类,并覆盖initializeprocess方法。为了提供对长进程的支持,我们还需要覆盖command_statuscancel_command_task并提供一种实际调用我们正在包装的长进程的方法。正是这最后一部分在模块中提供了长过程支持。

长流程支持

为了允许CodeProject.AI Server模块处理长进程,我们做了三件事:

  1. 向调用方和服务器本身发出信号,表明对方法的调用将导致一个漫长的过程。
  2. 在后台运行长进程
  3. 提供检查其状态并在必要时取消的方法

为此,我们从通常的process方法返回一个Callable,而不是通常包含调用结果的JSON对象。返回Callable向服务器发出信号,表明我们需要在后台运行一个方法。然后,调用方需要轮询模块状态API以检查进度,如果需要,请调用取消任务API以取消长时间运行的进程。

  • 要检查模块的状态,请调用/v1/<moduleid>/get_command_status 
  • 取消对/v1/<moduleid>/cancel_command的API调用的长进程

这些路由会自动添加到每个模块中,不需要在模块设置文件中定义。调用将分别映射到模块command_statuscancel_command_task方法。

代码

这是我们适配器的(大部分)完整列表。注意通常initializeprocess方法,以及从process返回的long_process方法,该方法表示长进程正在启动。

long_process内部,除了调用我们正在包装的代码(a_long_process)并报告结果之外,我们什么都不做。 

command_statuscancel_command_task方法同样简单:返回我们目前拥有的内容,并在请求时取消长操作。

最后一部分是我们的long_process_callback,我们传递给long_process。这将从long_process接收更新,并使我们有机会收集中期结果。

... other imports go here

# Import the method of the module we're wrapping
from long_process import a_long_process, cancel_process

class PythonLongProcess_adapter(ModuleRunner):

    def initialise(self) -> None:
        # Results from the long process
        self.result      = None
        self.step        = 0
        # Process state
        self.cancelled   = False
        self.stop_reason = None

    def process(self, data: RequestData) -> JSON:
        # This is a long process module, so all we need to do here is return the
        # long process method that will be run
        return self.long_process

    def long_process(self, data: RequestData) -> JSON:
        """ This calls the actual long process code and returns the results """
        self.cancelled   = False
        self.stop_reason = None
        self.result      = None
        self.step        = 0

        start_time = time.perf_counter()
        a_long_process(self.long_process_callback)
        inferenceMs : int = int((time.perf_counter() - start_time) * 1000)

        if self.stop_reason is None:
            self.stop_reason = "completed"

        response = {
            "success":     True, 
            "result":      self.result,
            "stop_reason": self.stop_reason,
            "processMs":   inferenceMs,
            "inferenceMs": inferenceMs
        }

        return response

    def command_status(self) -> JSON:
        """ This method will be called regularly during the long process to provide updates """
        return {
            "success": True, 
            "result":  self.result or ""
        }

    def cancel_command_task(self):
        """ This process is called when the client requests the process to stop """
        cancel_process()
        self.stop_reason = "cancelled"
        self.force_shutdown = False  # Tell ModuleRunner we'll shut ourselves down



    def long_process_callback(self, result, step):
        """ We'll provide this method as the callback for the a_long_process() 
            method in long_process.py """
        self.result = result
        self.step   = step


if __name__ == "__main__":
    PythonLongProcess_adapter().start_loop()

创建modulesettings.json文件

同样,请确保已查看Python ModuleSettings文件中的完整演练。我们的modulesettings文件非常基本,有趣的部分是:

  • 我们的适配器(将用于启动模块)的路径long_process_demo_adapter.py
  • 我们将在python3.9下运行
  • 我们将定义一个路由“pythonlongprocess/long-process”,它采用不接受任何输入值的命令“command”并返回字符串“reply”
  • 它可以在所有平台上运行

{
  "Modules": {
 
    "PythonLongProcess": {
      "Name": "Python Long Process Demo",
      "Version": "1.0.0",
 
      "PublishingInfo" : {
         ... 
      },
 
      "LaunchSettings": {
        "FilePath":    "llama_chat_adapter.py",
        "Runtime":     "python3.8",
      },
 
      "EnvironmentVariables": {
         ...
      },
 
      "GpuOptions" : {
         ...
      },
      
      "InstallOptions" : {
        "Platforms": [ "all" ],
        ...
      },
  
      "RouteMaps": [
        {
          "Name": "Long Process",
          "Route": "pythonlongprocess/long-process",
          "Method": "POST",
          "Command": "command",
          "MeshEnabled": false,
          "Description": "Demos a long process.",
          
          "Inputs": [
          ],
          "Outputs": [
            {
              "Name": "success",
              "Type": "Boolean",
              "Description": "True if successful."
            },
            {
              "Name": "reply",
              "Type": "Text",
              "Description": "The reply from the model."
            },
            ...
          ]
        }
      ]
    }
  }
}

从这个片段中删除了相当多的样板,所以请参考源代码来查看完整的Monty

安装脚本

对于我们的示例,我们实际上没有任何安装要做。下载此模块后,服务器将解压缩它,将文件移动到正确的文件夹,然后运行安装脚本,以便我们可以执行设置模块所需的任何操作。我们不需要做任何事情,所以我们将包含空脚本。不包含脚本将向服务器发出不应安装此模块的信号。

@if "%1" NEQ "install" (
    echo This script is only called from ..\..\setup.bat
    @goto:eof
)
call "!sdkScriptsDirPath!\utils.bat" WriteLine "No custom setup steps for this module." "!color_info!"

if [ "$1" != "install" ]; then
    read -t 3 -p "This script is only called from: bash ../../setup.sh"
    echo
    exit 1 
fi
writeLine "No custom setup steps for this module" "$color_info"

创建“CodeProject.AI测试页(和资源管理器UI

我们有我们希望包装并向世界公开的代码,一个用于执行此操作的适配器,一个用于定义如何设置和启动适配器的modulesettings.json文件,以及我们的安装脚本。最后一部分是演示页面,它允许我们测试我们的新模块。

我们的演示页面(explore.html)非常基本:一个用于启动漫长过程的按钮,一个用于取消的按钮,以及一个用于查看结果的输出窗格。

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <title>Python Long Process demo module</title>

    <link id="bootstrapCss" rel="stylesheet" type="text/css" href="http://localhost:32168/assets/bootstrap-dark.min.css">
    <link rel="stylesheet" type="text/css" href="http://localhost:32168/assets/server.css?v=2.6.1.0">
    <script type="text/javascript" src="http://localhost:32168/assets/server.js"></script>
    <script type="text/javascript" src="http://localhost:32168/assets/explorer.js"></script>

    <style>
/* START EXPLORER STYLE */
/* END EXPLORER STYLE */
    </style>

</head>
<body class="dark-mode">
<div class="mx-auto" style="max-width: 800px;">
    <h2 class="mb-3">Python Long Process demo module</h2>
    <form method="post" action="" enctype="multipart/form-data" id="myform">

<!-- START EXPLORER MARKUP -->
        <div class="form-group row g-0">
            <input id="_MID_things" class="form-control btn-success" type="button" value="Start long process"
                   style="width:9rem" onclick="_MID_onLongProcess()"/>
            <input id="_MID_cancel" class="form-control btn-warn" type="button" value="Cancel"
                   style="width:5rem" onclick="_MID_onCancel()"/>
        </div>
<!-- END EXPLORER MARKUP -->
        <div>
            <h2>Results</h2>
            <div id="results" name="results" class="bg-light p-3" style="min-height: 100px;"></div>
        </div>

    </form>

    <script type="text/javascript">
// START EXPLORER SCRIPT

        let _MID_params = null;

        async function _MID_onLongProcess() {

            if (_MID_params) {
                setResultsHtml("Process already running. Cancel first to start a new process");
                return;
            }

            setResultsHtml("Starting long process...");
            let data = await submitRequest('pythonlongprocess/long-process', 'command', null, null);
            if (data) {

                _MID_params = [['commandId', data.commandId], ['moduleId', data.moduleId]];

                let done = false;

                while (!done) {
                    
                    await delay(1000);

                    if (!_MID_params)    // may have been cancelled
                        break;

                    let results = await submitRequest('pythonlongprocess', 'get_command_status',
                                                        null, _MID_params);
                    if (results && results.success) {

                        if (results.commandStatus == "failed") {
                            done = true;
                            setResultsHtml(results?.error || "Unknown error");
                        } 
                        else {
                            let message = results.result;
                            if (results.commandStatus == "completed")
                                done = true;

                            setResultsHtml(message);
                        }
                    }
                    else {
                        done = true;
                        setResultsHtml(results?.error || "No response from server");
                    }
                }

                _MID_params = null;
            };
        }

        async function _MID_onCancel() {
            if (!_MID_params)
                return;
				
			let moduleId = _MID_params[1][1];
            let result = await submitRequest(moduleId, 'cancel_command', null, _MID_params);
            if (result.success) {
                _MID_params = null;
                setResultsHtml("Command stopped");
            }
        }
// END EXPLORER SCRIPT
    </script>
</div>
</body>
</html>

结论

由于服务器的帮助,在CodeProject.AI模块中包装需要很长时间才能执行的代码非常简单。如果您要包装的代码提供了一种定期查询其进度的方法,这将非常有帮助,但即使这样也不是必需的(尽管用户体验会受到一点影响)。

我们使用长进程支持来包装使用稳定扩散的文本到图像模块,并使用Llama大型语言模型在您的桌面上提供ChatGPT功能。除了编写标准CodeProject.AI Server模块之外,唯一的附加功能是将方法添加到适配器中以检查状态并在必要时取消,以及在我们的测试HTML页面中实际调用这些方法的代码。

长进程支持非常适合生成式AI解决方案,但在您希望在低规格硬件上支持AI操作时也很有用。例如,虽然OCR在一台像样的机器上可能需要几分之一秒,但在Raspberry Pi上对大量数据运行相同的文本检测和识别模型可能需要一段时间。通过长进程模块提供该功能可以提供更好的用户体验并避免HTTP超时问题。

https://www.codeproject.com/Articles/5380123/Supporting-long-operations-in-CodeProject-AI-Serve

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值