mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-11-01 05:15:22 +08:00
fix(agent): avoid duplicate replies and enable LocalAgent cards
This commit is contained in:
@@ -310,6 +310,7 @@ spec:
|
||||
skill_authoring: true
|
||||
interrupt: true
|
||||
steering: true
|
||||
interactions: true
|
||||
permissions:
|
||||
models:
|
||||
- count_tokens
|
||||
@@ -324,6 +325,8 @@ spec:
|
||||
- retrieve
|
||||
history:
|
||||
- page
|
||||
interactions:
|
||||
- request
|
||||
execution:
|
||||
python:
|
||||
path: default.py
|
||||
|
||||
@@ -3,7 +3,7 @@ kind: Plugin
|
||||
metadata:
|
||||
author: langbot-team
|
||||
name: LocalAgent
|
||||
version: 0.2.4
|
||||
version: 0.2.5
|
||||
repository: https://github.com/langbot-app/LangBot/tree/master/plugins/Runner/LocalAgent
|
||||
label:
|
||||
en_US: Local Agent
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "langbot-local-agent"
|
||||
version = "0.2.4"
|
||||
version = "0.2.5"
|
||||
description = "Built-in agent with model fallback, tools, and knowledge retrieval"
|
||||
requires-python = ">=3.11,<4.0"
|
||||
dependencies = [
|
||||
|
||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
import time
|
||||
import asyncio
|
||||
import contextlib
|
||||
import re
|
||||
import copy
|
||||
import typing
|
||||
from uuid import uuid4
|
||||
@@ -50,6 +51,14 @@ from ...telemetry.execution import close_trace as close_execution_trace
|
||||
from ...telemetry.execution import record as record_execution
|
||||
|
||||
|
||||
def _visible_auto_reply_text(content: str) -> str:
|
||||
visible = re.sub(r'<think>.*?(?:</think>|$)', '', content, flags=re.IGNORECASE | re.DOTALL)
|
||||
for size in range(min(7, len(visible)), 0, -1):
|
||||
if any(tag.startswith(visible[-size:].lower()) for tag in ('<think>', '</think>')):
|
||||
return visible[:-size]
|
||||
return visible
|
||||
|
||||
|
||||
class AgentRunOrchestrator:
|
||||
"""Coordinate one Runner execution.
|
||||
|
||||
@@ -266,6 +275,7 @@ class AgentRunOrchestrator:
|
||||
auto_reply_cumulative_text = ''
|
||||
auto_reply_prefix = ''
|
||||
auto_reply_deferred = False
|
||||
explicit_event_reply_sent = False
|
||||
explicit_reply_text: str | None = None
|
||||
pending_explicit_replies: dict[str, str] = {}
|
||||
# A run reached without a platform ingress (WebUI debug, service API)
|
||||
@@ -410,6 +420,8 @@ class AgentRunOrchestrator:
|
||||
and tool_definition.api == 'send_message'
|
||||
and not result_data.get('error')
|
||||
)
|
||||
if auto_reply_deferred and result_data.get('tool_name') == 'event_reply':
|
||||
explicit_event_reply_sent = True
|
||||
explicit_reply_text = pending_explicit_replies.pop(
|
||||
str(result_data.get('tool_call_id') or ''), None
|
||||
)
|
||||
@@ -500,15 +512,17 @@ class AgentRunOrchestrator:
|
||||
async with binding_for(execution_query).lock:
|
||||
result.attachments = exported_message(execution_query, file_ids, consume=True)
|
||||
|
||||
if auto_reply_enabled and result is not None:
|
||||
if auto_reply_enabled and not explicit_event_reply_sent and result is not None:
|
||||
if result_type == 'message.delta' and isinstance(result, provider_message.MessageChunk):
|
||||
cumulative_text = result.all_content or result.content
|
||||
if isinstance(cumulative_text, str):
|
||||
reply_text = _visible_auto_reply_text(cumulative_text)
|
||||
if auto_reply_prefix and cumulative_text.startswith(auto_reply_prefix):
|
||||
reply_text = cumulative_text[len(auto_reply_prefix) :]
|
||||
visible_prefix = _visible_auto_reply_text(auto_reply_prefix)
|
||||
if reply_text.startswith(visible_prefix):
|
||||
reply_text = reply_text[len(visible_prefix) :]
|
||||
else:
|
||||
auto_reply_prefix = ''
|
||||
reply_text = cumulative_text
|
||||
auto_reply_cumulative_text = cumulative_text
|
||||
auto_reply_stream_text = reply_text
|
||||
if reply_text.strip() and not auto_reply_deferred:
|
||||
@@ -523,8 +537,11 @@ class AgentRunOrchestrator:
|
||||
elif has_completed_message:
|
||||
reply_text = result.content
|
||||
if isinstance(reply_text, str):
|
||||
if auto_reply_prefix and reply_text.startswith(auto_reply_prefix):
|
||||
reply_text = reply_text[len(auto_reply_prefix) :]
|
||||
reply_text = _visible_auto_reply_text(reply_text)
|
||||
if auto_reply_prefix and result.content.startswith(auto_reply_prefix):
|
||||
visible_prefix = _visible_auto_reply_text(auto_reply_prefix)
|
||||
if reply_text.startswith(visible_prefix):
|
||||
reply_text = reply_text[len(visible_prefix) :]
|
||||
if reply_text.strip() and (
|
||||
not auto_reply_deferred
|
||||
or explicit_reply_text is None
|
||||
|
||||
@@ -1272,8 +1272,9 @@ class TestQueryEntrySessionQueryId:
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize('event_type', ['message.received', 'interaction.submitted'])
|
||||
@pytest.mark.parametrize('completed_message', [False, True])
|
||||
@pytest.mark.parametrize('reasoning', [False, True])
|
||||
async def test_direct_agent_streams_output_to_the_originating_platform(
|
||||
clean_agent_state, event_type, completed_message
|
||||
clean_agent_state, event_type, completed_message, reasoning
|
||||
):
|
||||
from langbot.pkg.agent.runner.host_models import (
|
||||
AgentBinding,
|
||||
@@ -1286,14 +1287,16 @@ async def test_direct_agent_streams_output_to_the_originating_platform(
|
||||
from langbot_plugin.api.entities.builtin.runner.delivery import DeliveryContext
|
||||
from langbot_plugin.api.entities.builtin.runner.input import AgentInput
|
||||
|
||||
first_content = '<think>Checking the request</think>hello' if reasoning else 'hello'
|
||||
final_content = '<think>Checking the request</think>hello world' if reasoning else 'hello world'
|
||||
runner_results = [
|
||||
{
|
||||
'type': 'message.delta',
|
||||
'data': {
|
||||
'chunk': {
|
||||
'role': 'assistant',
|
||||
'content': 'hello',
|
||||
'all_content': 'hello',
|
||||
'content': first_content,
|
||||
'all_content': first_content,
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -1302,7 +1305,7 @@ async def test_direct_agent_streams_output_to_the_originating_platform(
|
||||
runner_results.append(
|
||||
{
|
||||
'type': 'message.completed',
|
||||
'data': {'message': {'role': 'assistant', 'content': 'hello world'}},
|
||||
'data': {'message': {'role': 'assistant', 'content': final_content}},
|
||||
}
|
||||
)
|
||||
runner_results.append({'type': 'run.completed', 'data': {'finish_reason': 'stop'}})
|
||||
@@ -1363,7 +1366,9 @@ async def test_direct_agent_streams_output_to_the_originating_platform(
|
||||
)
|
||||
]
|
||||
|
||||
assert [item.content for item in results] == (['hello', 'hello world'] if completed_message else ['hello'])
|
||||
assert [item.content for item in results] == (
|
||||
[first_content, final_content] if completed_message else [first_content]
|
||||
)
|
||||
if event_type == 'message.received':
|
||||
adapter.create_message_card.assert_awaited_once()
|
||||
chunks = adapter.reply_message_chunk.await_args_list
|
||||
@@ -1398,6 +1403,27 @@ async def test_direct_agent_delivers_text_after_card_without_repeating_explicit_
|
||||
results = []
|
||||
if tool_name == 'present_interactive_card':
|
||||
results.append({'type': 'message.delta', 'data': {'chunk': {'role': 'assistant', 'content': 'Before'}}})
|
||||
else:
|
||||
results.extend(
|
||||
[
|
||||
{
|
||||
'type': 'message.delta',
|
||||
'data': {'chunk': {'role': 'assistant', 'content': '<thi'}},
|
||||
},
|
||||
{
|
||||
'type': 'message.delta',
|
||||
'data': {'chunk': {'role': 'assistant', 'content': '<think>Checking available tools'}},
|
||||
},
|
||||
{
|
||||
'type': 'tool.call.completed',
|
||||
'data': {'tool_call_id': 'skill-1', 'tool_name': 'list_skills', 'result': {'skills': []}},
|
||||
},
|
||||
{
|
||||
'type': 'message.delta',
|
||||
'data': {'chunk': {'role': 'assistant', 'content': '<think>Checking available tools</think>'}},
|
||||
},
|
||||
]
|
||||
)
|
||||
results.extend(
|
||||
[
|
||||
{
|
||||
@@ -1412,6 +1438,13 @@ async def test_direct_agent_delivers_text_after_card_without_repeating_explicit_
|
||||
)
|
||||
if tool_name == 'present_interactive_card':
|
||||
results.append({'type': 'message.delta', 'data': {'chunk': {'role': 'assistant', 'content': 'BeforeAfter'}}})
|
||||
else:
|
||||
results.append(
|
||||
{
|
||||
'type': 'message.delta',
|
||||
'data': {'chunk': {'role': 'assistant', 'content': '<think>Checking available tools</think>After'}},
|
||||
}
|
||||
)
|
||||
results.extend(
|
||||
[
|
||||
{'type': 'message.completed', 'data': {'message': {'role': 'assistant', 'content': 'After'}}},
|
||||
|
||||
Reference in New Issue
Block a user