mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-22 20:36:08 +00:00
fix(platform): resolve post-rebase compatibility
This commit is contained in:
@@ -0,0 +1,101 @@
|
|||||||
|
"""Shared presentation helpers for platform human-input prompts."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
import typing
|
||||||
|
|
||||||
|
|
||||||
|
def _field_name(field: dict[str, typing.Any]) -> str:
|
||||||
|
return str(field.get('output_variable_name') or '').strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _field_type(field: dict[str, typing.Any]) -> str:
|
||||||
|
return str(field.get('type') or 'text').strip().lower()
|
||||||
|
|
||||||
|
|
||||||
|
def _select_options(field: dict[str, typing.Any]) -> list[str]:
|
||||||
|
source = field.get('option_source') or {}
|
||||||
|
value = source.get('value') if isinstance(source, dict) else None
|
||||||
|
if isinstance(value, list):
|
||||||
|
return [str(item) for item in value]
|
||||||
|
if isinstance(value, str):
|
||||||
|
return [part.strip() for part in value.splitlines() if part.strip()]
|
||||||
|
options = field.get('options')
|
||||||
|
if not isinstance(options, list):
|
||||||
|
return []
|
||||||
|
normalized = []
|
||||||
|
for item in options:
|
||||||
|
if isinstance(item, dict):
|
||||||
|
normalized.append(str(item.get('label') or item.get('value') or ''))
|
||||||
|
else:
|
||||||
|
normalized.append(str(item))
|
||||||
|
return [item for item in normalized if item]
|
||||||
|
|
||||||
|
|
||||||
|
def _format_fields(input_defs: list[dict[str, typing.Any]]) -> str:
|
||||||
|
if not input_defs:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
lines = ['Fields:']
|
||||||
|
for field in input_defs:
|
||||||
|
name = _field_name(field)
|
||||||
|
typ = _field_type(field)
|
||||||
|
if typ == 'select':
|
||||||
|
options = _select_options(field)
|
||||||
|
option_text = ', '.join(f'{idx}. {value}' for idx, value in enumerate(options, start=1))
|
||||||
|
lines.append(f' - {name} (select): {option_text or "choose one option"}')
|
||||||
|
elif typ in {'file', 'file-list'}:
|
||||||
|
limit = field.get('number_limits') if typ == 'file-list' else 1
|
||||||
|
allowed_types = ', '.join(field.get('allowed_file_types') or [])
|
||||||
|
suffix = f', up to {limit}' if typ == 'file-list' and limit else ''
|
||||||
|
allowed = f' ({allowed_types})' if allowed_types else ''
|
||||||
|
lines.append(f' - {name} ({typ}{allowed}{suffix}): upload file(s) or reply "{name}: <url>"')
|
||||||
|
else:
|
||||||
|
lines.append(f' - {name} ({typ}): reply "{name}: <value>"')
|
||||||
|
|
||||||
|
lines.append('You can reply with one or more lines like "field_name: value".')
|
||||||
|
return '\n'.join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def _strip_field_placeholders(form_content: str, input_defs: list[dict[str, typing.Any]]) -> str:
|
||||||
|
if not form_content:
|
||||||
|
return ''
|
||||||
|
field_names = {_field_name(field) for field in input_defs if _field_name(field)}
|
||||||
|
kept_lines = []
|
||||||
|
for line in form_content.splitlines():
|
||||||
|
placeholder = re.fullmatch(r'\s*\{\{#\$output\.([^#{}]+)#\}\}\s*', line)
|
||||||
|
if placeholder and placeholder.group(1) in field_names:
|
||||||
|
continue
|
||||||
|
kept_lines.append(line)
|
||||||
|
return re.sub(r'\n{3,}', '\n\n', '\n'.join(kept_lines).strip())
|
||||||
|
|
||||||
|
|
||||||
|
def format_human_input_text(
|
||||||
|
node_title: str,
|
||||||
|
form_content: str,
|
||||||
|
actions: list[dict[str, typing.Any]],
|
||||||
|
input_defs: list[dict[str, typing.Any]] | None = None,
|
||||||
|
) -> str:
|
||||||
|
"""Render a platform-neutral plain-text prompt for a paused workflow."""
|
||||||
|
input_defs = input_defs or []
|
||||||
|
form_content = _strip_field_placeholders(form_content, input_defs)
|
||||||
|
lines = [f'[Human Input Required] {node_title or ""}'.rstrip()]
|
||||||
|
if form_content:
|
||||||
|
lines.extend(['', form_content])
|
||||||
|
field_help = _format_fields(input_defs)
|
||||||
|
if field_help:
|
||||||
|
lines.extend(['', field_help])
|
||||||
|
if actions:
|
||||||
|
lines.append('')
|
||||||
|
if input_defs:
|
||||||
|
lines.extend([
|
||||||
|
'Reply with action plus field values to continue:',
|
||||||
|
' action: <number or title>',
|
||||||
|
])
|
||||||
|
else:
|
||||||
|
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)
|
||||||
@@ -8,10 +8,12 @@ import pydantic
|
|||||||
import traceback
|
import traceback
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from ..core import app
|
|
||||||
import langbot_plugin.api.entities.builtin.platform.message as platform_message
|
import langbot_plugin.api.entities.builtin.platform.message as platform_message
|
||||||
import langbot_plugin.api.definition.abstract.platform.event_logger as abstract_platform_event_logger
|
import langbot_plugin.api.definition.abstract.platform.event_logger as abstract_platform_event_logger
|
||||||
|
|
||||||
|
if typing.TYPE_CHECKING:
|
||||||
|
from ..core import app
|
||||||
|
|
||||||
|
|
||||||
class EventLogLevel(enum.Enum):
|
class EventLogLevel(enum.Enum):
|
||||||
"""日志级别"""
|
"""日志级别"""
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import langbot_plugin.api.entities.builtin.provider.session as provider_session
|
|||||||
from langbot.libs.dingtalk_api.api import DingTalkClient
|
from langbot.libs.dingtalk_api.api import DingTalkClient
|
||||||
import datetime
|
import datetime
|
||||||
from langbot.pkg.platform.logger import EventLogger
|
from langbot.pkg.platform.logger import EventLogger
|
||||||
from langbot.pkg.provider.runners.difysvapi import _format_human_input_text
|
from langbot.pkg.platform.human_input import format_human_input_text
|
||||||
|
|
||||||
|
|
||||||
class DingTalkMessageConverter(abstract_platform_adapter.AbstractMessageConverter):
|
class DingTalkMessageConverter(abstract_platform_adapter.AbstractMessageConverter):
|
||||||
@@ -1171,7 +1171,7 @@ class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
parts.append(form_content)
|
parts.append(form_content)
|
||||||
display_text = '\n\n'.join(parts)
|
display_text = '\n\n'.join(parts)
|
||||||
else:
|
else:
|
||||||
display_text = _format_human_input_text(
|
display_text = format_human_input_text(
|
||||||
form_data.get('node_title', ''),
|
form_data.get('node_title', ''),
|
||||||
form_data.get('form_content', ''),
|
form_data.get('form_content', ''),
|
||||||
form_data.get('actions', []) or [],
|
form_data.get('actions', []) or [],
|
||||||
|
|||||||
@@ -444,9 +444,9 @@ class WecomBotAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
'WeComBot: cannot register form pause (no active stream session); falling back to plain text'
|
'WeComBot: cannot register form pause (no active stream session); falling back to plain text'
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
from langbot.pkg.provider.runners.difysvapi import _format_human_input_text
|
from langbot.pkg.platform.human_input import format_human_input_text
|
||||||
|
|
||||||
fallback = _format_human_input_text(
|
fallback = format_human_input_text(
|
||||||
form_data.get('node_title', ''),
|
form_data.get('node_title', ''),
|
||||||
form_data.get('form_content', ''),
|
form_data.get('form_content', ''),
|
||||||
form_data.get('actions', []) or [],
|
form_data.get('actions', []) or [],
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
from langbot.pkg.platform.human_input import format_human_input_text
|
||||||
|
|
||||||
|
|
||||||
|
def test_format_human_input_text_renders_fields_actions_and_strips_placeholders():
|
||||||
|
result = format_human_input_text(
|
||||||
|
'Approval',
|
||||||
|
'Review this request\n{{#$output.choice#}}',
|
||||||
|
[{'id': 'approve', 'title': 'Approve'}],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
'output_variable_name': 'choice',
|
||||||
|
'type': 'select',
|
||||||
|
'option_source': {'value': ['Yes', 'No']},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.startswith('[Human Input Required] Approval')
|
||||||
|
assert '{{#$output.choice#}}' not in result
|
||||||
|
assert 'choice (select): 1. Yes, 2. No' in result
|
||||||
|
assert 'action: <number or title>' in result
|
||||||
|
assert '1. Approve' in result
|
||||||
|
|
||||||
|
|
||||||
|
def test_format_human_input_text_supports_plain_action_prompt():
|
||||||
|
result = format_human_input_text('', '', [{'id': 'continue'}])
|
||||||
|
|
||||||
|
assert result == (
|
||||||
|
'[Human Input Required]\n\n'
|
||||||
|
'Reply with the number or title to continue:\n'
|
||||||
|
' 1. continue'
|
||||||
|
)
|
||||||
@@ -109,6 +109,7 @@ async def test_handle_websocket_message_marks_event_with_pipeline_uuid():
|
|||||||
connection = SimpleNamespace(
|
connection = SimpleNamespace(
|
||||||
pipeline_uuid='pipeline-123',
|
pipeline_uuid='pipeline-123',
|
||||||
session_type='person',
|
session_type='person',
|
||||||
|
session_id=None,
|
||||||
connection_id='conn-1',
|
connection_id='conn-1',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user