mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-16 17:36:07 +00:00
0755beebcd
* 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
159 lines
5.7 KiB
Python
159 lines
5.7 KiB
Python
"""Tests for Telegram Dify form callback helpers."""
|
|
|
|
import json
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
import pytest
|
|
from telegram import ForceReply
|
|
|
|
import langbot_plugin.api.entities.builtin.platform.entities as platform_entities
|
|
import langbot_plugin.api.entities.builtin.platform.events as platform_events
|
|
import langbot_plugin.api.entities.builtin.platform.message as platform_message
|
|
from langbot.pkg.platform.sources.telegram import (
|
|
TelegramAdapter,
|
|
_telegram_form_action_from_callback,
|
|
_telegram_select_field_options,
|
|
)
|
|
|
|
|
|
def _select_form_data() -> dict:
|
|
return {
|
|
'_current_input_field': 'choice',
|
|
'input_defs': [
|
|
{
|
|
'output_variable_name': 'choice',
|
|
'type': 'select',
|
|
'option_source': {'type': 'constant', 'value': ['A', 'B', 'C']},
|
|
}
|
|
],
|
|
}
|
|
|
|
|
|
def test_telegram_select_field_options_are_extracted():
|
|
assert _telegram_select_field_options(_select_form_data()) == ('choice', ['A', 'B', 'C'])
|
|
|
|
|
|
def test_telegram_select_callback_becomes_input_progress():
|
|
assert _telegram_form_action_from_callback({'f': 1, 'x': 1}) == {
|
|
'action_id': '',
|
|
'inputs': {'select': {'index': 1}},
|
|
'_input_progress': True,
|
|
}
|
|
|
|
|
|
def test_telegram_action_callback_remains_final_action():
|
|
assert _telegram_form_action_from_callback({'f': 1, 'a': 'approve'}) == {
|
|
'action_id': 'approve',
|
|
'inputs': {},
|
|
}
|
|
|
|
|
|
def test_telegram_invalid_select_callback_is_rejected():
|
|
assert _telegram_form_action_from_callback({'f': 1, 'x': -1}) is None
|
|
assert _telegram_form_action_from_callback({'f': 1, 'x': 'invalid'}) is None
|
|
|
|
|
|
def test_telegram_form_callback_cache_consumes_the_whole_form_group():
|
|
adapter = TelegramAdapter.model_construct()
|
|
adapter._form_action_titles = {}
|
|
adapter._cache_form_action_titles({'callback-a': 'A', 'callback-b': 'B'}, now=100.0)
|
|
|
|
assert adapter._take_form_action_title('callback-a', now=101.0) == 'A'
|
|
assert adapter._take_form_action_title('callback-a', now=101.0) is None
|
|
assert adapter._take_form_action_title('callback-b', now=101.0) is None
|
|
assert adapter._form_action_titles == {}
|
|
|
|
|
|
def test_telegram_form_callback_cache_prunes_expired_entries():
|
|
adapter = TelegramAdapter.model_construct()
|
|
adapter._form_action_titles = {}
|
|
adapter._cache_form_action_titles({'callback-a': 'A'}, now=100.0)
|
|
|
|
assert adapter._take_form_action_title('callback-a', now=100.0 + adapter._FORM_ACTION_CACHE_TTL) is None
|
|
assert adapter._form_action_titles == {}
|
|
|
|
|
|
def test_telegram_form_callback_cache_preserves_pipeline_uuid():
|
|
adapter = TelegramAdapter.model_construct()
|
|
adapter._form_action_titles = {}
|
|
adapter._cache_form_action_titles(
|
|
{'callback-a': 'Approve'},
|
|
pipeline_uuid='pipeline-routed',
|
|
now=100.0,
|
|
)
|
|
|
|
assert adapter._take_form_action_context('callback-a', now=101.0) == (
|
|
'Approve',
|
|
'pipeline-routed',
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_telegram_select_field_sends_two_column_inline_keyboard():
|
|
bot = MagicMock()
|
|
bot.send_message = AsyncMock()
|
|
adapter = TelegramAdapter.model_construct(bot=bot, config={}, msg_stream_id={}, seq=1, listeners={})
|
|
adapter._form_action_titles = {}
|
|
|
|
update = MagicMock()
|
|
update.effective_chat.id = 123
|
|
update.effective_message.message_thread_id = None
|
|
event = platform_events.FriendMessage(
|
|
sender=platform_entities.Friend(id='user-1', nickname='', remark=''),
|
|
message_chain=platform_message.MessageChain([]),
|
|
source_platform_object=update,
|
|
)
|
|
form_data = {
|
|
**_select_form_data(),
|
|
'node_title': 'Review',
|
|
'form_content': 'Choose one',
|
|
'workflow_run_id': 'workflow-run-12345678',
|
|
'actions': [{'id': 'approve', 'title': 'Approve'}],
|
|
}
|
|
|
|
await adapter._send_form_action_buttons(event, form_data)
|
|
|
|
args = bot.send_message.await_args.kwargs
|
|
rows = args['reply_markup'].inline_keyboard
|
|
assert [[button.text for button in row] for row in rows] == [['A', 'B'], ['C']]
|
|
callback_data = rows[0][1].callback_data
|
|
assert len(callback_data.encode('utf-8')) <= 64
|
|
assert json.loads(callback_data)['x'] == 1
|
|
assert callback_data in adapter._form_action_titles
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_telegram_text_field_does_not_show_action_buttons():
|
|
bot = MagicMock()
|
|
bot.send_message = AsyncMock()
|
|
adapter = TelegramAdapter.model_construct(bot=bot, config={}, msg_stream_id={}, seq=1, listeners={})
|
|
adapter._form_action_titles = {}
|
|
|
|
update = MagicMock()
|
|
update.effective_chat.id = 123
|
|
update.effective_message.message_thread_id = None
|
|
event = platform_events.FriendMessage(
|
|
sender=platform_entities.Friend(id='user-1', nickname='', remark=''),
|
|
message_chain=platform_message.MessageChain([]),
|
|
source_platform_object=update,
|
|
)
|
|
form_data = {
|
|
'_current_input_field': 'us_input',
|
|
'input_defs': [{'output_variable_name': 'us_input', 'type': 'paragraph'}],
|
|
'node_title': '人工介入',
|
|
'form_content': 'us_input (paragraph): reply "us_input: <value>"',
|
|
'workflow_run_id': 'workflow-run-12345678',
|
|
'actions': [{'id': 'yes', 'title': 'yes'}, {'id': 'no', 'title': 'no'}],
|
|
}
|
|
|
|
await adapter._send_form_action_buttons(event, form_data)
|
|
|
|
args = bot.send_message.await_args.kwargs
|
|
assert isinstance(args['reply_markup'], ForceReply)
|
|
assert args['reply_markup'].selective is False
|
|
assert args['reply_markup'].input_field_placeholder == 'us_input'
|
|
assert 'Please reply' not in args['text']
|
|
assert args['text'].startswith('[人工介入]')
|
|
assert 'us_input (paragraph)' in args['text']
|
|
assert adapter._form_action_titles == {}
|