mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-01 23:27:14 +00:00
feat: Add the function for formatting human input text to support adapters without rich UI.
This commit is contained in:
@@ -2250,7 +2250,59 @@ class LarkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
'vertical_align': 'top',
|
'vertical_align': 'top',
|
||||||
'margin': '0px 0px 0px 0px',
|
'margin': '0px 0px 0px 0px',
|
||||||
'weight': 1,
|
'weight': 1,
|
||||||
}
|
},
|
||||||
|
*(
|
||||||
|
[]
|
||||||
|
if form_data
|
||||||
|
else [
|
||||||
|
{
|
||||||
|
'tag': 'column',
|
||||||
|
'width': '20px',
|
||||||
|
'elements': [
|
||||||
|
{
|
||||||
|
'tag': 'button',
|
||||||
|
'text': {'tag': 'plain_text', 'content': ''},
|
||||||
|
'type': 'text',
|
||||||
|
'width': 'fill',
|
||||||
|
'size': 'medium',
|
||||||
|
'icon': {'tag': 'standard_icon', 'token': 'thumbsup_outlined'},
|
||||||
|
'hover_tips': {'tag': 'plain_text', 'content': '有帮助'},
|
||||||
|
'behaviors': [{'type': 'callback', 'value': {'feedback': '有帮助'}}],
|
||||||
|
'margin': '0px 0px 0px 0px',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'padding': '0px 0px 0px 0px',
|
||||||
|
'direction': 'vertical',
|
||||||
|
'horizontal_spacing': '8px',
|
||||||
|
'vertical_spacing': '8px',
|
||||||
|
'horizontal_align': 'left',
|
||||||
|
'vertical_align': 'top',
|
||||||
|
'margin': '0px 0px 0px 0px',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'tag': 'column',
|
||||||
|
'width': '30px',
|
||||||
|
'elements': [
|
||||||
|
{
|
||||||
|
'tag': 'button',
|
||||||
|
'text': {'tag': 'plain_text', 'content': ''},
|
||||||
|
'type': 'text',
|
||||||
|
'width': 'default',
|
||||||
|
'size': 'medium',
|
||||||
|
'icon': {'tag': 'standard_icon', 'token': 'thumbdown_outlined'},
|
||||||
|
'hover_tips': {'tag': 'plain_text', 'content': '无帮助'},
|
||||||
|
'behaviors': [{'type': 'callback', 'value': {'feedback': '无帮助'}}],
|
||||||
|
'margin': '0px 0px 0px 0px',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'padding': '0px 0px 0px 0px',
|
||||||
|
'vertical_spacing': '8px',
|
||||||
|
'horizontal_align': 'left',
|
||||||
|
'vertical_align': 'top',
|
||||||
|
'margin': '0px 0px 0px 0px',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
),
|
||||||
],
|
],
|
||||||
'margin': '0px 0px 4px 0px',
|
'margin': '0px 0px 4px 0px',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -114,6 +114,29 @@ def _clear_pending_form(session_key: str, form_token: str | None = None) -> None
|
|||||||
_PENDING_FORMS.pop(session_key, None)
|
_PENDING_FORMS.pop(session_key, None)
|
||||||
|
|
||||||
|
|
||||||
|
def _format_human_input_text(
|
||||||
|
node_title: str,
|
||||||
|
form_content: str,
|
||||||
|
actions: list[dict[str, typing.Any]],
|
||||||
|
) -> str:
|
||||||
|
"""Render a paused-workflow human-input prompt as plain text.
|
||||||
|
|
||||||
|
Used by adapters without rich UI (no buttons/cards) so users can reply
|
||||||
|
with the option number or the option title to resume the workflow.
|
||||||
|
"""
|
||||||
|
lines: list[str] = [f'[Human Input Required] {node_title or ""}'.rstrip()]
|
||||||
|
if form_content:
|
||||||
|
lines.append('')
|
||||||
|
lines.append(form_content)
|
||||||
|
if actions:
|
||||||
|
lines.append('')
|
||||||
|
lines.append('Reply with the number or title to continue:')
|
||||||
|
for idx, action in enumerate(actions, start=1):
|
||||||
|
title = action.get('title') or action.get('id') or ''
|
||||||
|
lines.append(f' {idx}. {title}')
|
||||||
|
return '\n'.join(lines)
|
||||||
|
|
||||||
|
|
||||||
@runner.runner_class('dify-service-api')
|
@runner.runner_class('dify-service-api')
|
||||||
class DifyServiceAPIRunner(runner.RequestRunner):
|
class DifyServiceAPIRunner(runner.RequestRunner):
|
||||||
"""Dify Service API 对话请求器"""
|
"""Dify Service API 对话请求器"""
|
||||||
@@ -520,14 +543,37 @@ class DifyServiceAPIRunner(runner.RequestRunner):
|
|||||||
def _match_pending_form_action(self, session_key: str, user_text: str) -> dict | None:
|
def _match_pending_form_action(self, session_key: str, user_text: str) -> dict | None:
|
||||||
"""Match plain text replies against pending Dify form actions.
|
"""Match plain text replies against pending Dify form actions.
|
||||||
|
|
||||||
Iterates all pending forms newest-first; the first action whose
|
Resolution order:
|
||||||
title/id matches the text wins. This means when multiple forms are
|
1. A pure digit reply (e.g. "1", "2") maps to the 1-indexed action of
|
||||||
pending with the same button label, the most recent one resolves.
|
the most recent pending form. Lets users on plain-text platforms
|
||||||
|
pick options without retyping titles.
|
||||||
|
2. Otherwise, iterate pending forms newest-first and match each
|
||||||
|
action's title/id case-insensitively. The first hit wins, so when
|
||||||
|
two forms share a button label the newer one resolves.
|
||||||
"""
|
"""
|
||||||
normalized_text = user_text.strip().lower()
|
normalized_text = user_text.strip().lower()
|
||||||
if not normalized_text:
|
if not normalized_text:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _build(pending_form: dict, action: dict) -> dict:
|
||||||
|
return {
|
||||||
|
'form_token': pending_form.get('form_token', ''),
|
||||||
|
'workflow_run_id': pending_form.get('workflow_run_id', ''),
|
||||||
|
'action_id': action.get('id', ''),
|
||||||
|
'action_title': action.get('title', action.get('id', '')),
|
||||||
|
'node_title': pending_form.get('node_title', ''),
|
||||||
|
'inputs': pending_form.get('inputs', {}),
|
||||||
|
'user': pending_form.get('user', ''),
|
||||||
|
}
|
||||||
|
|
||||||
|
if normalized_text.isdigit():
|
||||||
|
position = int(normalized_text)
|
||||||
|
latest_form = _get_latest_pending_form(session_key)
|
||||||
|
if latest_form is not None:
|
||||||
|
actions = latest_form.get('actions', [])
|
||||||
|
if 1 <= position <= len(actions):
|
||||||
|
return _build(latest_form, actions[position - 1])
|
||||||
|
|
||||||
for pending_form in _iter_pending_forms(session_key):
|
for pending_form in _iter_pending_forms(session_key):
|
||||||
for action in pending_form.get('actions', []):
|
for action in pending_form.get('actions', []):
|
||||||
titles = {
|
titles = {
|
||||||
@@ -535,15 +581,7 @@ class DifyServiceAPIRunner(runner.RequestRunner):
|
|||||||
str(action.get('id', '')).strip().lower(),
|
str(action.get('id', '')).strip().lower(),
|
||||||
}
|
}
|
||||||
if normalized_text in titles:
|
if normalized_text in titles:
|
||||||
return {
|
return _build(pending_form, action)
|
||||||
'form_token': pending_form.get('form_token', ''),
|
|
||||||
'workflow_run_id': pending_form.get('workflow_run_id', ''),
|
|
||||||
'action_id': action.get('id', ''),
|
|
||||||
'action_title': action.get('title', action.get('id', '')),
|
|
||||||
'node_title': pending_form.get('node_title', ''),
|
|
||||||
'inputs': pending_form.get('inputs', {}),
|
|
||||||
'user': pending_form.get('user', ''),
|
|
||||||
}
|
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -636,8 +674,7 @@ class DifyServiceAPIRunner(runner.RequestRunner):
|
|||||||
'node_title': node_title,
|
'node_title': node_title,
|
||||||
}
|
}
|
||||||
|
|
||||||
action_lines = '\n'.join(f'- [{a.get("title", a.get("id", ""))}]' for a in actions)
|
display_text = _format_human_input_text(node_title, form_content, actions)
|
||||||
display_text = f'[Human Input Required] {node_title}\n{form_content}\n{action_lines}'
|
|
||||||
|
|
||||||
human_input_yielded = True
|
human_input_yielded = True
|
||||||
yield provider_message.Message(
|
yield provider_message.Message(
|
||||||
@@ -1139,8 +1176,7 @@ class DifyServiceAPIRunner(runner.RequestRunner):
|
|||||||
'node_title': node_title,
|
'node_title': node_title,
|
||||||
}
|
}
|
||||||
|
|
||||||
action_lines = '\n'.join(f'- [{a.get("title", a.get("id", ""))}]' for a in actions)
|
display_text = _format_human_input_text(node_title, form_content, actions)
|
||||||
display_text = f'[Human Input Required] {node_title}\n{form_content}\n{action_lines}'
|
|
||||||
workflow_contents += display_text + '\n'
|
workflow_contents += display_text + '\n'
|
||||||
|
|
||||||
# Save form data to attach to the final chunk later.
|
# Save form data to attach to the final chunk later.
|
||||||
|
|||||||
Reference in New Issue
Block a user