mirror of
https://github.com/NCBM/plyngent.git
synced 2026-07-24 06:54:57 +08:00
core/agent+cli: reasoning deltas, /stream and /verbose
Decode and stream reasoning_content as ReasoningDeltaEvent; keep it on assistant messages. CLI shows dim reasoning, toggles stream/verbose, and prints full tool results when verbose.
This commit is contained in:
@@ -9,6 +9,7 @@ from plyngent.agent import (
|
||||
AssistantMessageEvent,
|
||||
ChatAgent,
|
||||
MaxRoundsEvent,
|
||||
ReasoningDeltaEvent,
|
||||
TextDeltaEvent,
|
||||
ToolCallEvent,
|
||||
ToolRegistry,
|
||||
@@ -338,6 +339,78 @@ async def test_stream_yields_deltas_incrementally() -> None:
|
||||
assert messages[-1].content == "ab"
|
||||
|
||||
|
||||
async def test_stream_yields_reasoning_deltas() -> None:
|
||||
class ReasoningClient:
|
||||
@overload
|
||||
async def chat_completions(
|
||||
self, param: ChatCompletionsParam, *, stream: Literal[False] = False
|
||||
) -> ChatCompletionResponse: ...
|
||||
|
||||
@overload
|
||||
async def chat_completions(
|
||||
self, param: ChatCompletionsParam, *, stream: Literal[True]
|
||||
) -> AsyncIterator[ChatCompletionChunk]: ...
|
||||
|
||||
async def chat_completions(
|
||||
self, param: ChatCompletionsParam, *, stream: bool = False
|
||||
) -> ChatCompletionResponse | AsyncIterator[ChatCompletionChunk]:
|
||||
del param
|
||||
if not stream:
|
||||
return _response(AssistantChatMessage(content="ans", reasoning_content="think"))
|
||||
|
||||
async def chunks() -> AsyncIterator[ChatCompletionChunk]:
|
||||
for part in ("th", "ink"):
|
||||
yield ChatCompletionChunk(
|
||||
id="1",
|
||||
object="chat.completion.chunk",
|
||||
created=0,
|
||||
model="t",
|
||||
choices=[
|
||||
ChunkChoice(
|
||||
index=0,
|
||||
delta=DeltaMessage(reasoning_content=part),
|
||||
finish_reason=None,
|
||||
)
|
||||
],
|
||||
)
|
||||
yield ChatCompletionChunk(
|
||||
id="1",
|
||||
object="chat.completion.chunk",
|
||||
created=0,
|
||||
model="t",
|
||||
choices=[
|
||||
ChunkChoice(
|
||||
index=0,
|
||||
delta=DeltaMessage(content="ans"),
|
||||
finish_reason="stop",
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
return chunks()
|
||||
|
||||
messages: list[AnyChatMessage] = [UserChatMessage(content="hi")]
|
||||
events = [e async for e in run_chat_loop(ReasoningClient(), messages, model="m", stream=True)]
|
||||
reasoning = [e for e in events if isinstance(e, ReasoningDeltaEvent)]
|
||||
assert [r.content for r in reasoning] == ["th", "ink"]
|
||||
deltas = [e for e in events if isinstance(e, TextDeltaEvent)]
|
||||
assert [d.content for d in deltas] == ["ans"]
|
||||
assert isinstance(messages[-1], AssistantChatMessage)
|
||||
assert messages[-1].content == "ans"
|
||||
assert messages[-1].reasoning_content == "think"
|
||||
|
||||
|
||||
async def test_non_stream_yields_reasoning() -> None:
|
||||
client = ScriptedClient([_response(AssistantChatMessage(content="ok", reasoning_content="plan"))])
|
||||
messages: list[AnyChatMessage] = [UserChatMessage(content="hi")]
|
||||
events = [e async for e in run_chat_loop(client, messages, model="m", stream=False)]
|
||||
reasoning = [e for e in events if isinstance(e, ReasoningDeltaEvent)]
|
||||
assert len(reasoning) == 1
|
||||
assert reasoning[0].content == "plan"
|
||||
assert isinstance(messages[-1], AssistantChatMessage)
|
||||
assert messages[-1].reasoning_content == "plan"
|
||||
|
||||
|
||||
async def test_run_chat_loop_with_tools() -> None:
|
||||
@tool
|
||||
def add(a: int, b: int) -> int:
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from plyngent.agent import ReasoningDeltaEvent, TextDeltaEvent, ToolResultEvent
|
||||
from plyngent.cli.display import render_events, set_verbose_tool_results
|
||||
from plyngent.lmproto.openai_compatible.model import ToolChatMessage
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import AsyncIterator
|
||||
|
||||
import pytest
|
||||
|
||||
from plyngent.agent import AgentEvent
|
||||
|
||||
|
||||
async def _aiter(events: list[AgentEvent]) -> AsyncIterator[AgentEvent]:
|
||||
for event in events:
|
||||
yield event
|
||||
|
||||
|
||||
async def test_render_reasoning_and_text(capsys: pytest.CaptureFixture[str]) -> None:
|
||||
await render_events(
|
||||
_aiter(
|
||||
[
|
||||
ReasoningDeltaEvent(content="think"),
|
||||
TextDeltaEvent(content="hello"),
|
||||
]
|
||||
)
|
||||
)
|
||||
out = capsys.readouterr().out
|
||||
assert "reasoning: " in out
|
||||
assert "think" in out
|
||||
assert "assistant: " in out
|
||||
assert "hello" in out
|
||||
|
||||
|
||||
async def test_tool_result_preview_vs_verbose(capsys: pytest.CaptureFixture[str]) -> None:
|
||||
long = "x" * 200
|
||||
msg = ToolChatMessage(content=long, tool_call_id="1")
|
||||
set_verbose_tool_results(False)
|
||||
await render_events(_aiter([ToolResultEvent(message=msg)]))
|
||||
out = capsys.readouterr().out
|
||||
assert "…" in out
|
||||
assert long not in out
|
||||
|
||||
set_verbose_tool_results(True)
|
||||
await render_events(_aiter([ToolResultEvent(message=msg)]), verbose=True)
|
||||
out2 = capsys.readouterr().out
|
||||
assert long in out2
|
||||
set_verbose_tool_results(False)
|
||||
@@ -77,6 +77,8 @@ def test_completer_commands(tmp_path: object, monkeypatch: object) -> None:
|
||||
found.append(item)
|
||||
index += 1
|
||||
assert "/help" in found
|
||||
assert "/stream" in SLASH_COMMANDS
|
||||
assert "/verbose" in SLASH_COMMANDS
|
||||
assert set(found) <= set(SLASH_COMMANDS)
|
||||
|
||||
|
||||
|
||||
@@ -116,6 +116,27 @@ async def test_tools_toggle(state: ReplState) -> None:
|
||||
assert state.tools_enabled is False
|
||||
|
||||
|
||||
async def test_stream_toggle(state: ReplState) -> None:
|
||||
assert state.agent.stream is True
|
||||
assert await handle_slash(state, "/stream off") is True
|
||||
assert state.agent.stream is False
|
||||
assert state.stream_enabled is False
|
||||
assert await handle_slash(state, "/stream on") is True
|
||||
assert state.agent.stream is True
|
||||
|
||||
|
||||
async def test_verbose_toggle(state: ReplState) -> None:
|
||||
from plyngent.cli.display import get_verbose_tool_results
|
||||
|
||||
assert state.verbose is False
|
||||
assert await handle_slash(state, "/verbose on") is True
|
||||
assert state.verbose is True
|
||||
assert get_verbose_tool_results() is True
|
||||
assert await handle_slash(state, "/verbose off") is True
|
||||
assert state.verbose is False
|
||||
assert get_verbose_tool_results() is False
|
||||
|
||||
|
||||
async def test_resume(state: ReplState) -> None:
|
||||
sid = state.session_id
|
||||
assert sid is not None
|
||||
|
||||
Reference in New Issue
Block a user