fix(gateway_tools,conversations): say refused in user turns, the reply reaches the human itself
This commit is contained in:
@@ -883,6 +883,11 @@ class Conversations:
|
|||||||
self._ensure_worker(cast("int", conv.id))
|
self._ensure_worker(cast("int", conv.id))
|
||||||
return item
|
return item
|
||||||
|
|
||||||
|
def turn_origin(self, conv: Conversation) -> str | None:
|
||||||
|
"""Origin of the running turn (``user``, ``inject``, ...); None when idle."""
|
||||||
|
runner = self._runners.get(cast("int", conv.id))
|
||||||
|
return runner.origin if runner is not None and runner.turn_id else None
|
||||||
|
|
||||||
async def say(self, conv: Conversation, text: str) -> dict[str, Any]:
|
async def say(self, conv: Conversation, text: str) -> dict[str, Any]:
|
||||||
runner = self._runners.get(cast("int", conv.id))
|
runner = self._runners.get(cast("int", conv.id))
|
||||||
_log.info("say[%s]: %s", conv.external_id, text[:200])
|
_log.info("say[%s]: %s", conv.external_id, text[:200])
|
||||||
|
|||||||
@@ -26,6 +26,11 @@ __all__ = ["SERVER_NAME", "TOOL_NAMES", "build_tool_server"]
|
|||||||
_log = logging.getLogger("beaver_gateway.core.gateway_tools")
|
_log = logging.getLogger("beaver_gateway.core.gateway_tools")
|
||||||
|
|
||||||
SERVER_NAME = "gateway"
|
SERVER_NAME = "gateway"
|
||||||
|
SAY_IN_USER_TURN = (
|
||||||
|
"not delivered: this turn was started by the human's message, so your "
|
||||||
|
"reply text reaches them by itself - put what you wanted to say into the "
|
||||||
|
"reply instead of repeating it here"
|
||||||
|
)
|
||||||
TOOL_NAMES = ("read_conversation", "spawn", "say", "schedule", "inject", "close_chat")
|
TOOL_NAMES = ("read_conversation", "spawn", "say", "schedule", "inject", "close_chat")
|
||||||
|
|
||||||
|
|
||||||
@@ -118,11 +123,14 @@ def _tools(conversations: Conversations, key: str) -> list[SdkMcpTool[Any]]:
|
|||||||
"say",
|
"say",
|
||||||
"Say something to the human in the frontend this conversation is bound "
|
"Say something to the human in the frontend this conversation is bound "
|
||||||
"to. The only way an inject-started turn can speak; silence is simply "
|
"to. The only way an inject-started turn can speak; silence is simply "
|
||||||
"not calling it.",
|
"not calling it. Refused in a turn started by the human's own message: "
|
||||||
|
"there your reply text reaches them by itself, so just write the reply.",
|
||||||
{"text": str},
|
{"text": str},
|
||||||
)
|
)
|
||||||
async def say(args: dict[str, Any]) -> dict[str, Any]:
|
async def say(args: dict[str, Any]) -> dict[str, Any]:
|
||||||
conv = await current()
|
conv = await current()
|
||||||
|
if conversations.turn_origin(conv) == "user":
|
||||||
|
return _error(SAY_IN_USER_TURN)
|
||||||
await conversations.say(conv, str(args["text"]))
|
await conversations.say(conv, str(args["text"]))
|
||||||
return _text("ok")
|
return _text("ok")
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ from beaver_gateway.agents.claude import ClaudeAgent, ClaudeOptions
|
|||||||
from beaver_gateway.backends.claude_sdk import ClaudeSdkBackend
|
from beaver_gateway.backends.claude_sdk import ClaudeSdkBackend
|
||||||
from beaver_gateway.core.bus import EventBus
|
from beaver_gateway.core.bus import EventBus
|
||||||
from beaver_gateway.core.conversations import Conversations, ConversationTexts, parse_at
|
from beaver_gateway.core.conversations import Conversations, ConversationTexts, parse_at
|
||||||
|
from beaver_gateway.core.gateway_tools import SAY_IN_USER_TURN, _tools
|
||||||
from beaver_gateway.core.registry import AgentRegistry
|
from beaver_gateway.core.registry import AgentRegistry
|
||||||
from beaver_gateway.core.sessions import SessionPool
|
from beaver_gateway.core.sessions import SessionPool
|
||||||
from beaver_gateway.core.transcript import (
|
from beaver_gateway.core.transcript import (
|
||||||
@@ -253,6 +254,43 @@ async def test_two_messages_run_one_at_a_time_in_order(world: World) -> None:
|
|||||||
assert row.session_id == ScriptedClient.instances[0].session_id
|
assert row.session_id == ScriptedClient.instances[0].session_id
|
||||||
|
|
||||||
|
|
||||||
|
async def test_say_is_refused_in_user_turns_and_works_in_inject_turns(
|
||||||
|
world: World,
|
||||||
|
) -> None:
|
||||||
|
conv = await world.conversations.create(kind="master", agent="a", origin="test")
|
||||||
|
say = next(
|
||||||
|
t for t in _tools(world.conversations, conv.external_id) if t.name == "say"
|
||||||
|
)
|
||||||
|
said: list[str] = []
|
||||||
|
|
||||||
|
async def watch() -> None:
|
||||||
|
async for event in world.bus.stream(conversation_id=conv.external_id):
|
||||||
|
if event["type"] == "say":
|
||||||
|
said.append(str(event["text"]))
|
||||||
|
|
||||||
|
watcher = asyncio.create_task(watch())
|
||||||
|
ScriptedClient.hold = asyncio.Event()
|
||||||
|
await world.conversations.post(conv, "hi")
|
||||||
|
await asyncio.sleep(0.2)
|
||||||
|
assert world.conversations.turn_origin(conv) == "user"
|
||||||
|
out = await say.handler({"text": "dup"})
|
||||||
|
assert out.get("is_error") and out["content"][0]["text"] == SAY_IN_USER_TURN
|
||||||
|
ScriptedClient.hold.set()
|
||||||
|
await world.settle(conv, 1)
|
||||||
|
assert world.conversations.turn_origin(conv) is None
|
||||||
|
ScriptedClient.hold = asyncio.Event()
|
||||||
|
await world.conversations.inject(conv, "tick", urgency="urgent", origin="крон")
|
||||||
|
await asyncio.sleep(0.2)
|
||||||
|
assert world.conversations.turn_origin(conv) == "inject"
|
||||||
|
out = await say.handler({"text": "psst"})
|
||||||
|
assert not out.get("is_error")
|
||||||
|
ScriptedClient.hold.set()
|
||||||
|
await world.settle(conv, 2)
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
watcher.cancel()
|
||||||
|
assert said == ["psst"]
|
||||||
|
|
||||||
|
|
||||||
async def test_urgent_interrupts_and_goes_first(world: World) -> None:
|
async def test_urgent_interrupts_and_goes_first(world: World) -> None:
|
||||||
conv = await world.conversations.create(kind="master", agent="a", origin="test")
|
conv = await world.conversations.create(kind="master", agent="a", origin="test")
|
||||||
ScriptedClient.hold = asyncio.Event()
|
ScriptedClient.hold = asyncio.Event()
|
||||||
|
|||||||
Reference in New Issue
Block a user