从 python 中运行交互式程序 [英] Running interactive program from within python

查看:58
本文介绍了从 python 中运行交互式程序的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我想实现与

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

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

导入系统导入子流程从线程导入线程从队列导入队列,空 # python 3.xdef enqueue_output(out, queue):对于行 in iter(out.readline, b''):queue.put(行)关闭()def getOutput(outQueue):outStr = ''尝试:while True: #从队列中添加输出直到它为空outStr+=outQueue.get_nowait()除了空:返回输出Strp = subprocess.Popen('command_to_run_rasa',标准输入=子进程.PIPE,标准输出=子进程.PIPE,stderr=subprocess.PIPE,壳=假,Universal_newlines=真,)出队列 = 队列()outThread = Thread(target=enqueue_output, args=(p.stdout, outQueue))outThread.daemon = TrueoutThread.start()someInput = ""而 someInput != "stop":someInput = input("Input: ") # 接受用户的输入p.stdin.write(someInput) # 传递由 rasa 命令处理的输入p.stdin.flush()输出 = getOutput(outQueue)打印(输出:"+输出+\n")p.stdout.flush()

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

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

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

解决方案

你需要与你的子流程保持交互——目前,一旦你从你的子流程中选择输出,当你关闭它的 时,你几乎就完成了STDOUT 流.

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

导入子流程导入系统导入时间if __name__ == "__main__": # 防止意外使用的保护input_buffer = sys.stdin # 用于获取用户输入的缓冲区output_buffer = sys.stdout # 将 rasa 的输出写入的缓冲区proc = subprocess.Popen(["path/to/rasa", "arg1", "arg2", "etc."], # 启动进程stdin=subprocess.PIPE, # 管道它的 STDIN 以便我们可以写入它stdout=output_buffer, # 管道直接到 output_bufferUniversal_newlines=真)while True: # 运行一个主循环time.sleep(0.5) # 给`rasa`一些时间来转发它的标准输出print("Input: ", end="", file=output_buffer, flush=True) # 打印输入提示print(input_buffer.readline(), file=proc.stdin, flush=True) # 转发用户输入

您可以将 input_buffer 替换为来自远程用户的缓冲区,将 output_buffer 替换为将数据转发给您的用户的缓冲区,然后您基本上会得到你正在寻找的东西 - 子进程将直接从用户那里获取输入(input_buffer)并将其输出打印给用户(output_buffer).

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

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

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

导入子流程导入系统进口螺纹# 我们将使用一个单独的线程和一个定时事件来请求用户输入def timed_user_input(timer, wait, buffer_in, buffer_out, buffer_target):while True: # 用户输入循环timer.wait(wait) # 等待指定的时间...if not timer.is_set(): # 如果定时器没有停止/重启...print("Input: ", end="", file=buffer_out, flush=True) # 打印输入提示print(buffer_in.readline(), file=buffer_target, flush=True) # 转发输入timer.clear() # 重置 'timer' 事件if __name__ == "__main__": # 防止意外使用的保护input_buffer = sys.stdin # 用于获取用户输入的缓冲区output_buffer = sys.stdout # 将 rasa 的输出写入的缓冲区proc = subprocess.Popen(["path/to/rasa", "arg1", "arg2", "etc."], # 启动进程stdin=subprocess.PIPE, # 管道它的 STDIN 以便我们可以写入它stdout=subprocess.PIPE, # 管道它的 STDIN 以便我们可以处理它Universal_newlines=真)# 让我们建立一个计时器,如果我们不重置它就会触发timer = threading.Event() # 一个简单的事件定时器input_thread = threading.Thread(target=timed_user_input,args=(timer, # 传递定时器1.0, # 一秒后提示input_buffer, output_buffer, proc.stdin))input_thread.daemon = True # 无需保持输入线程阻塞...input_thread.start() # 启动定时器线程# 现在我们将逐行读取 `rasa` STDOUT,将其转发到 output_buffer 并重置# 每次遇到新行时的计时器对于 proc.stdout 中的行:output_buffer.write(line) # 转发 STDOUT 行output_buffer.flush() # 刷新输出缓冲区timer.set() # 重置定时器

您可以使用类似的技术来检查更复杂的预期用户输入"模式.有一个名为 pexpect 的整个模块旨在处理这个问题类型的任务,如果你愿意放弃一些灵活性,我全心全意地推荐它.

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

I want to achieve something which is very similar to this.

My actual goal is to run Rasa from within python. Taken from Rasa's site:

Rasa is a framework for building conversational software: Messenger/Slack bots, Alexa skills, etc. We’ll abbreviate this as a bot in this documentation.

It is basically a chatbot which runs in the command prompt. This is how it works on cmd :

Now I want to run Rasa from python so that I can integrate it with my Django-based website. i.e. I want to keep taking inputs from the user, pass it to rasa, rasa processes the text and gives me an output which I show back to the user.

I have tried this (running it from cmd as of now)

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()

But it works fine only for the first line of output. Not for successive input/output cycles. See output below.

How do I get it working for multiple cycles? I've referred to this, and I think I understand the problem in my code from it but I dont know how to solve it.

EDIT: I'm using Python 3.6.2 (64-bit) on Windows 10

解决方案

You need to keep interacting with your subprocess - at the moment once you pick the output from your subprocess you're pretty much done as you close its STDOUT stream.

Here is the most rudimentary way to continue user input -> process output cycle:

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

You can replace input_buffer with a buffer coming from your remote user(s) and output_buffer with a buffer that forwards the data to your user(s) and you'll get essentially what you're looking for - the sub-process will be getting the input directly from the user (input_buffer) and print its output to the user (output_buffer).

If you need to perform other tasks while all this is running in the background, just run everything under the if __name__ == "__main__": guard in a separate thread, and I'd suggest adding a try..except block to pick up KeyboardInterrupt and exit gracefully.

But... soon enough you'll notice that it doesn't exactly work properly all the time - if it takes longer than half a second of wait for rasa to print its STDOUT and enter the wait for STDIN stage, the outputs will start to mix. This problem is considerably more complex than you might expect. The main issue is that STDOUT and STDIN (and STDERR) are separate buffers and you cannot know when a subprocess is actually expecting something on its STDIN. This means that without a clear indication from the subprocess (like you have the \r\n[path]> in Windows CMD prompt on its STDOUT for example) you can only send data to the subprocesses STDIN and hope it will be picked up.

Based on your screenshot, it doesn't really give a distinguishable STDIN request prompt because the first prompt is ... :\n and then it waits for STDIN, but then once the command is sent it lists options without an indication of its end of STDOUT stream (technically making the prompt just ...\n but that would match any line preceding it as well). Maybe you can be clever and read the STDOUT line by line, then on each new line measure how much time has passed since the sub-process wrote to it and once a threshold of inactivity is reached assume that rasa expects input and prompt the user for it. Something like:

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

You can use a similar technique to check for more complex 'expected user input' patterns. There is a whole module called pexpect designed to deal with this type of tasks and I wholeheartedly recommend it if you're willing to give up some flexibility.

Now... all this being said, you are aware that Rasa is built in Python, installs as a Python module and has a Python API, right? Since you're already using Python why would you call it as a subprocess and deal with all this STDOUT/STDIN shenanigans when you can directly run it from your Python code? Just import it and interact with it directly, they even have a very simple example that does exactly what you're trying to do: Rasa Core with minimal Python.

这篇关于从 python 中运行交互式程序的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆