mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-10 21:10:59 +00:00
feat: Refactor pending form handling to isolate by bot and pipeline
This commit is contained in:
@@ -22,11 +22,11 @@ from langbot.libs.dify_service_api.v1 import client, errors
|
|||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
|
|
||||||
# Module-level store for paused-workflow form state, keyed by session key
|
# Module-level store for paused-workflow form state. The key isolates the bot,
|
||||||
# (launcher_type_value + "_" + launcher_id). Each session holds an
|
# pipeline, adapter, and launcher; each value holds an insertion-ordered map of
|
||||||
# insertion-ordered dict of form_token -> form_data, allowing multiple
|
# form_token -> form_data so one conversation can pause multiple workflows.
|
||||||
# Dify workflows to be paused simultaneously for the same session.
|
PendingFormKey = tuple[str, str, str, str, str]
|
||||||
_PENDING_FORMS: dict[str, 'OrderedDict[str, dict[str, typing.Any]]'] = {}
|
_PENDING_FORMS: dict[PendingFormKey, 'OrderedDict[str, dict[str, typing.Any]]'] = {}
|
||||||
_PENDING_FORM_DEFAULT_TTL = 30 * 60 # 30 minutes safety cap
|
_PENDING_FORM_DEFAULT_TTL = 30 * 60 # 30 minutes safety cap
|
||||||
_STREAM_FORM_PLACEHOLDER = '\u200b'
|
_STREAM_FORM_PLACEHOLDER = '\u200b'
|
||||||
|
|
||||||
@@ -43,10 +43,23 @@ def _merge_stream_text(accumulated: str, incoming: typing.Any) -> str:
|
|||||||
return accumulated + incoming_text
|
return accumulated + incoming_text
|
||||||
|
|
||||||
|
|
||||||
def _session_key_from_query(query: pipeline_query.Query) -> str:
|
def _dify_user_from_query(query: pipeline_query.Query) -> str:
|
||||||
return f'{query.session.launcher_type.value}_{query.session.launcher_id}'
|
return f'{query.session.launcher_type.value}_{query.session.launcher_id}'
|
||||||
|
|
||||||
|
|
||||||
|
def _session_key_from_query(query: pipeline_query.Query) -> PendingFormKey:
|
||||||
|
"""Build a process-local pending-form key isolated by bot and pipeline."""
|
||||||
|
adapter = getattr(query, 'adapter', None)
|
||||||
|
adapter_type = f'{type(adapter).__module__}.{type(adapter).__qualname__}'
|
||||||
|
return (
|
||||||
|
str(getattr(query, 'bot_uuid', '') or ''),
|
||||||
|
str(getattr(query, 'pipeline_uuid', '') or ''),
|
||||||
|
adapter_type,
|
||||||
|
str(query.session.launcher_type.value),
|
||||||
|
str(query.session.launcher_id),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _prune_pending_forms(now: float | None = None) -> None:
|
def _prune_pending_forms(now: float | None = None) -> None:
|
||||||
if now is None:
|
if now is None:
|
||||||
now = time.time()
|
now = time.time()
|
||||||
@@ -59,7 +72,7 @@ def _prune_pending_forms(now: float | None = None) -> None:
|
|||||||
_PENDING_FORMS.pop(session_key, None)
|
_PENDING_FORMS.pop(session_key, None)
|
||||||
|
|
||||||
|
|
||||||
def _set_pending_form(session_key: str, form_data: dict[str, typing.Any]) -> None:
|
def _set_pending_form(session_key: PendingFormKey, form_data: dict[str, typing.Any]) -> None:
|
||||||
_prune_pending_forms()
|
_prune_pending_forms()
|
||||||
stored = dict(form_data)
|
stored = dict(form_data)
|
||||||
expiration_time = stored.get('expiration_time')
|
expiration_time = stored.get('expiration_time')
|
||||||
@@ -75,7 +88,7 @@ def _set_pending_form(session_key: str, form_data: dict[str, typing.Any]) -> Non
|
|||||||
forms[form_token] = stored
|
forms[form_token] = stored
|
||||||
|
|
||||||
|
|
||||||
def _get_pending_form_by_token(session_key: str, form_token: str) -> dict[str, typing.Any] | None:
|
def _get_pending_form_by_token(session_key: PendingFormKey, form_token: str) -> dict[str, typing.Any] | None:
|
||||||
_prune_pending_forms()
|
_prune_pending_forms()
|
||||||
forms = _PENDING_FORMS.get(session_key)
|
forms = _PENDING_FORMS.get(session_key)
|
||||||
if not forms or not form_token:
|
if not forms or not form_token:
|
||||||
@@ -83,7 +96,7 @@ def _get_pending_form_by_token(session_key: str, form_token: str) -> dict[str, t
|
|||||||
return forms.get(form_token)
|
return forms.get(form_token)
|
||||||
|
|
||||||
|
|
||||||
def _get_pending_form_by_w_suffix(session_key: str, w_suffix: str) -> dict[str, typing.Any] | None:
|
def _get_pending_form_by_w_suffix(session_key: PendingFormKey, w_suffix: str) -> dict[str, typing.Any] | None:
|
||||||
"""Look up a pending form whose workflow_run_id ends with the given suffix.
|
"""Look up a pending form whose workflow_run_id ends with the given suffix.
|
||||||
|
|
||||||
Used by adapters (e.g. Telegram) whose callback payload is too small to
|
Used by adapters (e.g. Telegram) whose callback payload is too small to
|
||||||
@@ -100,7 +113,7 @@ def _get_pending_form_by_w_suffix(session_key: str, w_suffix: str) -> dict[str,
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _get_latest_pending_form(session_key: str) -> dict[str, typing.Any] | None:
|
def _get_latest_pending_form(session_key: PendingFormKey) -> dict[str, typing.Any] | None:
|
||||||
_prune_pending_forms()
|
_prune_pending_forms()
|
||||||
forms = _PENDING_FORMS.get(session_key)
|
forms = _PENDING_FORMS.get(session_key)
|
||||||
if not forms:
|
if not forms:
|
||||||
@@ -108,7 +121,7 @@ def _get_latest_pending_form(session_key: str) -> dict[str, typing.Any] | None:
|
|||||||
return forms[next(reversed(forms))]
|
return forms[next(reversed(forms))]
|
||||||
|
|
||||||
|
|
||||||
def _iter_pending_forms(session_key: str) -> typing.Iterator[dict[str, typing.Any]]:
|
def _iter_pending_forms(session_key: PendingFormKey) -> typing.Iterator[dict[str, typing.Any]]:
|
||||||
"""Iterate pending forms for a session, newest-first."""
|
"""Iterate pending forms for a session, newest-first."""
|
||||||
_prune_pending_forms()
|
_prune_pending_forms()
|
||||||
forms = _PENDING_FORMS.get(session_key)
|
forms = _PENDING_FORMS.get(session_key)
|
||||||
@@ -118,7 +131,7 @@ def _iter_pending_forms(session_key: str) -> typing.Iterator[dict[str, typing.An
|
|||||||
yield forms[token]
|
yield forms[token]
|
||||||
|
|
||||||
|
|
||||||
def _clear_pending_form(session_key: str, form_token: str | None = None) -> None:
|
def _clear_pending_form(session_key: PendingFormKey, form_token: str | None = None) -> None:
|
||||||
"""Clear one specific pending form (by token) or all forms for the session."""
|
"""Clear one specific pending form (by token) or all forms for the session."""
|
||||||
forms = _PENDING_FORMS.get(session_key)
|
forms = _PENDING_FORMS.get(session_key)
|
||||||
if not forms:
|
if not forms:
|
||||||
@@ -683,7 +696,7 @@ class DifyServiceAPIRunner(runner.RequestRunner):
|
|||||||
"""
|
"""
|
||||||
plain_text = ''
|
plain_text = ''
|
||||||
upload_files: list[dict] = []
|
upload_files: list[dict] = []
|
||||||
user_tag = f'{query.session.launcher_type.value}_{query.session.launcher_id}'
|
user_tag = _dify_user_from_query(query)
|
||||||
|
|
||||||
async def upload_file_bytes(file_name: str, file_bytes: bytes, content_type: str) -> str:
|
async def upload_file_bytes(file_name: str, file_bytes: bytes, content_type: str) -> str:
|
||||||
file_name = file_name or 'file'
|
file_name = file_name or 'file'
|
||||||
@@ -823,7 +836,7 @@ class DifyServiceAPIRunner(runner.RequestRunner):
|
|||||||
|
|
||||||
values = dict(pending_form.get('inputs') or {})
|
values = dict(pending_form.get('inputs') or {})
|
||||||
keyed_values = _extract_key_value_inputs(user_text)
|
keyed_values = _extract_key_value_inputs(user_text)
|
||||||
user = pending_form.get('user') or _session_key_from_query(query)
|
user = pending_form.get('user') or _dify_user_from_query(query)
|
||||||
current_field_name = str(pending_form.get('current_input_field') or '').strip()
|
current_field_name = str(pending_form.get('current_input_field') or '').strip()
|
||||||
|
|
||||||
file_fields = [field for field in input_defs if _field_type(field) in {'file', 'file-list'}]
|
file_fields = [field for field in input_defs if _field_type(field) in {'file', 'file-list'}]
|
||||||
@@ -933,7 +946,7 @@ class DifyServiceAPIRunner(runner.RequestRunner):
|
|||||||
form_action,
|
form_action,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
async for msg in self._submit_workflow_form_blocking(form_action):
|
async for msg in self._submit_workflow_form_blocking(form_action, session_key):
|
||||||
yield msg
|
yield msg
|
||||||
_clear_pending_form(session_key, form_action.get('form_token') or None)
|
_clear_pending_form(session_key, form_action.get('form_token') or None)
|
||||||
return
|
return
|
||||||
@@ -965,7 +978,7 @@ class DifyServiceAPIRunner(runner.RequestRunner):
|
|||||||
async for chunk in self.dify_client.chat_messages(
|
async for chunk in self.dify_client.chat_messages(
|
||||||
inputs=inputs,
|
inputs=inputs,
|
||||||
query=plain_text,
|
query=plain_text,
|
||||||
user=f'{query.session.launcher_type.value}_{query.session.launcher_id}',
|
user=_dify_user_from_query(query),
|
||||||
conversation_id=cov_id,
|
conversation_id=cov_id,
|
||||||
files=files,
|
files=files,
|
||||||
timeout=120,
|
timeout=120,
|
||||||
@@ -982,7 +995,7 @@ class DifyServiceAPIRunner(runner.RequestRunner):
|
|||||||
for reason in reasons:
|
for reason in reasons:
|
||||||
if reason.get('TYPE') != 'human_input_required':
|
if reason.get('TYPE') != 'human_input_required':
|
||||||
continue
|
continue
|
||||||
user = f'{query.session.launcher_type.value}_{query.session.launcher_id}'
|
user = _dify_user_from_query(query)
|
||||||
form_snapshot, raw_form_content, input_defs, _ = _extract_form_snapshot(
|
form_snapshot, raw_form_content, input_defs, _ = _extract_form_snapshot(
|
||||||
workflow_run_id,
|
workflow_run_id,
|
||||||
reason,
|
reason,
|
||||||
@@ -1063,7 +1076,7 @@ class DifyServiceAPIRunner(runner.RequestRunner):
|
|||||||
async for chunk in self.dify_client.chat_messages(
|
async for chunk in self.dify_client.chat_messages(
|
||||||
inputs=inputs,
|
inputs=inputs,
|
||||||
query=plain_text,
|
query=plain_text,
|
||||||
user=f'{query.session.launcher_type.value}_{query.session.launcher_id}',
|
user=_dify_user_from_query(query),
|
||||||
response_mode='streaming',
|
response_mode='streaming',
|
||||||
conversation_id=cov_id,
|
conversation_id=cov_id,
|
||||||
files=files,
|
files=files,
|
||||||
@@ -1131,7 +1144,7 @@ class DifyServiceAPIRunner(runner.RequestRunner):
|
|||||||
query.session.using_conversation.uuid = chunk['conversation_id']
|
query.session.using_conversation.uuid = chunk['conversation_id']
|
||||||
|
|
||||||
async def _submit_workflow_form_blocking(
|
async def _submit_workflow_form_blocking(
|
||||||
self, form_action: dict
|
self, form_action: dict, session_key: PendingFormKey
|
||||||
) -> typing.AsyncGenerator[provider_message.Message, None]:
|
) -> typing.AsyncGenerator[provider_message.Message, None]:
|
||||||
"""Submit human input to resume a paused Dify workflow (non-streaming)."""
|
"""Submit human input to resume a paused Dify workflow (non-streaming)."""
|
||||||
|
|
||||||
@@ -1206,7 +1219,7 @@ class DifyServiceAPIRunner(runner.RequestRunner):
|
|||||||
actions = form_snapshot.get('actions', [])
|
actions = form_snapshot.get('actions', [])
|
||||||
paused_node_title = form_snapshot.get('node_title', '')
|
paused_node_title = form_snapshot.get('node_title', '')
|
||||||
|
|
||||||
_set_pending_form(user, form_snapshot)
|
_set_pending_form(session_key, form_snapshot)
|
||||||
|
|
||||||
display_text = _format_human_input_text(
|
display_text = _format_human_input_text(
|
||||||
paused_node_title,
|
paused_node_title,
|
||||||
@@ -1226,7 +1239,7 @@ class DifyServiceAPIRunner(runner.RequestRunner):
|
|||||||
content, _ = self._process_thinking_content(pending_content)
|
content, _ = self._process_thinking_content(pending_content)
|
||||||
yield provider_message.Message(role='assistant', content=content)
|
yield provider_message.Message(role='assistant', content=content)
|
||||||
|
|
||||||
def _resolve_pending_form(self, session_key: str, form_action: dict) -> dict | None:
|
def _resolve_pending_form(self, session_key: PendingFormKey, form_action: dict) -> dict | None:
|
||||||
"""Locate the pending form this action targets.
|
"""Locate the pending form this action targets.
|
||||||
|
|
||||||
Tries identifiers in order of specificity: form_token, full
|
Tries identifiers in order of specificity: form_token, full
|
||||||
@@ -1253,7 +1266,7 @@ class DifyServiceAPIRunner(runner.RequestRunner):
|
|||||||
|
|
||||||
return _get_latest_pending_form(session_key)
|
return _get_latest_pending_form(session_key)
|
||||||
|
|
||||||
def _merge_pending_form_action(self, session_key: str, form_action: dict | None) -> dict | None:
|
def _merge_pending_form_action(self, session_key: PendingFormKey, form_action: dict | None) -> dict | None:
|
||||||
"""Backfill resume fields from the matching pending form."""
|
"""Backfill resume fields from the matching pending form."""
|
||||||
if not form_action:
|
if not form_action:
|
||||||
return None
|
return None
|
||||||
@@ -1310,7 +1323,7 @@ class DifyServiceAPIRunner(runner.RequestRunner):
|
|||||||
async def _match_pending_form_action(
|
async def _match_pending_form_action(
|
||||||
self,
|
self,
|
||||||
query: pipeline_query.Query,
|
query: pipeline_query.Query,
|
||||||
session_key: str,
|
session_key: PendingFormKey,
|
||||||
user_text: str,
|
user_text: str,
|
||||||
) -> dict | None:
|
) -> dict | None:
|
||||||
"""Match plain text replies against pending Dify form actions.
|
"""Match plain text replies against pending Dify form actions.
|
||||||
@@ -1428,7 +1441,7 @@ class DifyServiceAPIRunner(runner.RequestRunner):
|
|||||||
form_action,
|
form_action,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
async for msg in self._submit_workflow_form_blocking(form_action):
|
async for msg in self._submit_workflow_form_blocking(form_action, session_key):
|
||||||
yield msg
|
yield msg
|
||||||
_clear_pending_form(session_key, form_action.get('form_token') or None)
|
_clear_pending_form(session_key, form_action.get('form_token') or None)
|
||||||
return
|
return
|
||||||
@@ -1463,7 +1476,7 @@ class DifyServiceAPIRunner(runner.RequestRunner):
|
|||||||
|
|
||||||
async for chunk in self.dify_client.workflow_run(
|
async for chunk in self.dify_client.workflow_run(
|
||||||
inputs=inputs,
|
inputs=inputs,
|
||||||
user=f'{query.session.launcher_type.value}_{query.session.launcher_id}',
|
user=_dify_user_from_query(query),
|
||||||
files=files,
|
files=files,
|
||||||
timeout=120,
|
timeout=120,
|
||||||
):
|
):
|
||||||
@@ -1476,7 +1489,7 @@ class DifyServiceAPIRunner(runner.RequestRunner):
|
|||||||
workflow_run_id = chunk['data'].get('workflow_run_id', '')
|
workflow_run_id = chunk['data'].get('workflow_run_id', '')
|
||||||
for reason in reasons:
|
for reason in reasons:
|
||||||
if reason.get('TYPE') == 'human_input_required':
|
if reason.get('TYPE') == 'human_input_required':
|
||||||
user = f'{query.session.launcher_type.value}_{query.session.launcher_id}'
|
user = _dify_user_from_query(query)
|
||||||
form_snapshot, raw_form_content, input_defs, _ = _extract_form_snapshot(
|
form_snapshot, raw_form_content, input_defs, _ = _extract_form_snapshot(
|
||||||
workflow_run_id,
|
workflow_run_id,
|
||||||
reason,
|
reason,
|
||||||
@@ -1561,7 +1574,7 @@ class DifyServiceAPIRunner(runner.RequestRunner):
|
|||||||
form_action,
|
form_action,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
async for msg in self._submit_workflow_form(form_action):
|
async for msg in self._submit_workflow_form(form_action, session_key):
|
||||||
yield msg
|
yield msg
|
||||||
_clear_pending_form(session_key, form_action.get('form_token') or None)
|
_clear_pending_form(session_key, form_action.get('form_token') or None)
|
||||||
return
|
return
|
||||||
@@ -1607,7 +1620,7 @@ class DifyServiceAPIRunner(runner.RequestRunner):
|
|||||||
async for chunk in self.dify_client.chat_messages(
|
async for chunk in self.dify_client.chat_messages(
|
||||||
inputs=inputs,
|
inputs=inputs,
|
||||||
query=plain_text,
|
query=plain_text,
|
||||||
user=f'{query.session.launcher_type.value}_{query.session.launcher_id}',
|
user=_dify_user_from_query(query),
|
||||||
conversation_id=cov_id,
|
conversation_id=cov_id,
|
||||||
files=files,
|
files=files,
|
||||||
timeout=120,
|
timeout=120,
|
||||||
@@ -1639,7 +1652,7 @@ class DifyServiceAPIRunner(runner.RequestRunner):
|
|||||||
for reason in reasons:
|
for reason in reasons:
|
||||||
if reason.get('TYPE') != 'human_input_required':
|
if reason.get('TYPE') != 'human_input_required':
|
||||||
continue
|
continue
|
||||||
user = f'{query.session.launcher_type.value}_{query.session.launcher_id}'
|
user = _dify_user_from_query(query)
|
||||||
form_snapshot, raw_form_content, input_defs, display_form_content = _extract_form_snapshot(
|
form_snapshot, raw_form_content, input_defs, display_form_content = _extract_form_snapshot(
|
||||||
workflow_run_id,
|
workflow_run_id,
|
||||||
reason,
|
reason,
|
||||||
@@ -1759,7 +1772,7 @@ class DifyServiceAPIRunner(runner.RequestRunner):
|
|||||||
async for chunk in self.dify_client.chat_messages(
|
async for chunk in self.dify_client.chat_messages(
|
||||||
inputs=inputs,
|
inputs=inputs,
|
||||||
query=plain_text,
|
query=plain_text,
|
||||||
user=f'{query.session.launcher_type.value}_{query.session.launcher_id}',
|
user=_dify_user_from_query(query),
|
||||||
response_mode='streaming',
|
response_mode='streaming',
|
||||||
conversation_id=cov_id,
|
conversation_id=cov_id,
|
||||||
files=files,
|
files=files,
|
||||||
@@ -1846,7 +1859,7 @@ class DifyServiceAPIRunner(runner.RequestRunner):
|
|||||||
query.session.using_conversation.uuid = chunk['conversation_id']
|
query.session.using_conversation.uuid = chunk['conversation_id']
|
||||||
|
|
||||||
async def _submit_workflow_form(
|
async def _submit_workflow_form(
|
||||||
self, form_action: dict
|
self, form_action: dict, session_key: PendingFormKey
|
||||||
) -> typing.AsyncGenerator[provider_message.MessageChunk, None]:
|
) -> typing.AsyncGenerator[provider_message.MessageChunk, None]:
|
||||||
"""Submit human input to resume a paused Dify workflow."""
|
"""Submit human input to resume a paused Dify workflow."""
|
||||||
|
|
||||||
@@ -1901,7 +1914,7 @@ class DifyServiceAPIRunner(runner.RequestRunner):
|
|||||||
# card still shows which step the user acted on.
|
# card still shows which step the user acted on.
|
||||||
paused_node_title = form_snapshot.get('node_title', '')
|
paused_node_title = form_snapshot.get('node_title', '')
|
||||||
|
|
||||||
_set_pending_form(user, form_snapshot)
|
_set_pending_form(session_key, form_snapshot)
|
||||||
|
|
||||||
repause_form_data = _initial_interactive_form_data(form_snapshot) or {
|
repause_form_data = _initial_interactive_form_data(form_snapshot) or {
|
||||||
'form_content': display_form_content,
|
'form_content': display_form_content,
|
||||||
@@ -2014,7 +2027,7 @@ class DifyServiceAPIRunner(runner.RequestRunner):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
# Resume paused workflow via submit endpoint
|
# Resume paused workflow via submit endpoint
|
||||||
async for msg in self._submit_workflow_form(form_action):
|
async for msg in self._submit_workflow_form(form_action, session_key):
|
||||||
yield msg
|
yield msg
|
||||||
_clear_pending_form(session_key, form_action.get('form_token') or None)
|
_clear_pending_form(session_key, form_action.get('form_token') or None)
|
||||||
return
|
return
|
||||||
@@ -2060,7 +2073,7 @@ class DifyServiceAPIRunner(runner.RequestRunner):
|
|||||||
remove_think = self.pipeline_config['output'].get('misc', {}).get('remove-think')
|
remove_think = self.pipeline_config['output'].get('misc', {}).get('remove-think')
|
||||||
async for chunk in self.dify_client.workflow_run(
|
async for chunk in self.dify_client.workflow_run(
|
||||||
inputs=inputs,
|
inputs=inputs,
|
||||||
user=f'{query.session.launcher_type.value}_{query.session.launcher_id}',
|
user=_dify_user_from_query(query),
|
||||||
files=files,
|
files=files,
|
||||||
timeout=120,
|
timeout=120,
|
||||||
):
|
):
|
||||||
@@ -2075,7 +2088,7 @@ class DifyServiceAPIRunner(runner.RequestRunner):
|
|||||||
workflow_run_id = chunk['data'].get('workflow_run_id', workflow_run_id)
|
workflow_run_id = chunk['data'].get('workflow_run_id', workflow_run_id)
|
||||||
for reason in reasons:
|
for reason in reasons:
|
||||||
if reason.get('TYPE') == 'human_input_required':
|
if reason.get('TYPE') == 'human_input_required':
|
||||||
user = f'{query.session.launcher_type.value}_{query.session.launcher_id}'
|
user = _dify_user_from_query(query)
|
||||||
form_snapshot, raw_form_content, input_defs, display_form_content = _extract_form_snapshot(
|
form_snapshot, raw_form_content, input_defs, display_form_content = _extract_form_snapshot(
|
||||||
workflow_run_id,
|
workflow_run_id,
|
||||||
reason,
|
reason,
|
||||||
|
|||||||
@@ -196,6 +196,49 @@ class TestDifyHumanInputForms:
|
|||||||
runner.dify_client.upload_file = AsyncMock(return_value={'id': 'upload-1'})
|
runner.dify_client.upload_file = AsyncMock(return_value={'id': 'upload-1'})
|
||||||
return runner
|
return runner
|
||||||
|
|
||||||
|
def test_pending_forms_are_isolated_by_bot_and_pipeline(self):
|
||||||
|
from langbot.pkg.provider.runners import difysvapi
|
||||||
|
|
||||||
|
query_a = MagicMock()
|
||||||
|
query_a.bot_uuid = 'bot-a'
|
||||||
|
query_a.pipeline_uuid = 'pipeline-a'
|
||||||
|
query_a.session.launcher_type.value = 'person'
|
||||||
|
query_a.session.launcher_id = 'shared-user'
|
||||||
|
|
||||||
|
query_b = MagicMock()
|
||||||
|
query_b.bot_uuid = 'bot-b'
|
||||||
|
query_b.pipeline_uuid = 'pipeline-a'
|
||||||
|
query_b.session.launcher_type.value = 'person'
|
||||||
|
query_b.session.launcher_id = 'shared-user'
|
||||||
|
|
||||||
|
query_c = MagicMock()
|
||||||
|
query_c.bot_uuid = 'bot-a'
|
||||||
|
query_c.pipeline_uuid = 'pipeline-b'
|
||||||
|
query_c.session.launcher_type.value = 'person'
|
||||||
|
query_c.session.launcher_id = 'shared-user'
|
||||||
|
|
||||||
|
key_a = difysvapi._session_key_from_query(query_a)
|
||||||
|
key_b = difysvapi._session_key_from_query(query_b)
|
||||||
|
key_c = difysvapi._session_key_from_query(query_c)
|
||||||
|
difysvapi._PENDING_FORMS.clear()
|
||||||
|
difysvapi._set_pending_form(key_a, {'form_token': 'token-a', 'workflow_run_id': 'run-a'})
|
||||||
|
difysvapi._set_pending_form(key_b, {'form_token': 'token-b', 'workflow_run_id': 'run-b'})
|
||||||
|
difysvapi._set_pending_form(key_c, {'form_token': 'token-c', 'workflow_run_id': 'run-c'})
|
||||||
|
|
||||||
|
assert key_a != key_b
|
||||||
|
assert key_a != key_c
|
||||||
|
assert difysvapi._get_pending_form_by_token(key_a, 'token-a') is not None
|
||||||
|
assert difysvapi._get_pending_form_by_token(key_a, 'token-b') is None
|
||||||
|
assert difysvapi._get_pending_form_by_token(key_a, 'token-c') is None
|
||||||
|
assert difysvapi._get_pending_form_by_token(key_b, 'token-b') is not None
|
||||||
|
assert difysvapi._get_pending_form_by_token(key_c, 'token-c') is not None
|
||||||
|
assert difysvapi._get_latest_pending_form(key_a)['workflow_run_id'] == 'run-a'
|
||||||
|
assert difysvapi._get_latest_pending_form(key_b)['workflow_run_id'] == 'run-b'
|
||||||
|
assert difysvapi._get_latest_pending_form(key_c)['workflow_run_id'] == 'run-c'
|
||||||
|
assert difysvapi._dify_user_from_query(query_a) == difysvapi._dify_user_from_query(query_b)
|
||||||
|
assert difysvapi._dify_user_from_query(query_a) == difysvapi._dify_user_from_query(query_c)
|
||||||
|
difysvapi._PENDING_FORMS.clear()
|
||||||
|
|
||||||
def test_format_human_input_text_includes_field_help(self):
|
def test_format_human_input_text_includes_field_help(self):
|
||||||
from langbot.pkg.provider.runners.difysvapi import _format_human_input_text
|
from langbot.pkg.provider.runners.difysvapi import _format_human_input_text
|
||||||
|
|
||||||
@@ -737,7 +780,8 @@ class TestDifyHumanInputForms:
|
|||||||
'user': 'person_user-1',
|
'user': 'person_user-1',
|
||||||
'action_id': 'yes',
|
'action_id': 'yes',
|
||||||
'inputs': {},
|
'inputs': {},
|
||||||
}
|
},
|
||||||
|
('bot-1', 'pipeline-1', 'adapter.Type', 'person', 'user-1'),
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -748,18 +792,7 @@ class TestDifyHumanInputForms:
|
|||||||
from langbot.pkg.provider.runners import difysvapi
|
from langbot.pkg.provider.runners import difysvapi
|
||||||
|
|
||||||
runner = self._create_runner()
|
runner = self._create_runner()
|
||||||
session_key = 'person_user-1'
|
|
||||||
difysvapi._PENDING_FORMS.clear()
|
difysvapi._PENDING_FORMS.clear()
|
||||||
difysvapi._set_pending_form(
|
|
||||||
session_key,
|
|
||||||
{
|
|
||||||
'form_token': 'token-1',
|
|
||||||
'workflow_run_id': 'run-1',
|
|
||||||
'actions': [{'id': 'yes', 'title': 'Yes'}],
|
|
||||||
'inputs': {},
|
|
||||||
'user': session_key,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
async def workflow_submit(**kwargs):
|
async def workflow_submit(**kwargs):
|
||||||
del kwargs
|
del kwargs
|
||||||
@@ -772,12 +805,25 @@ class TestDifyHumanInputForms:
|
|||||||
query = MagicMock()
|
query = MagicMock()
|
||||||
query.session.launcher_type.value = 'person'
|
query.session.launcher_type.value = 'person'
|
||||||
query.session.launcher_id = 'user-1'
|
query.session.launcher_id = 'user-1'
|
||||||
|
query.bot_uuid = 'bot-1'
|
||||||
|
query.pipeline_uuid = 'pipeline-1'
|
||||||
|
session_key = difysvapi._session_key_from_query(query)
|
||||||
|
difysvapi._set_pending_form(
|
||||||
|
session_key,
|
||||||
|
{
|
||||||
|
'form_token': 'token-1',
|
||||||
|
'workflow_run_id': 'run-1',
|
||||||
|
'actions': [{'id': 'yes', 'title': 'Yes'}],
|
||||||
|
'inputs': {},
|
||||||
|
'user': 'person_user-1',
|
||||||
|
},
|
||||||
|
)
|
||||||
query.variables = {
|
query.variables = {
|
||||||
'_dify_form_action': {
|
'_dify_form_action': {
|
||||||
'form_token': 'token-1',
|
'form_token': 'token-1',
|
||||||
'workflow_run_id': 'run-1',
|
'workflow_run_id': 'run-1',
|
||||||
'action_id': 'yes',
|
'action_id': 'yes',
|
||||||
'user': session_key,
|
'user': 'person_user-1',
|
||||||
'inputs': {},
|
'inputs': {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -793,18 +839,7 @@ class TestDifyHumanInputForms:
|
|||||||
from langbot.pkg.provider.runners import difysvapi
|
from langbot.pkg.provider.runners import difysvapi
|
||||||
|
|
||||||
runner = self._create_runner()
|
runner = self._create_runner()
|
||||||
session_key = 'person_user-1'
|
|
||||||
difysvapi._PENDING_FORMS.clear()
|
difysvapi._PENDING_FORMS.clear()
|
||||||
difysvapi._set_pending_form(
|
|
||||||
session_key,
|
|
||||||
{
|
|
||||||
'form_token': 'token-1',
|
|
||||||
'workflow_run_id': 'run-1',
|
|
||||||
'actions': [{'id': 'yes', 'title': 'Yes'}],
|
|
||||||
'inputs': {},
|
|
||||||
'user': session_key,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
async def workflow_submit(**kwargs):
|
async def workflow_submit(**kwargs):
|
||||||
del kwargs
|
del kwargs
|
||||||
@@ -815,12 +850,25 @@ class TestDifyHumanInputForms:
|
|||||||
query = MagicMock()
|
query = MagicMock()
|
||||||
query.session.launcher_type.value = 'person'
|
query.session.launcher_type.value = 'person'
|
||||||
query.session.launcher_id = 'user-1'
|
query.session.launcher_id = 'user-1'
|
||||||
|
query.bot_uuid = 'bot-1'
|
||||||
|
query.pipeline_uuid = 'pipeline-1'
|
||||||
|
session_key = difysvapi._session_key_from_query(query)
|
||||||
|
difysvapi._set_pending_form(
|
||||||
|
session_key,
|
||||||
|
{
|
||||||
|
'form_token': 'token-1',
|
||||||
|
'workflow_run_id': 'run-1',
|
||||||
|
'actions': [{'id': 'yes', 'title': 'Yes'}],
|
||||||
|
'inputs': {},
|
||||||
|
'user': 'person_user-1',
|
||||||
|
},
|
||||||
|
)
|
||||||
query.variables = {
|
query.variables = {
|
||||||
'_dify_form_action': {
|
'_dify_form_action': {
|
||||||
'form_token': 'token-1',
|
'form_token': 'token-1',
|
||||||
'workflow_run_id': 'run-1',
|
'workflow_run_id': 'run-1',
|
||||||
'action_id': 'yes',
|
'action_id': 'yes',
|
||||||
'user': session_key,
|
'user': 'person_user-1',
|
||||||
'inputs': {},
|
'inputs': {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user