chore: 添加虚拟环境到仓库
- 添加 backend_service/venv 虚拟环境 - 包含所有Python依赖包 - 注意:虚拟环境约393MB,包含12655个文件
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
# Copyright (c) Alibaba, Inc. and its affiliates.
|
||||
|
||||
from .tingwu import TingWu
|
||||
from .tingwu_realtime import TingWuRealtime, TingWuRealtimeCallback
|
||||
|
||||
__all__ = [
|
||||
'TingWu',
|
||||
'TingWuRealtime',
|
||||
'TingWuRealtimeCallback'
|
||||
]
|
||||
@@ -0,0 +1,80 @@
|
||||
from typing import Dict, Any
|
||||
|
||||
from dashscope.api_entities.api_request_factory import _build_api_request
|
||||
from dashscope.api_entities.dashscope_response import DashScopeAPIResponse
|
||||
from dashscope.client.base_api import BaseApi
|
||||
from dashscope.common.error import ModelRequired
|
||||
|
||||
|
||||
class TingWu(BaseApi):
|
||||
"""API for TingWu APP.
|
||||
|
||||
"""
|
||||
|
||||
task = None
|
||||
task_group = None
|
||||
function = None
|
||||
|
||||
@classmethod
|
||||
def call(
|
||||
cls,
|
||||
model: str,
|
||||
user_defined_input: Dict[str, Any],
|
||||
parameters: Dict[str, Any] = None,
|
||||
api_key: str = None,
|
||||
**kwargs
|
||||
) -> DashScopeAPIResponse:
|
||||
"""Call generation model service.
|
||||
|
||||
Args:
|
||||
model (str): The requested model, such as qwen-turbo
|
||||
api_key (str, optional): The api api_key, can be None,
|
||||
if None, will get by default rule(TODO: api key doc).
|
||||
user_defined_input: custom input
|
||||
parameters: custom parameters
|
||||
**kwargs:
|
||||
base_address: base address
|
||||
additional parameters for request
|
||||
|
||||
Raises:
|
||||
InvalidInput: The history and auto_history are mutually exclusive.
|
||||
|
||||
Returns:
|
||||
Union[GenerationResponse,
|
||||
Generator[GenerationResponse, None, None]]: If
|
||||
stream is True, return Generator, otherwise GenerationResponse.
|
||||
"""
|
||||
if model is None or not model:
|
||||
raise ModelRequired('Model is required!')
|
||||
input_config, parameters = cls._build_input_parameters(input_config=user_defined_input,
|
||||
params=parameters,
|
||||
**kwargs)
|
||||
|
||||
request = _build_api_request(
|
||||
model=model,
|
||||
input=input_config,
|
||||
api_key=api_key,
|
||||
task_group=TingWu.task_group,
|
||||
task=TingWu.task,
|
||||
function=TingWu.function,
|
||||
is_service=False,
|
||||
**parameters)
|
||||
response = request.call()
|
||||
|
||||
return response
|
||||
|
||||
@classmethod
|
||||
def _build_input_parameters(cls,
|
||||
input_config,
|
||||
params: Dict[str, Any] = None,
|
||||
**kwargs):
|
||||
parameters = {}
|
||||
if params is not None:
|
||||
parameters = params
|
||||
|
||||
input_param = input_config
|
||||
|
||||
if kwargs.keys() is not None:
|
||||
for key in kwargs.keys():
|
||||
parameters[key] = kwargs[key]
|
||||
return input_param, {**parameters, **kwargs}
|
||||
@@ -0,0 +1,579 @@
|
||||
# Copyright (c) Alibaba, Inc. and its affiliates.
|
||||
|
||||
import json
|
||||
import platform
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
from dataclasses import dataclass, field
|
||||
from queue import Queue
|
||||
import dashscope
|
||||
from dashscope.client.base_api import BaseApi
|
||||
from dashscope.common.error import (InvalidParameter, ModelRequired)
|
||||
import websocket
|
||||
|
||||
from dashscope.common.logging import logger
|
||||
from dashscope.protocol.websocket import ActionType
|
||||
|
||||
|
||||
class TingWuRealtimeCallback:
|
||||
"""An interface that defines callback methods for getting TingWu results.
|
||||
Derive from this class and implement its function to provide your own data.
|
||||
"""
|
||||
|
||||
def on_open(self) -> None:
|
||||
pass
|
||||
|
||||
def on_started(self, task_id: str) -> None:
|
||||
pass
|
||||
|
||||
def on_speech_listen(self, result: dict):
|
||||
pass
|
||||
|
||||
def on_recognize_result(self, result: dict):
|
||||
pass
|
||||
|
||||
def on_ai_result(self, result: dict):
|
||||
pass
|
||||
|
||||
def on_stopped(self) -> None:
|
||||
pass
|
||||
|
||||
def on_error(self, error_code: str, error_msg: str) -> None:
|
||||
pass
|
||||
|
||||
def on_close(self, close_status_code, close_msg):
|
||||
"""
|
||||
callback when websocket connection is closed
|
||||
|
||||
:param close_status_code
|
||||
:param close_msg
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class TingWuRealtime(BaseApi):
|
||||
"""TingWuRealtime interface.
|
||||
|
||||
Args:
|
||||
model (str): The requested model_id.
|
||||
callback (TingWuRealtimeCallback): A callback that returns
|
||||
speech recognition results.
|
||||
app_id (str): The dashscope tingwu app id.
|
||||
format (str): The input audio format for TingWu request.
|
||||
sample_rate (int): The input audio sample rate.
|
||||
terminology (str): The correct instruction set id.
|
||||
workspace (str): The dashscope workspace id.
|
||||
|
||||
**kwargs:
|
||||
max_end_silence (int): The maximum end silence time.
|
||||
other_params (dict, `optional`): Other parameters.
|
||||
|
||||
Raises:
|
||||
InputRequired: Input is required.
|
||||
"""
|
||||
|
||||
SILENCE_TIMEOUT_S = 60
|
||||
|
||||
def __init__(self,
|
||||
model: str,
|
||||
callback: TingWuRealtimeCallback,
|
||||
audio_format: str = "pcm",
|
||||
sample_rate: int = 16000,
|
||||
max_end_silence: int = None,
|
||||
app_id: str = None,
|
||||
terminology: str = None,
|
||||
workspace: str = None,
|
||||
api_key: str = None,
|
||||
base_address: str = None,
|
||||
data_id: str = None,
|
||||
**kwargs):
|
||||
if api_key is None:
|
||||
self.api_key = dashscope.api_key
|
||||
else:
|
||||
self.api_key = api_key
|
||||
if base_address is None:
|
||||
self.base_address = dashscope.base_websocket_api_url
|
||||
else:
|
||||
self.base_address = base_address
|
||||
|
||||
if model is None:
|
||||
raise ModelRequired('Model is required!')
|
||||
|
||||
self.data_id = data_id
|
||||
self.max_end_silence = max_end_silence
|
||||
self.model = model
|
||||
self.audio_format = audio_format
|
||||
self.app_id = app_id
|
||||
self.terminology = terminology
|
||||
self.sample_rate = sample_rate
|
||||
# continuous recognition with start() or once recognition with call()
|
||||
self._recognition_once = False
|
||||
self._callback = callback
|
||||
self._running = False
|
||||
self._stream_data = Queue()
|
||||
self._worker = None
|
||||
self._silence_timer = None
|
||||
self._kwargs = kwargs
|
||||
self._workspace = workspace
|
||||
self._start_stream_timestamp = -1
|
||||
self._first_package_timestamp = -1
|
||||
self._stop_stream_timestamp = -1
|
||||
self._on_complete_timestamp = -1
|
||||
self.request_id_confirmed = False
|
||||
self.last_request_id = uuid.uuid4().hex
|
||||
self.request = _Request()
|
||||
self.response = _TingWuResponse(self._callback, self.close) # 传递 self.close 作为回调
|
||||
|
||||
def _on_message(self, ws, message):
|
||||
logger.debug(f"<<<<<<< Received message: {message}")
|
||||
if isinstance(message, str):
|
||||
self.response.handle_text_response(message)
|
||||
elif isinstance(message, (bytes, bytearray)):
|
||||
self.response.handle_binary_response(message)
|
||||
|
||||
def _on_error(self, ws, error):
|
||||
logger.error(f"Error: {error}")
|
||||
if self._callback:
|
||||
error_code = "" # 默认错误码
|
||||
if "connection" in str(error).lower():
|
||||
error_code = "1001" # 连接错误
|
||||
elif "timeout" in str(error).lower():
|
||||
error_code = "1002" # 超时错误
|
||||
elif "authentication" in str(error).lower():
|
||||
error_code = "1003" # 认证错误
|
||||
self._callback.on_error(error_code=error_code, error_msg=str(error))
|
||||
|
||||
def _on_close(self, ws, close_status_code, close_msg):
|
||||
try:
|
||||
logger.debug(
|
||||
"WebSocket connection closed with status {} and message {}".format(close_status_code, close_msg))
|
||||
if close_status_code is None:
|
||||
close_status_code = 1000
|
||||
if close_msg is None:
|
||||
close_msg = "websocket is closed"
|
||||
self._callback.on_close(close_status_code, close_msg)
|
||||
except Exception as e:
|
||||
logger.error(f"Error: {e}")
|
||||
|
||||
def _on_open(self, ws):
|
||||
self._callback.on_open()
|
||||
self._running = True
|
||||
|
||||
# def _on_pong(self):
|
||||
# logger.debug("on pong")
|
||||
|
||||
def start(self, **kwargs):
|
||||
"""
|
||||
interface for starting TingWu connection
|
||||
"""
|
||||
assert self._callback is not None, 'Please set the callback to get the TingWu result.' # noqa E501
|
||||
|
||||
if self._running:
|
||||
raise InvalidParameter('TingWu client has started.')
|
||||
|
||||
# self._start_stream_timestamp = -1
|
||||
# self._first_package_timestamp = -1
|
||||
# self._stop_stream_timestamp = -1
|
||||
# self._on_complete_timestamp = -1
|
||||
if self._kwargs is not None and len(self._kwargs) != 0:
|
||||
self._kwargs.update(**kwargs)
|
||||
|
||||
self._connect(self.api_key)
|
||||
logger.debug("connected with server.")
|
||||
self._send_start_request()
|
||||
|
||||
def send_audio_data(self, speech_data: bytes):
|
||||
"""send audio data to server"""
|
||||
if self._running:
|
||||
self.__send_binary_frame(speech_data)
|
||||
|
||||
def stop(self):
|
||||
if self.ws is None or not self.ws.sock or not self.ws.sock.connected:
|
||||
self._callback.on_close(1001, "websocket is not connected")
|
||||
return
|
||||
_send_speech_json = self.request.generate_stop_request("stop")
|
||||
self._send_text_frame(_send_speech_json)
|
||||
|
||||
"""inner class"""
|
||||
|
||||
def _send_start_request(self):
|
||||
"""send start request"""
|
||||
_start_json = self.request.generate_start_request(
|
||||
workspace_id=self._workspace,
|
||||
direction_name="start",
|
||||
app_id=self.app_id,
|
||||
model=self.model,
|
||||
audio_format=self.audio_format,
|
||||
sample_rate=self.sample_rate,
|
||||
terminology=self.terminology,
|
||||
max_end_silence=self.max_end_silence,
|
||||
data_id=self.data_id,
|
||||
**self._kwargs
|
||||
)
|
||||
# send start request
|
||||
self._send_text_frame(_start_json)
|
||||
|
||||
def _run_forever(self):
|
||||
self.ws.run_forever(ping_interval=5, ping_timeout=4)
|
||||
|
||||
def _connect(self, api_key: str):
|
||||
"""init websocket connection"""
|
||||
self.ws = websocket.WebSocketApp(self.base_address, header=self.request.get_websocket_header(api_key),
|
||||
on_open=self._on_open,
|
||||
on_message=self._on_message,
|
||||
on_error=self._on_error,
|
||||
on_close=self._on_close)
|
||||
self.thread = threading.Thread(target=self._run_forever)
|
||||
# 统一心跳机制配置
|
||||
self.ws.ping_interval = 5
|
||||
self.ws.ping_timeout = 4
|
||||
self.thread.daemon = True
|
||||
self.thread.start()
|
||||
|
||||
self._wait_for_connection()
|
||||
|
||||
def close(self):
|
||||
if self.ws is None or not self.ws.sock or not self.ws.sock.connected:
|
||||
return
|
||||
self.ws.close()
|
||||
|
||||
def _wait_for_connection(self):
|
||||
"""wait for connection using event instead of busy waiting"""
|
||||
timeout = 5
|
||||
start_time = time.time()
|
||||
while not (self.ws.sock and self.ws.sock.connected) and (time.time() - start_time) < timeout:
|
||||
time.sleep(0.1) # 短暂休眠,避免密集轮询
|
||||
|
||||
def _send_text_frame(self, text: str):
|
||||
# 避免在日志中记录敏感信息,如API密钥等
|
||||
# 只记录非敏感信息
|
||||
if '"Authorization"' not in text:
|
||||
logger.info('>>>>>> send text frame : %s' % text)
|
||||
else:
|
||||
logger.info('>>>>>> send text frame with authorization header')
|
||||
self.ws.send(text, websocket.ABNF.OPCODE_TEXT)
|
||||
|
||||
def __send_binary_frame(self, binary: bytes):
|
||||
# _log.info('send binary frame length: %d' % len(binary))
|
||||
self.ws.send(binary, websocket.ABNF.OPCODE_BINARY)
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.cleanup()
|
||||
return False
|
||||
|
||||
def cleanup(self):
|
||||
"""cleanup resources"""
|
||||
try:
|
||||
if self.ws:
|
||||
self.ws.close()
|
||||
if self.thread and self.thread.is_alive():
|
||||
# 设置标志位通知线程退出
|
||||
self.thread.join(timeout=2)
|
||||
# 清除引用
|
||||
self.ws = None
|
||||
self.thread = None
|
||||
self._callback = None
|
||||
self.response = None
|
||||
except Exception as e:
|
||||
logger.error(f"Error in cleanup: {e}")
|
||||
|
||||
def send_audio_frame(self, buffer: bytes):
|
||||
"""Push audio to server
|
||||
|
||||
Raises:
|
||||
InvalidParameter: Cannot send data to an uninitiated recognition.
|
||||
"""
|
||||
if self._running is False:
|
||||
raise InvalidParameter('TingWu client has stopped.')
|
||||
|
||||
if self._start_stream_timestamp < 0:
|
||||
self._start_stream_timestamp = time.time() * 1000
|
||||
logger.debug('send_audio_frame: {}'.format(len(buffer)))
|
||||
self.__send_binary_frame(buffer)
|
||||
|
||||
|
||||
class _Request:
|
||||
def __init__(self):
|
||||
# websocket header
|
||||
self.ws_headers = None
|
||||
# request body for voice chat
|
||||
self.header = None
|
||||
self.payload = None
|
||||
# params
|
||||
self.task_id = None
|
||||
self.app_id = None
|
||||
self.workspace_id = None
|
||||
|
||||
def get_websocket_header(self, api_key):
|
||||
ua = 'dashscope/%s; python/%s; platform/%s; processor/%s' % (
|
||||
'1.18.0', # dashscope version
|
||||
platform.python_version(),
|
||||
platform.platform(),
|
||||
platform.processor(),
|
||||
)
|
||||
self.ws_headers = {
|
||||
"User-Agent": ua,
|
||||
"Authorization": f"bearer {api_key}",
|
||||
"Accept": "application/json"
|
||||
}
|
||||
logger.info('websocket header: {}'.format(self.ws_headers))
|
||||
return self.ws_headers
|
||||
|
||||
def generate_start_request(self, direction_name: str,
|
||||
app_id: str,
|
||||
model: str = None,
|
||||
workspace_id: str = None,
|
||||
audio_format: str = None,
|
||||
sample_rate: int = None,
|
||||
terminology: str = None,
|
||||
max_end_silence: int = None,
|
||||
data_id: str = None,
|
||||
**kwargs
|
||||
) -> str:
|
||||
"""
|
||||
build start request.
|
||||
:param app_id: web console app id
|
||||
:param direction_name:
|
||||
:param workspace_id: web console workspace id
|
||||
:param model: model name
|
||||
:param audio_format: audio format
|
||||
:param sample_rate: sample rate
|
||||
:param terminology:
|
||||
:param max_end_silence:
|
||||
:param data_id:
|
||||
:return:
|
||||
Args:
|
||||
:
|
||||
"""
|
||||
self._get_dash_request_header(ActionType.START)
|
||||
parameters = self._get_start_parameters(audio_format=audio_format, sample_rate=sample_rate,
|
||||
max_end_silence=max_end_silence,
|
||||
terminology=terminology,
|
||||
**kwargs)
|
||||
self._get_dash_request_payload(direction_name=direction_name, app_id=app_id, workspace_id=workspace_id,
|
||||
model=model,
|
||||
data_id=data_id,
|
||||
request_params=parameters)
|
||||
|
||||
cmd = {
|
||||
"header": self.header,
|
||||
"payload": self.payload
|
||||
}
|
||||
return json.dumps(cmd)
|
||||
|
||||
@staticmethod
|
||||
def _get_start_parameters(audio_format: str = None,
|
||||
sample_rate: int = None,
|
||||
terminology: str = None,
|
||||
max_end_silence: int = None,
|
||||
**kwargs):
|
||||
"""
|
||||
build start request parameters inner.
|
||||
:param kwargs: parameters
|
||||
:return
|
||||
"""
|
||||
parameters = {}
|
||||
if audio_format is not None:
|
||||
parameters['format'] = audio_format
|
||||
if sample_rate is not None:
|
||||
parameters['sampleRate'] = sample_rate
|
||||
if terminology is not None:
|
||||
parameters['terminology'] = terminology
|
||||
if max_end_silence is not None:
|
||||
parameters['maxEndSilence'] = max_end_silence
|
||||
if kwargs is not None and len(kwargs) != 0:
|
||||
parameters.update(kwargs)
|
||||
return parameters
|
||||
|
||||
def generate_stop_request(self, direction_name: str) -> str:
|
||||
"""
|
||||
build stop request.
|
||||
:param direction_name
|
||||
:return
|
||||
"""
|
||||
self._get_dash_request_header(ActionType.FINISHED)
|
||||
self._get_dash_request_payload(direction_name, self.app_id)
|
||||
|
||||
cmd = {
|
||||
"header": self.header,
|
||||
"payload": self.payload
|
||||
}
|
||||
return json.dumps(cmd)
|
||||
|
||||
def _get_dash_request_header(self, action: str):
|
||||
"""
|
||||
:param action: ActionType :run-task, continue-task, finish-task
|
||||
"""
|
||||
if self.task_id is None:
|
||||
self.task_id = get_random_uuid()
|
||||
self.header = DashHeader(action=action, task_id=self.task_id).to_dict()
|
||||
|
||||
def _get_dash_request_payload(self, direction_name: str,
|
||||
app_id: str,
|
||||
workspace_id: str = None,
|
||||
custom_input=None,
|
||||
model: str = None,
|
||||
data_id: str = None,
|
||||
request_params=None,
|
||||
):
|
||||
"""
|
||||
build start request payload inner.
|
||||
:param direction_name: inner direction name
|
||||
:param app_id: web console app id
|
||||
:param request_params: start direction body parameters
|
||||
:param custom_input: user custom input
|
||||
:param data_id: data id
|
||||
:param model: model name
|
||||
"""
|
||||
if custom_input is not None:
|
||||
input = custom_input
|
||||
else:
|
||||
input = RequestBodyInput(
|
||||
workspace_id=workspace_id,
|
||||
app_id=app_id,
|
||||
directive=direction_name,
|
||||
data_id=data_id
|
||||
)
|
||||
|
||||
self.payload = DashPayload(
|
||||
model=model,
|
||||
input=input.to_dict(),
|
||||
parameters=request_params
|
||||
).to_dict()
|
||||
|
||||
|
||||
class _TingWuResponse:
|
||||
def __init__(self, callback: TingWuRealtimeCallback, close_callback=None):
|
||||
super().__init__()
|
||||
self.task_id = None # 对话ID.
|
||||
self._callback = callback
|
||||
self._close_callback = close_callback # 保存关闭回调函数
|
||||
|
||||
def handle_text_response(self, response_json: str):
|
||||
"""
|
||||
handle text response.
|
||||
:param response_json: json format response from server
|
||||
"""
|
||||
logger.info("<<<<<< server response: %s" % response_json)
|
||||
try:
|
||||
# try to parse response as json
|
||||
json_data = json.loads(response_json)
|
||||
header = json_data.get('header', {})
|
||||
if header.get('event') == 'task-failed':
|
||||
logger.error('Server returned invalid message: %s' % response_json)
|
||||
if self._callback:
|
||||
self._callback.on_error(error_code=header.get('error_code'),
|
||||
error_msg=header.get('error_message'))
|
||||
return
|
||||
if header.get('event') == "task-started":
|
||||
self._handle_started(header.get('task_id'))
|
||||
return
|
||||
|
||||
payload = json_data.get('payload', {})
|
||||
output = payload.get('output', {})
|
||||
if output is not None:
|
||||
action = output.get('action')
|
||||
logger.info("Server response action: %s" % action)
|
||||
self._handle_tingwu_agent_text_response(action=action, response_json=json_data)
|
||||
|
||||
except json.JSONDecodeError:
|
||||
logger.error("Failed to parse message as JSON.")
|
||||
|
||||
def handle_binary_response(self, response_binary: bytes):
|
||||
"""
|
||||
handle binary response.
|
||||
:param response_binary: server response binary。
|
||||
"""
|
||||
logger.info("<<<<<< server response binary length: %d" % len(response_binary))
|
||||
|
||||
def _handle_tingwu_agent_text_response(self, action: str, response_json: dict):
|
||||
payload = response_json.get('payload', {})
|
||||
output = payload.get('output', {})
|
||||
if action == "task-failed":
|
||||
self._callback.on_error(error_code=output.get('errorCode'),
|
||||
error_msg=output.get('errorMessage'))
|
||||
elif action == "speech-listen":
|
||||
self._callback.on_speech_listen(response_json)
|
||||
elif action == "recognize-result":
|
||||
self._callback.on_recognize_result(response_json)
|
||||
elif action == "ai-result":
|
||||
self._callback.on_ai_result(response_json)
|
||||
elif action == "speech-end": # ai-result事件永远会先于speech-end事件
|
||||
self._callback.on_stopped()
|
||||
if self._close_callback is not None:
|
||||
self._close_callback()
|
||||
else:
|
||||
logger.info("Unknown response name:" + action)
|
||||
|
||||
def _handle_started(self, task_id: str):
|
||||
self.task_id = task_id
|
||||
self._callback.on_started(self.task_id)
|
||||
|
||||
|
||||
def get_random_uuid() -> str:
|
||||
"""generate random uuid."""
|
||||
return uuid.uuid4().hex
|
||||
|
||||
|
||||
@dataclass
|
||||
class RequestBodyInput():
|
||||
app_id: str
|
||||
directive: str
|
||||
data_id: str = field(default=None)
|
||||
workspace_id: str = field(default=None)
|
||||
|
||||
def to_dict(self):
|
||||
body_input = {
|
||||
"appId": self.app_id,
|
||||
"directive": self.directive,
|
||||
}
|
||||
if self.workspace_id is not None:
|
||||
body_input["workspace_id"] = self.workspace_id
|
||||
if self.data_id is not None:
|
||||
body_input["dataId"] = self.data_id
|
||||
return body_input
|
||||
|
||||
|
||||
@dataclass
|
||||
class DashHeader:
|
||||
action: str
|
||||
task_id: str = field(default=get_random_uuid())
|
||||
streaming: str = field(default="duplex") # 默认为 duplex
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"action": self.action,
|
||||
"task_id": self.task_id,
|
||||
"request_id": self.task_id,
|
||||
"streaming": self.streaming
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class DashPayload:
|
||||
task_group: str = field(default="aigc")
|
||||
function: str = field(default="generation")
|
||||
model: str = field(default="")
|
||||
task: str = field(default="multimodal-generation")
|
||||
parameters: dict = field(default=None)
|
||||
input: dict = field(default=None)
|
||||
|
||||
def to_dict(self):
|
||||
payload = {
|
||||
"task_group": self.task_group,
|
||||
"function": self.function,
|
||||
"model": self.model,
|
||||
"task": self.task,
|
||||
}
|
||||
|
||||
if self.parameters is not None:
|
||||
payload["parameters"] = self.parameters
|
||||
|
||||
if self.input is not None:
|
||||
payload["input"] = self.input
|
||||
|
||||
return payload
|
||||
Reference in New Issue
Block a user