mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-16 17:36:07 +00:00
feat: add supports for dify hitl (#2226)
* feat: Implement workflow form handling for paused workflows - Added module-level storage for pending forms to manage state across sessions. - Introduced functions to set, get, and clear pending forms with expiration handling. - Enhanced DifyServiceAPIRunner to support resuming paused workflows via form actions. - Implemented logic to yield human input requests and display appropriate messages. - Updated workflow submission methods to handle paused states and resume actions. - Ensured proper merging of pending form actions with user inputs for seamless interaction. * feat: Add '_routed_by_rule' variable to form action in Lark and Telegram adapters * feat: Enhance Lark and Telegram adapters with new form handling for paused workflows * feat: Enhance TelegramAdapter to handle form action buttons and message threading * feat: Improve TelegramAdapter message handling with enhanced error management and draft message support * feat: Add the function for formatting human input text to support adapters without rich UI. * feat(dingtalk): implement human input card support and card action handling - Add a new module `card_callback.py` to handle card action button clicks from DingTalk. - Introduce `DingTalkCardActionHandler` to process card action callbacks and extract parameters. - Update `DingTalkAdapter` to manage card state and handle form input through a single card template. - Add configuration for `human_input_card_template_id` in `dingtalk.yaml` to specify the template for human input. - Create a new card template `dingtalk_human_input_card.json` for rendering human input prompts and buttons. * feat(dingtalk): enhance human input card functionality with streaming support and active turn management - Updated the DingTalk card template to enable streaming mode and multi-update configuration. - Removed the obsolete delete_card method from DingTalkClient to streamline card management. - Enhanced DingTalkAdapter to manage active turn cards and accumulated streaming text, ensuring a seamless user experience during human input prompts. - Modified the create_message_card method to utilize existing active cards for resumed workflows, preventing duplication. - Improved the _paint_form_on_card method to update existing cards with human input prompts and buttons dynamically. - Updated the dingtalk_human_input_card.json template to reflect the new streaming capabilities and configuration options. * feat(wecom): implement Dify human input pause handling with button interaction support * feat(qqofficial): implement Dify human input button interaction handling and markdown keyboard support * feat(qqofficial): implement one-click QR binding and enhance localization support * feat(discord): implement Discord form view with button interactions for Dify actions * fix(telegram): correct group chat type check and handle oversized callback data for Telegram actions fix(difysvapi): ensure safe access to remove-think configuration in pipeline settings * feat(dify): add support for chatflow app type and enhance human input handling * feat(telegram): add action title feedback for user selections in Telegram messages * feat(lark): enhance LarkAdapter to store form content for resume notices * feat(dingtalk): update display formatting for card content with HTML line breaks * feat(dingtalk): add feedback functionality to cards with 👍/👎 buttons - Implemented feedback state management for cards, allowing users to provide feedback via thumbs up/down buttons. - Enhanced card rendering to include feedback buttons when appropriate. - Registered feedback listeners to handle feedback events and update card states accordingly. - Updated the card template to support dynamic button rendering for feedback actions. - Improved error handling and logging for feedback actions and card updates. * fix: add Avatar component to dingtalk_human_input_card.json for enhanced user interaction * feat(wecom): add optional source block to interactive template cards for enhanced branding * feat(wecom): add functions for template card action extraction and update, enhance button interaction handling * feat(qqofficial): synchronize passive-reply counter with inbound message sequence * feat(qqofficial): add method to identify invisible form placeholder chunks in messages * feat(dingtalk): add download link for human input card template and enhance dynamic form configuration * feat(telegram): enhance message handling with group stream deletion and form placeholder detection * Add unit tests for DingTalk, Lark, WeComBot, and Dify service API runners - Implement tests for DingTalk adapter helper functions including form content cleaning, input extraction, and completed input lines. - Create unit tests for Lark adapter helper functions focusing on input extraction and completed input lines. - Add tests for WeComBot template card functionalities, including event extraction and payload building for human input. - Enhance Dify service API runner tests to cover human input forms, including input collection, action handling, and form snapshot extraction. * feat: Enhance Telegram and QQ Official adapters with select field handling and form action processing - Added support for select fields in Telegram adapter, including option extraction and callback handling. - Implemented form action processing for Telegram callbacks, improving user interaction feedback. - Introduced new helper functions for building keyboards and resolving select button actions in QQ Official adapter. - Enhanced DifyServiceAPIRunner to handle cumulative streaming responses and improve error handling during workflow resumes. - Added unit tests for new functionalities in Telegram and QQ Official adapters, ensuring robust behavior for select fields and form actions. * feat(lark): add functions for current input definitions and visible form content handling feat(qqofficial): update fallback text handling for non-streaming scenarios feat(difysvapi): enhance form content processing for interactive fields and actions test: add unit tests for Lark and QQ Official adapter functionalities * Add tests for DingTalk adapter content processing and markdown formatting - Updated the assertion in `test_dingtalk_completed_input_lines_include_text_and_select_values` to remove unnecessary markdown formatting. - Added new tests to verify that `_dingtalk_clean_form_content` maintains the order of prompts and completed values in various scenarios. - Introduced `test_dingtalk_card_markdown_preserves_internal_line_breaks` to ensure internal line breaks are correctly converted to HTML line breaks. * feat: Refactor input handling and feedback messages across multiple adapters * feat: Update the human-computer interaction template cards, and optimize the prompt information and content display. * feat: Refactor pending form handling to isolate by bot and pipeline * feat: Enhance error handling and caching for Dify and WeCom interactions * feat: Enhance select input handling and validation in Dify API runner and Telegram adapter * feat: Add missing completed input lines handling in DingTalk adapter * feat: Add pipeline_uuid handling across multiple adapters and update related tests
This commit is contained in:
@@ -0,0 +1,254 @@
|
||||
"""Tests for DingTalk adapter helper behavior."""
|
||||
|
||||
import asyncio
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from langbot.pkg.platform.sources.dingtalk import (
|
||||
DingTalkAdapter,
|
||||
_dingtalk_card_markdown,
|
||||
_dingtalk_clean_form_content,
|
||||
_dingtalk_completed_input_lines,
|
||||
_dingtalk_extract_component_inputs,
|
||||
_dingtalk_form_component_params,
|
||||
_dingtalk_missing_completed_input_lines,
|
||||
_dingtalk_pending_input_defs,
|
||||
)
|
||||
|
||||
|
||||
def test_dingtalk_select_component_params_expose_options():
|
||||
params = _dingtalk_form_component_params(
|
||||
{
|
||||
'_current_input_field': 'choice',
|
||||
'input_defs': [
|
||||
{
|
||||
'output_variable_name': 'choice',
|
||||
'type': 'select',
|
||||
'option_source': {'type': 'constant', 'value': ['A', 'B']},
|
||||
}
|
||||
],
|
||||
'inputs': {},
|
||||
}
|
||||
)
|
||||
|
||||
assert params['select_visible'] == 'true'
|
||||
assert params['select_placeholder'] == 'choice'
|
||||
assert params['select_options'] == ['A', 'B']
|
||||
assert [option['value'] for option in params['index_o']] == ['A', 'B']
|
||||
assert [option['value'] for option in params['test_index']] == ['A', 'B']
|
||||
assert params['index_o'][0]['text']['zh_CN'] == 'A'
|
||||
assert params['index_o'][0]['text']['en_US'] == 'A'
|
||||
assert params['select_index'] == -1
|
||||
|
||||
|
||||
def test_dingtalk_extract_select_from_builtin_result_dict():
|
||||
inputs = _dingtalk_extract_component_inputs({'selectResult': {'index': 1, 'value': 'B'}})
|
||||
|
||||
assert inputs == {'select': 'B'}
|
||||
|
||||
|
||||
def test_dingtalk_extract_select_from_template_param_string():
|
||||
inputs = _dingtalk_extract_component_inputs({'select': '{"index": 1, "value": "B"}'})
|
||||
|
||||
assert inputs == {'select': '{"index": 1, "value": "B"}'}
|
||||
|
||||
|
||||
def test_dingtalk_extract_input_and_select_together():
|
||||
inputs = _dingtalk_extract_component_inputs(
|
||||
{
|
||||
'inputResult': {'value': 'looks good'},
|
||||
'__built_in_selectResult__': {'index': 0, 'value': 'A'},
|
||||
}
|
||||
)
|
||||
|
||||
assert inputs == {'input': 'looks good', 'select': 'A'}
|
||||
|
||||
|
||||
def test_dingtalk_extract_component_inputs_strips_card_line_endings():
|
||||
inputs = _dingtalk_extract_component_inputs(
|
||||
{
|
||||
'inputResult': {'value': '回复我测试\r\n'},
|
||||
'selectResult': {'value': '1\r'},
|
||||
}
|
||||
)
|
||||
|
||||
assert inputs == {'input': '回复我测试', 'select': '1'}
|
||||
|
||||
|
||||
def test_dingtalk_pending_input_defs_includes_file_fields():
|
||||
pending = _dingtalk_pending_input_defs(
|
||||
{
|
||||
'input_defs': [
|
||||
{'output_variable_name': 'comment', 'type': 'paragraph'},
|
||||
{'output_variable_name': 'files', 'type': 'file-list'},
|
||||
],
|
||||
'inputs': {'comment': 'ready'},
|
||||
}
|
||||
)
|
||||
|
||||
assert [field['output_variable_name'] for field in pending] == ['files']
|
||||
|
||||
|
||||
def test_dingtalk_completed_input_lines_include_text_and_select_values():
|
||||
lines = _dingtalk_completed_input_lines(
|
||||
{
|
||||
'all_input_defs': [
|
||||
{'output_variable_name': 'comment', 'type': 'paragraph'},
|
||||
{
|
||||
'output_variable_name': 'choice',
|
||||
'type': 'select',
|
||||
'option_source': {'type': 'constant', 'value': ['A', 'B']},
|
||||
},
|
||||
],
|
||||
'inputs': {'comment': 'looks good', 'choice': 'B'},
|
||||
}
|
||||
)
|
||||
|
||||
assert lines == ['✅ comment:looks good', '✅ choice:B']
|
||||
|
||||
|
||||
def test_dingtalk_completed_inputs_are_not_repeated_when_already_interleaved():
|
||||
form_data = {
|
||||
'all_input_defs': [
|
||||
{'output_variable_name': 'us_input', 'type': 'paragraph'},
|
||||
{'output_variable_name': 'xiala', 'type': 'select'},
|
||||
],
|
||||
'inputs': {'us_input': '回复我测试\r', 'xiala': '1'},
|
||||
}
|
||||
form_content = '你好\n请输入你的问题\n✅ us_input:回复我测试\n请选择你的答案\n✅ xiala:1'
|
||||
|
||||
assert _dingtalk_missing_completed_input_lines(form_data, form_content) == []
|
||||
|
||||
|
||||
def test_dingtalk_completed_inputs_are_appended_when_template_does_not_render_them():
|
||||
form_data = {
|
||||
'all_input_defs': [{'output_variable_name': 'comment', 'type': 'paragraph'}],
|
||||
'inputs': {'comment': 'ready'},
|
||||
}
|
||||
|
||||
assert _dingtalk_missing_completed_input_lines(form_data, 'Please review') == ['✅ comment:ready']
|
||||
|
||||
|
||||
def test_dingtalk_clean_form_content_uses_all_input_defs():
|
||||
content = _dingtalk_clean_form_content(
|
||||
{
|
||||
'raw_form_content': 'Hello\n\n{{#$output.comment#}}\n\n{{#$output.choice#}}\n',
|
||||
'input_defs': [],
|
||||
'all_input_defs': [
|
||||
{'output_variable_name': 'comment', 'type': 'paragraph'},
|
||||
{'output_variable_name': 'choice', 'type': 'select'},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
assert content == 'Hello'
|
||||
|
||||
|
||||
def test_dingtalk_field_stage_keeps_prior_prompts_and_completed_values():
|
||||
content = _dingtalk_clean_form_content(
|
||||
{
|
||||
'_current_input_field': 'choice',
|
||||
'raw_form_content': ('Question\n{{#$output.comment#}}\nChoose an answer\n{{#$output.choice#}}'),
|
||||
'form_content': 'Choose an answer',
|
||||
'input_defs': [
|
||||
{'output_variable_name': 'comment', 'type': 'paragraph'},
|
||||
{'output_variable_name': 'choice', 'type': 'select'},
|
||||
],
|
||||
'inputs': {'comment': 'hello'},
|
||||
}
|
||||
)
|
||||
|
||||
assert '{{#$output.' not in content
|
||||
assert content.index('Question') < content.index('comment')
|
||||
assert content.index('comment') < content.index('Choose an answer')
|
||||
|
||||
|
||||
def test_dingtalk_final_action_stage_interleaves_prompts_and_completed_values():
|
||||
content = _dingtalk_clean_form_content(
|
||||
{
|
||||
'_action_select_only': True,
|
||||
'raw_form_content': ('11\nQuestion\n{{#$output.comment#}}\nChoose an answer\n{{#$output.choice#}}'),
|
||||
'all_input_defs': [
|
||||
{'output_variable_name': 'comment', 'type': 'paragraph'},
|
||||
{'output_variable_name': 'choice', 'type': 'select'},
|
||||
],
|
||||
'inputs': {'comment': 'hello', 'choice': 'B'},
|
||||
}
|
||||
)
|
||||
|
||||
assert '{{#$output.' not in content
|
||||
assert content.startswith('11\nQuestion')
|
||||
assert content.index('Question') < content.index('comment')
|
||||
assert content.index('comment') < content.index('Choose an answer')
|
||||
assert content.index('Choose an answer') < content.index('choice')
|
||||
|
||||
|
||||
def test_dingtalk_card_markdown_preserves_internal_line_breaks():
|
||||
assert _dingtalk_card_markdown('11\nQuestion\nCompleted') == '11<br>Question<br>Completed'
|
||||
|
||||
|
||||
def _build_card_action_adapter() -> DingTalkAdapter:
|
||||
adapter = DingTalkAdapter.model_construct(
|
||||
card_state={
|
||||
'card-1': {
|
||||
'session_key': 'group_group-1',
|
||||
'launcher_type': 'group',
|
||||
'launcher_id': 'group-1',
|
||||
'sender_user_id': 'initiator-1',
|
||||
'form_token': 'token-1',
|
||||
'workflow_run_id': 'run-1',
|
||||
'actions': [{'id': 'approve', 'title': 'Approve'}],
|
||||
'node_title': 'Review',
|
||||
'form_content': 'Please review',
|
||||
'input_defs': [],
|
||||
'inputs': {},
|
||||
}
|
||||
},
|
||||
active_turn_card={},
|
||||
active_turn_text={},
|
||||
)
|
||||
adapter.logger = AsyncMock()
|
||||
adapter.ap = MagicMock()
|
||||
adapter.ap.platform_mgr.bots = []
|
||||
adapter.ap.query_pool.add_query = AsyncMock()
|
||||
return adapter
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dingtalk_group_card_action_uses_clicker_as_sender():
|
||||
adapter = _build_card_action_adapter()
|
||||
|
||||
with patch.object(DingTalkAdapter, '_mark_card_resolved', new=AsyncMock()) as mark_resolved:
|
||||
await adapter._on_card_action(
|
||||
{
|
||||
'out_track_id': 'card-1',
|
||||
'user_id': 'reviewer-2',
|
||||
'action_id': 'approve',
|
||||
'params': {},
|
||||
}
|
||||
)
|
||||
await asyncio.sleep(0)
|
||||
|
||||
call = adapter.ap.query_pool.add_query.await_args
|
||||
assert call.kwargs['launcher_id'] == 'group-1'
|
||||
assert call.kwargs['sender_id'] == 'reviewer-2'
|
||||
assert call.kwargs['message_event'].sender.id == 'reviewer-2'
|
||||
mark_resolved.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dingtalk_unknown_card_action_is_rejected():
|
||||
adapter = _build_card_action_adapter()
|
||||
|
||||
await adapter._on_card_action(
|
||||
{
|
||||
'out_track_id': 'card-1',
|
||||
'user_id': 'reviewer-2',
|
||||
'action_id': 'not-on-card',
|
||||
'params': {},
|
||||
}
|
||||
)
|
||||
|
||||
adapter.ap.query_pool.add_query.assert_not_awaited()
|
||||
adapter.logger.warning.assert_awaited_once()
|
||||
Reference in New Issue
Block a user