Skip to content

Commit 6cae954

Browse files
committed
feat: Refactor input handling and feedback messages across multiple adapters
1 parent 183fb87 commit 6cae954

12 files changed

Lines changed: 77 additions & 95 deletions

File tree

src/langbot/libs/qq_official_api/api.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -693,7 +693,7 @@ async def send_markdown_keyboard(
693693
target_type: str,
694694
target_id: str,
695695
markdown_content: str,
696-
keyboard: dict,
696+
keyboard: Optional[dict] = None,
697697
msg_id: Optional[str] = None,
698698
event_id: Optional[str] = None,
699699
msg_seq: int = 1,
@@ -734,9 +734,10 @@ async def send_markdown_keyboard(
734734
body: dict[str, Any] = {
735735
'msg_type': 2,
736736
'markdown': {'content': markdown_content},
737-
'keyboard': keyboard,
738737
'msg_seq': msg_seq,
739738
}
739+
if keyboard and keyboard.get('content', {}).get('rows'):
740+
body['keyboard'] = keyboard
740741
if msg_id:
741742
body['msg_id'] = msg_id
742743
if event_id:

src/langbot/libs/wecom_ai_bot_api/api.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1129,12 +1129,10 @@ def build_button_interaction_payload(
11291129
input_hint_lines = _wecom_input_hint_lines(form_data)
11301130
if input_hint_lines:
11311131
sub_title_parts.append('Fill these fields in chat before choosing an action:\n' + '\n'.join(input_hint_lines))
1132-
elif should_show_actions and actions:
1133-
sub_title_parts.append('Choose an action to continue.')
11341132
if overflow:
11351133
extra_lines = [f' - {a.get("title") or a.get("id") or ""} (回复 id: {a.get("id") or ""})' for a in overflow]
11361134
sub_title_parts.append(f'另有 {len(overflow)} 个选项不在按钮列表中,可直接回复 id:\n' + '\n'.join(extra_lines))
1137-
sub_title_text = '\n\n'.join(sub_title_parts) or '请选择一个操作以继续。'
1135+
sub_title_text = '\n\n'.join(sub_title_parts)
11381136

11391137
button_list = []
11401138
for idx, action in enumerate(visible_actions):
@@ -1452,8 +1450,8 @@ def build_button_interaction_update_card(
14521450
}
14531451
if action_key == clean_action_id:
14541452
button['style'] = _wecom_button_style(action, selected=True)
1455-
button['text'] = f'已选择:{button_title}'
1456-
button['replace_text'] = f'已选择:{button_title}'
1453+
button['text'] = f'{button_title}'
1454+
button['replace_text'] = f'{button_title}'
14571455
matched = True
14581456
button_list.append(button)
14591457

@@ -1463,7 +1461,7 @@ def build_button_interaction_update_card(
14631461
'text': action_title or clean_action_id,
14641462
'style': 1,
14651463
'key': clean_action_id,
1466-
'replace_text': f'已选择:{action_title or clean_action_id}',
1464+
'replace_text': f'{action_title or clean_action_id}',
14671465
}
14681466
)
14691467

src/langbot/pkg/platform/sources/dingtalk.py

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -296,14 +296,8 @@ def _dingtalk_completed_input_lines(form_data: dict) -> list[str]:
296296
value = inputs.get(field_name)
297297
if value in (None, '', []):
298298
continue
299-
field_type = _dingtalk_field_type(field)
300299
display_value = _dingtalk_display_input_value(field, value)
301-
if field_type == 'select':
302-
lines.append(f'✅ 已选择 {field_name}{display_value}')
303-
elif field_type in {'file', 'file-list'}:
304-
lines.append(f'✅ 已上传 {field_name}{display_value}')
305-
else:
306-
lines.append(f'✅ 已填写 {field_name}{display_value}')
300+
lines.append(f'✅ {field_name}{display_value}')
307301
return lines
308302

309303

@@ -960,9 +954,7 @@ async def _paint_form_on_card(
960954
input_hint_lines = [] if native_field else _dingtalk_input_hint_lines(form_data)
961955
if input_hint_lines:
962956
parts.append('Fill these fields in chat before choosing an action:<br>' + '<br>'.join(input_hint_lines))
963-
elif should_show_actions and actions:
964-
parts.append('Choose an action to continue.')
965-
display_content = '<br><br>'.join(parts) or '请选择一个操作以继续。'
957+
display_content = '<br><br>'.join(parts)
966958

967959
try:
968960
await self.bot.update_card_data(
@@ -1062,9 +1054,7 @@ async def _send_form_card(
10621054
input_hint_lines = [] if native_field else _dingtalk_input_hint_lines(form_data)
10631055
if input_hint_lines:
10641056
parts.append('Fill these fields in chat before choosing an action:<br>' + '<br>'.join(input_hint_lines))
1065-
elif should_show_actions and actions:
1066-
parts.append('Choose an action to continue.')
1067-
display_content = '<br><br>'.join(parts) or '请选择一个操作以继续。'
1057+
display_content = '<br><br>'.join(parts)
10681058

10691059
btns = self._build_btns(actions if should_show_actions else [], out_track_id)
10701060

@@ -1100,7 +1090,7 @@ async def _lazy_create_resume_chat_card(
11001090
"""Create a new card for resumed-workflow streaming output.
11011091
11021092
Used after a button click triggers a synthetic event — the form
1103-
card stays put with the "已选择" notice, and a fresh card is
1093+
card stays put with the selection notice, and a fresh card is
11041094
spawned here for the LLM reply to stream into.
11051095
"""
11061096
form_template_id = (self.config.get('human_input_card_template_id') or '').strip()
@@ -1349,7 +1339,7 @@ async def _on_card_action(self, payload: dict) -> None:
13491339
return
13501340

13511341
# Visual feedback on the form card itself: keep the prompt visible,
1352-
# add a "已选择" line, remove the buttons. The resumed-workflow
1342+
# add a selection line, remove the buttons. The resumed-workflow
13531343
# output lives on a separate new card (lazy-created in
13541344
# reply_message_chunk on the synthetic event), so the form card
13551345
# stays put as a record of the user's selection.
@@ -1367,7 +1357,7 @@ async def _on_card_action(self, payload: dict) -> None:
13671357
# Crucial: do NOT leave the form card's out_track_id in
13681358
# active_turn_card — otherwise create_message_card for the
13691359
# synthetic event would reuse it for the resume output, painting
1370-
# the LLM reply on top of the "已选择" notice. Clear it so the
1360+
# the LLM reply on top of the selection notice. Clear it so the
13711361
# resume goes through the lazy-create path and spawns a fresh card.
13721362
session_key = state.get('session_key', '')
13731363
if session_key and self.active_turn_card.get(session_key) == out_track_id:
@@ -1487,7 +1477,7 @@ async def _mark_card_resolved(
14871477
) -> None:
14881478
"""Update the form card to acknowledge the user's selection.
14891479
1490-
Keeps the original prompt visible, adds a "已选择: X" notice, and
1480+
Keeps the original prompt visible, adds a selection notice, and
14911481
clears the buttons. The card stays as a permanent record of the
14921482
choice; the resumed workflow's output goes to a separate new card.
14931483
"""
@@ -1504,7 +1494,7 @@ async def _mark_card_resolved(
15041494
)
15051495
if completed_lines and not all(line in form_content for line in completed_lines):
15061496
parts.append('<hr>' + '<br>'.join(completed_lines))
1507-
parts.append(f'<hr>✅ 已选择:**{action_title}**')
1497+
parts.append(f'<hr>✅ {action_title}')
15081498
content = '<br><br>'.join(parts)
15091499
if self.ap is not None:
15101500
self.ap.logger.info(f'DingTalk _mark_card_resolved: out_track_id={out_track_id} action={action_title!r}')

src/langbot/pkg/platform/sources/discord.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1396,7 +1396,6 @@ async def _handle_form_chunk(
13961396
body_parts: list[str] = []
13971397
if form_content:
13981398
body_parts.append(form_content)
1399-
body_parts.append('Please select an option below:')
14001399
embed_body = '\n\n'.join(body_parts)
14011400
# Discord embed.description has a 4096 char limit — defensive trim.
14021401
if len(embed_body) > 4000:
@@ -1589,7 +1588,7 @@ async def _lock_view_message(
15891588
stale: bool = False,
15901589
) -> None:
15911590
"""Disable all buttons on the form view and annotate the chosen
1592-
one — mirrors DingTalk/Lark's in-card 已选择 feedback."""
1591+
one — mirrors DingTalk/Lark's in-card selection feedback."""
15931592
try:
15941593
for child in view.children:
15951594
if not isinstance(child, discord_ui.Button):

src/langbot/pkg/platform/sources/lark.py

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -187,13 +187,7 @@ def _lark_completed_input_lines(form_data: dict) -> list[str]:
187187
if value in (None, '', []):
188188
continue
189189
display_value = _lark_display_input_value(field, value)
190-
field_type = _dify_field_type(field)
191-
if field_type == 'select':
192-
lines.append(f'✅ 已选择 {field_name}:**{display_value}**')
193-
elif field_type in {'file', 'file-list'}:
194-
lines.append(f'✅ 已上传 {field_name}:**{display_value}**')
195-
else:
196-
lines.append(f'✅ 已填写 {field_name}:**{display_value}**')
190+
lines.append(f'✅ {field_name}{display_value}')
197191
return lines
198192

199193

@@ -2014,7 +2008,7 @@ async def reply_message_chunk(
20142008
20152009
Supports Dify form-action resume: when the runner yields a chunk with
20162010
``_resume_from_form=True``, the card transitions from buttons to a
2017-
grey "已选择" notice and a new ``streaming_txt_resume`` element is added
2011+
grey selection notice and a new ``streaming_txt_resume`` element is added
20182012
for subsequent resume chunks to stream into.
20192013
20202014
When ``_open_new_card=True`` on the final chunk, the existing card is
@@ -2051,7 +2045,7 @@ async def reply_message_chunk(
20512045
)
20522046
if completed_lines and not all(line in stored_form_content for line in completed_lines):
20532047
notice_parts.append('---\n' + '\n'.join(completed_lines))
2054-
notice_parts.append(f'---\n已选择:**{action_title}**')
2048+
notice_parts.append(f'---\n{action_title}')
20552049
selected_notice = '\n\n'.join(notice_parts)
20562050
else:
20572051
selected_notice = ''
@@ -2379,7 +2373,7 @@ async def _update_card_layout(
23792373
"""Update the entire card layout.
23802374
23812375
• form_data → show interactive buttons (initial Dify pause)
2382-
• notice_text → replace buttons with a grey "已选择" notice (resume transition)
2376+
• notice_text → replace buttons with a grey selection notice (resume transition)
23832377
• resume_placeholder_text → add a streaming_txt_resume markdown element
23842378
"""
23852379
form_data = form_data or {}

src/langbot/pkg/platform/sources/qqofficial.py

Lines changed: 10 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -842,51 +842,22 @@ async def _handle_form_chunk(
842842
form_content = form_data.get('form_content') or ''
843843
is_field_step = bool(form_data.get('_current_input_field')) and not form_data.get('_action_select_only')
844844
parts = [f'### {node_title}']
845+
plain_parts = [node_title]
845846
if form_content.strip():
846847
parts.append(form_content.strip())
847-
parts.append('请点击下方按钮选择:')
848+
plain_parts.append(form_content.strip())
848849
markdown_content = '\n\n'.join(parts)
850+
plain_content = '\n\n'.join(plain_parts)
849851

850852
keyboard = build_keyboard_from_select_field(form_data) if is_field_step else None
851-
if is_field_step and not keyboard.get('content', {}).get('rows'):
852-
field_parts = parts[:-1] if len(parts) > 1 else parts
853-
text_msg = platform_message.MessageChain([platform_message.Plain(text='\n\n'.join(field_parts))])
854-
try:
855-
await self.reply_message(message_source, text_msg)
856-
except Exception:
857-
await self.logger.error(f'QQ Official: field-step text send failed: {traceback.format_exc()}')
858-
return
859-
860-
sender_id = ''
861-
if source is not None:
862-
sender_id = (
863-
getattr(source, 'user_openid', None)
864-
or getattr(source, 'member_openid', None)
865-
or getattr(source, 'd_author_id', None)
866-
or ''
867-
)
868-
if not sender_id and message_source.sender is not None:
869-
sender_id = str(getattr(message_source.sender, 'id', '') or '')
870-
self._pending_forms[session_key] = {
871-
'form_data': form_data,
872-
'msg_id': msg_id,
873-
'sender_id': sender_id,
874-
'target_type': target_type,
875-
'target_id': target_id,
876-
'source_event_t': source.t if source is not None else None,
877-
'posted_at': time.time(),
878-
}
879-
await self.logger.info(
880-
f'QQ Official: form field step posted session={session_key} '
881-
f'field={form_data.get("_current_input_field")!r}'
882-
)
883-
return
884-
885-
if keyboard is None:
853+
is_text_field_step = is_field_step and not keyboard.get('content', {}).get('rows')
854+
if is_text_field_step:
855+
keyboard = None
856+
if keyboard is None and not is_text_field_step:
886857
keyboard = build_keyboard_from_form(form_data, buttons_per_row=2)
887-
if not keyboard.get('content', {}).get('rows'):
858+
if keyboard is not None and not keyboard.get('content', {}).get('rows') and not is_text_field_step:
888859
# No actions to render — fall back to plain text.
889-
text_msg = platform_message.MessageChain([platform_message.Plain(text=markdown_content)])
860+
text_msg = platform_message.MessageChain([platform_message.Plain(text=plain_content)])
890861
try:
891862
await self.reply_message(message_source, text_msg)
892863
except Exception:
@@ -935,7 +906,7 @@ async def _handle_form_chunk(
935906
await self.logger.error(
936907
f'QQ Official: send_markdown_keyboard failed, falling back to text: {traceback.format_exc()}'
937908
)
938-
text_msg = platform_message.MessageChain([platform_message.Plain(text=markdown_content)])
909+
text_msg = platform_message.MessageChain([platform_message.Plain(text=plain_content)])
939910
try:
940911
await self.reply_message(message_source, text_msg)
941912
except Exception:

src/langbot/pkg/platform/sources/telegram.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ async def callback_query_handler(update: Update, context: ContextTypes.DEFAULT_T
304304
action_title = self._form_action_titles[query.data]
305305
try:
306306
original_text = query.message.text or ''
307-
selected_text = f'{original_text}\n\nSelected: {action_title}'
307+
selected_text = f'{original_text}\n\n{action_title}'
308308
await query.edit_message_text(text=selected_text, reply_markup=None)
309309
except Exception:
310310
# If edit fails (e.g. message too long), just pass
@@ -687,15 +687,10 @@ async def _send_form_action_buttons(
687687
effective_message = update.effective_message
688688
message_thread_id = getattr(effective_message, 'message_thread_id', None) if effective_message else None
689689

690-
if is_select_field:
691-
prompt = f'Please select {select_field}:'
692-
elif is_field_step:
693-
prompt = f'Please reply with a value for {current_field}.'
694-
else:
695-
prompt = 'Please select an action:'
696-
text_lines = [f'[{node_title}] {prompt}']
690+
heading = f'[{node_title}]'
691+
text_lines = [heading]
697692
if form_content:
698-
text_lines.insert(0, form_content)
693+
text_lines.append(form_content)
699694

700695
if oversized:
701696
# callback_data exceeds Telegram's 64-byte limit — fall back to
@@ -722,7 +717,7 @@ async def _send_form_action_buttons(
722717
# prompts even when they cannot read ordinary group messages.
723718
'reply_markup': ForceReply(
724719
selective=False,
725-
input_field_placeholder=f'Enter {current_field}',
720+
input_field_placeholder=current_field,
726721
),
727722
}
728723
else:

tests/unit_tests/platform/test_dingtalk_adapter.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ def test_dingtalk_completed_input_lines_include_text_and_select_values():
9393
}
9494
)
9595

96-
assert lines == ['✅ 已填写 comment:looks good', '✅ 已选择 choice:B']
96+
assert lines == ['✅ comment:looks good', '✅ choice:B']
9797

9898

9999
def test_dingtalk_clean_form_content_uses_all_input_defs():

tests/unit_tests/platform/test_lark_adapter.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,9 @@ def test_lark_completed_input_lines_include_text_select_and_files():
109109
)
110110

111111
assert lines == [
112-
'✅ 已填写 us_input:**你好**',
113-
'✅ 已选择 xiala:**or**',
114-
'✅ 已上传 files:**2 file(s)**',
112+
'✅ us_input:你好',
113+
'✅ xiala:or',
114+
'✅ files:2 file(s)',
115115
]
116116

117117

@@ -193,4 +193,4 @@ def test_lark_completed_input_lines_display_select_value_from_object():
193193
}
194194
)
195195

196-
assert lines == ['✅ 已选择 xiala:**B**']
196+
assert lines == ['✅ xiala:B']

tests/unit_tests/platform/test_qqofficial_api.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ def _stream_test_adapter():
7575
adapter.logger = AsyncMock()
7676
adapter.bot = MagicMock()
7777
adapter.bot.send_stream_msg = AsyncMock(return_value={'id': 'stream-1'})
78+
adapter.bot.send_markdown_keyboard = AsyncMock(return_value={'id': 'message-1'})
7879
adapter.ap = None
7980
adapter._stream_ctx = {}
8081
adapter._stream_ctx_ts = {}
@@ -142,6 +143,36 @@ async def test_qq_non_streaming_fallback_keeps_latest_snapshot_only():
142143
assert str(sent_chain) == 'Hello'
143144

144145

146+
@pytest.mark.asyncio
147+
async def test_qq_text_field_prompt_keeps_form_content():
148+
from langbot.pkg.platform.sources.qqofficial import QQOfficialAdapter
149+
150+
adapter = _stream_test_adapter()
151+
adapter._pending_forms = {}
152+
adapter._session_event_ids = {}
153+
adapter._anchor_msg_seq = {}
154+
source = MagicMock()
155+
source.d_id = 'source-1'
156+
source.t = 'C2C_MESSAGE_CREATE'
157+
event = MagicMock()
158+
event.source_platform_object = source
159+
event.sender.id = 'user-1'
160+
form_data = {
161+
'_current_input_field': 'us_input',
162+
'node_title': 'Manual input',
163+
'form_content': '1234\nEnter your question',
164+
'input_defs': [{'output_variable_name': 'us_input', 'type': 'paragraph'}],
165+
'actions': [{'id': 'yes', 'title': 'yes'}],
166+
}
167+
168+
with patch.object(QQOfficialAdapter, '_resolve_target_from_event', return_value=('c2c', 'user-1')):
169+
await adapter._handle_form_chunk(event, platform_message.MessageChain([]), form_data)
170+
171+
send_call = adapter.bot.send_markdown_keyboard.await_args.kwargs
172+
assert send_call['markdown_content'] == '### Manual input\n\n1234\nEnter your question'
173+
assert send_call['keyboard'] is None
174+
175+
145176
@pytest.mark.asyncio
146177
async def test_qq_select_click_enqueues_input_progress_query():
147178
import langbot.pkg.core.app # noqa: F401

0 commit comments

Comments
 (0)