【问题标题】:Running interactive program from within python从 python 中运行交互式程序
【发布时间】:2018-10-30 05:54:08
【问题描述】:

我想实现与this 非常相似的东西。

我的实际目标是在 python 中运行 Rasa。 取自 Rasa 的网站:

Rasa 是一个用于构建对话软件的框架:Messenger/Slack 机器人、Alexa 技能等。我们将在本文档中将其简称为机器人。

它基本上是一个在命令提示符下运行的聊天机器人。这是它在 cmd 上的工作方式:

现在我想从 python 运行 Rasa,以便我可以将它与基于 Django 的网站集成。即我想继续从用户那里获取输入,将其传递给 rasa,rasa 处理文本并给我一个输出,然后我会显示给用户。

我已经尝试过了(现在从 cmd 运行它)

import sys
import subprocess
from threading import Thread
from queue import Queue, Empty  # python 3.x


def enqueue_output(out, queue):
    for line in iter(out.readline, b''):
        queue.put(line)
    out.close()


def getOutput(outQueue):
    outStr = ''
    try:
        while True: #Adds output from the Queue until it is empty
            outStr+=outQueue.get_nowait()
    except Empty:
        return outStr

p = subprocess.Popen('command_to_run_rasa', 
                    stdin=subprocess.PIPE, 
                    stdout=subprocess.PIPE, 
                    stderr=subprocess.PIPE, 
                    shell=False, 
                    universal_newlines=True,
                    )

outQueue = Queue()

outThread = Thread(target=enqueue_output, args=(p.stdout, outQueue))

outThread.daemon = True

outThread.start()

someInput = ""

while someInput != "stop":
    someInput = input("Input: ") # to take input from user
    p.stdin.write(someInput) # passing input to be processed by the rasa command
    p.stdin.flush()
    output = getOutput(outQueue)
    print("Output: " + output + "\n")
    p.stdout.flush()

但它只适用于第一行输出。不适用于连续的输入/输出周期。请参阅下面的输出。

如何让它在多个周期内工作? 我提到了this,我想我从中理解了我的代码中的问题,但我不知道如何解决它。

编辑:我在 Windows 10 上使用 Python 3.6.2(64 位)

【问题讨论】:

    标签: python subprocess python-multithreading rasa-nlu


    【解决方案1】:

    您需要继续与您的子流程进行交互 - 目前,一旦您从您的子流程中选择了输出,当您关闭其 STDOUT 流时,您就已经完成了。

    这是继续用户输入的最基本方法 -> 处理输出循环:

    import subprocess
    import sys
    import time
    
    if __name__ == "__main__":  # a guard from unintended usage
        input_buffer = sys.stdin  # a buffer to get the user input from
        output_buffer = sys.stdout  # a buffer to write rasa's output to
        proc = subprocess.Popen(["path/to/rasa", "arg1", "arg2", "etc."],  # start the process
                                stdin=subprocess.PIPE,  # pipe its STDIN so we can write to it
                                stdout=output_buffer, # pipe directly to the output_buffer
                                universal_newlines=True)
        while True:  # run a main loop
            time.sleep(0.5)  # give some time for `rasa` to forward its STDOUT
            print("Input: ", end="", file=output_buffer, flush=True)  # print the input prompt
            print(input_buffer.readline(), file=proc.stdin, flush=True)  # forward the user input
    

    您可以将input_buffer 替换为来自远程用户的缓冲区,将output_buffer 替换为将数据转发给您的用户的缓冲区,您将获得所需的内容 -子流程将直接从用户 (input_buffer) 获取输入并将其输出打印给用户 (output_buffer)。

    如果您需要在所有这些都在后台运行时执行其他任务,只需在单独的线程中运行 if __name__ == "__main__": 保护下的所有内容,我建议添加一个 try..except 块来获取 KeyboardInterrupt并优雅地退出。

    但是......很快你就会注意到它并不能一直正常工作 - 如果等待 rasa 打印它的 STDOUT 并输入 等待STDIN阶段,输出将开始混合。这个问题比你想象的要复杂得多。主要问题是STDOUTSTDIN(和STDERR)是单独的缓冲区,您无法知道子进程何时实际在其STDIN 上期待某些东西。这意味着如果没有来自子进程的明确指示(例如,您在其STDOUT 上的 Windows CMD 提示符中有 \r\n[path]>),您只能将数据发送到子进程 STDIN 并希望它会被接收。

    根据您的屏幕截图,它并没有真正给出可区分的STDIN 请求提示,因为第一个提示是... :\n,然后它等待STDIN,但是一旦发送命令,它就会列出没有指示其结束 STDOUT 流(技术上只提示 ...\n 但这也将匹配它之前的任何行)。也许你可以聪明一点,逐行阅读STDOUT,然后在每一行上测量自子进程写入它以来已经过去了多少时间,一旦达到不活动阈值,就假设rasa期望输入和提示用户。比如:

    import subprocess
    import sys
    import threading
    
    # we'll be using a separate thread and a timed event to request the user input
    def timed_user_input(timer, wait, buffer_in, buffer_out, buffer_target):
        while True:  # user input loop
            timer.wait(wait)  # wait for the specified time...
            if not timer.is_set():  # if the timer was not stopped/restarted...
                print("Input: ", end="", file=buffer_out, flush=True)  # print the input prompt
                print(buffer_in.readline(), file=buffer_target, flush=True)  # forward the input
            timer.clear()  # reset the 'timer' event
    
    if __name__ == "__main__":  # a guard from unintended usage
        input_buffer = sys.stdin  # a buffer to get the user input from
        output_buffer = sys.stdout  # a buffer to write rasa's output to
        proc = subprocess.Popen(["path/to/rasa", "arg1", "arg2", "etc."],  # start the process
                                stdin=subprocess.PIPE,  # pipe its STDIN so we can write to it
                                stdout=subprocess.PIPE,  # pipe its STDIN so we can process it
                                universal_newlines=True)
        # lets build a timer which will fire off if we don't reset it
        timer = threading.Event()  # a simple Event timer
        input_thread = threading.Thread(target=timed_user_input,
                                        args=(timer,  # pass the timer
                                              1.0,  # prompt after one second
                                              input_buffer, output_buffer, proc.stdin))
        input_thread.daemon = True  # no need to keep the input thread blocking...
        input_thread.start()  # start the timer thread
        # now we'll read the `rasa` STDOUT line by line, forward it to output_buffer and reset
        # the timer each time a new line is encountered
        for line in proc.stdout:
            output_buffer.write(line)  # forward the STDOUT line
            output_buffer.flush()  # flush the output buffer
            timer.set()  # reset the timer
    

    您可以使用类似的技术来检查更复杂的“预期用户输入”模式。有一个名为 pexpect 的完整模块旨在处理此类任务,如果您愿意放弃一些灵活性,我衷心推荐它。

    现在...说了这么多,您知道Rasa 是用 Python 构建的,作为 Python 模块安装并具有 Python API,对吗?既然您已经在使用 Python,为什么当您可以直接从您的 Python 代码运行它时,您为什么要将它称为子进程并处理所有这些 STDOUT/STDIN 恶作剧?只需导入它并直接与之交互,他们甚至有一个非常简单的示例可以完全按照您的要求进行操作:Rasa Core with minimal Python

    【讨论】:

      猜你喜欢
      • 2014-12-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多