v1.2.2
Co-Authored-By: Quentin Torroba <quentin.torroba@mistral.ai> Co-Authored-By: Michel Thomazo <michel.thomazo@mistral.ai>
This commit is contained in:
parent
402e898f39
commit
2e1e15120d
32 changed files with 391 additions and 549 deletions
|
|
@ -426,7 +426,7 @@ class TestSessionManagement:
|
|||
class TestSessionUpdates:
|
||||
@pytest.mark.asyncio
|
||||
async def test_agent_message_chunk_structure(self, vibe_home_dir: Path) -> None:
|
||||
mock_env = get_mocking_env([mock_llm_chunk(content="Hi") for _ in range(2)])
|
||||
mock_env = get_mocking_env([mock_llm_chunk(content="Hi")])
|
||||
async for process in get_acp_agent_process(
|
||||
mock_env=mock_env, vibe_home=vibe_home_dir
|
||||
):
|
||||
|
|
@ -465,7 +465,6 @@ class TestSessionUpdates:
|
|||
@pytest.mark.asyncio
|
||||
async def test_tool_call_update_structure(self, vibe_home_dir: Path) -> None:
|
||||
mock_env = get_mocking_env([
|
||||
mock_llm_chunk(content="Hey"),
|
||||
mock_llm_chunk(
|
||||
tool_calls=[
|
||||
ToolCall(
|
||||
|
|
@ -475,14 +474,9 @@ class TestSessionUpdates:
|
|||
type="function",
|
||||
index=0,
|
||||
)
|
||||
],
|
||||
name="bash",
|
||||
finish_reason="tool_calls",
|
||||
),
|
||||
mock_llm_chunk(
|
||||
content="The files containing the pattern 'auth' are ...",
|
||||
finish_reason="stop",
|
||||
]
|
||||
),
|
||||
mock_llm_chunk(content="The files containing the pattern 'auth' are ..."),
|
||||
])
|
||||
async for process in get_acp_agent_process(
|
||||
mock_env=mock_env, vibe_home=vibe_home_dir
|
||||
|
|
@ -567,7 +561,6 @@ class TestToolCallStructure:
|
|||
self, vibe_home_grep_ask: Path
|
||||
) -> None:
|
||||
custom_results = [
|
||||
mock_llm_chunk(content="Hey"),
|
||||
mock_llm_chunk(
|
||||
tool_calls=[
|
||||
ToolCall(
|
||||
|
|
@ -578,10 +571,8 @@ class TestToolCallStructure:
|
|||
type="function",
|
||||
index=0,
|
||||
)
|
||||
],
|
||||
name="grep",
|
||||
finish_reason="tool_calls",
|
||||
),
|
||||
]
|
||||
)
|
||||
]
|
||||
mock_env = get_mocking_env(custom_results)
|
||||
async for process in get_acp_agent_process(
|
||||
|
|
@ -628,7 +619,6 @@ class TestToolCallStructure:
|
|||
self, vibe_home_grep_ask: Path
|
||||
) -> None:
|
||||
custom_results = [
|
||||
mock_llm_chunk(content="Hey"),
|
||||
mock_llm_chunk(
|
||||
tool_calls=[
|
||||
ToolCall(
|
||||
|
|
@ -639,13 +629,10 @@ class TestToolCallStructure:
|
|||
type="function",
|
||||
index=0,
|
||||
)
|
||||
],
|
||||
name="grep",
|
||||
finish_reason="tool_calls",
|
||||
),
|
||||
mock_llm_chunk(
|
||||
content="The search for 'auth' has been completed", finish_reason="stop"
|
||||
]
|
||||
),
|
||||
mock_llm_chunk(content="The search for 'auth' has been completed"),
|
||||
mock_llm_chunk(content="The file test.txt has been created"),
|
||||
]
|
||||
mock_env = get_mocking_env(custom_results)
|
||||
async for process in get_acp_agent_process(
|
||||
|
|
@ -692,7 +679,6 @@ class TestToolCallStructure:
|
|||
self, vibe_home_grep_ask: Path
|
||||
) -> None:
|
||||
custom_results = [
|
||||
mock_llm_chunk(content="Hey"),
|
||||
mock_llm_chunk(
|
||||
tool_calls=[
|
||||
ToolCall(
|
||||
|
|
@ -703,14 +689,11 @@ class TestToolCallStructure:
|
|||
type="function",
|
||||
index=0,
|
||||
)
|
||||
],
|
||||
name="grep",
|
||||
finish_reason="tool_calls",
|
||||
]
|
||||
),
|
||||
mock_llm_chunk(
|
||||
content="The search for 'auth' has not been performed, "
|
||||
"because you rejected the permission request",
|
||||
finish_reason="stop",
|
||||
"because you rejected the permission request"
|
||||
),
|
||||
]
|
||||
mock_env = get_mocking_env(custom_results)
|
||||
|
|
@ -759,7 +742,6 @@ class TestToolCallStructure:
|
|||
self, vibe_home_grep_ask: Path
|
||||
) -> None:
|
||||
custom_results = [
|
||||
mock_llm_chunk(content="Hey"),
|
||||
mock_llm_chunk(
|
||||
tool_calls=[
|
||||
ToolCall(
|
||||
|
|
@ -770,13 +752,10 @@ class TestToolCallStructure:
|
|||
type="function",
|
||||
index=0,
|
||||
)
|
||||
],
|
||||
name="grep",
|
||||
finish_reason="tool_calls",
|
||||
),
|
||||
mock_llm_chunk(
|
||||
content="The search for 'auth' has been completed", finish_reason="stop"
|
||||
]
|
||||
),
|
||||
mock_llm_chunk(content="The search for 'auth' has been completed"),
|
||||
mock_llm_chunk(content="The command sleep 3 has been run"),
|
||||
]
|
||||
mock_env = get_mocking_env(custom_results)
|
||||
async for process in get_acp_agent_process(
|
||||
|
|
@ -820,7 +799,6 @@ class TestToolCallStructure:
|
|||
self, vibe_home_grep_ask: Path
|
||||
) -> None:
|
||||
custom_results = [
|
||||
mock_llm_chunk(content="Hey"),
|
||||
mock_llm_chunk(
|
||||
tool_calls=[
|
||||
ToolCall(
|
||||
|
|
@ -831,14 +809,11 @@ class TestToolCallStructure:
|
|||
type="function",
|
||||
index=0,
|
||||
)
|
||||
],
|
||||
name="grep",
|
||||
finish_reason="tool_calls",
|
||||
]
|
||||
),
|
||||
mock_llm_chunk(
|
||||
content="The search for 'auth' has failed "
|
||||
"because the path does not exist",
|
||||
finish_reason="stop",
|
||||
"because the path does not exist"
|
||||
),
|
||||
]
|
||||
mock_env = get_mocking_env(custom_results)
|
||||
|
|
@ -894,7 +869,6 @@ class TestCancellationStructure:
|
|||
self, vibe_home_dir: Path
|
||||
) -> None:
|
||||
custom_results = [
|
||||
mock_llm_chunk(content="Hey"),
|
||||
mock_llm_chunk(
|
||||
tool_calls=[
|
||||
ToolCall(
|
||||
|
|
@ -906,14 +880,11 @@ class TestCancellationStructure:
|
|||
type="function",
|
||||
index=0,
|
||||
)
|
||||
],
|
||||
name="write_file",
|
||||
finish_reason="tool_calls",
|
||||
]
|
||||
),
|
||||
mock_llm_chunk(
|
||||
content="The file test.txt has not been created, "
|
||||
"because you cancelled the permission request",
|
||||
finish_reason="stop",
|
||||
"because you cancelled the permission request"
|
||||
),
|
||||
]
|
||||
mock_env = get_mocking_env(custom_results)
|
||||
|
|
|
|||
|
|
@ -22,13 +22,10 @@ from vibe.core.types import LLMChunk, LLMMessage, LLMUsage, Role
|
|||
@pytest.fixture
|
||||
def backend() -> FakeBackend:
|
||||
backend = FakeBackend(
|
||||
results=[
|
||||
LLMChunk(
|
||||
message=LLMMessage(role=Role.assistant, content="Hi"),
|
||||
finish_reason="end_turn",
|
||||
usage=LLMUsage(prompt_tokens=1, completion_tokens=1),
|
||||
)
|
||||
]
|
||||
LLMChunk(
|
||||
message=LLMMessage(role=Role.assistant, content="Hi"),
|
||||
usage=LLMUsage(prompt_tokens=1, completion_tokens=1),
|
||||
)
|
||||
)
|
||||
return backend
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ class TestACPInitialize:
|
|||
),
|
||||
)
|
||||
assert response.agentInfo == Implementation(
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="1.2.1"
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="1.2.2"
|
||||
)
|
||||
|
||||
assert response.authMethods == []
|
||||
|
|
@ -63,7 +63,7 @@ class TestACPInitialize:
|
|||
),
|
||||
)
|
||||
assert response.agentInfo == Implementation(
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="1.2.1"
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="1.2.2"
|
||||
)
|
||||
|
||||
assert response.authMethods is not None
|
||||
|
|
|
|||
|
|
@ -116,9 +116,9 @@ class TestMultiSessionCore:
|
|||
)
|
||||
session2 = acp_agent.sessions[session2_response.sessionId]
|
||||
|
||||
backend._chunks = [
|
||||
mock_llm_chunk(content="Response 1", finish_reason="stop"),
|
||||
mock_llm_chunk(content="Response 2", finish_reason="stop"),
|
||||
backend._streams = [
|
||||
[mock_llm_chunk(content="Response 1")],
|
||||
[mock_llm_chunk(content="Response 2")],
|
||||
]
|
||||
|
||||
async def run_session1():
|
||||
|
|
|
|||
|
|
@ -18,13 +18,10 @@ from vibe.core.types import LLMChunk, LLMMessage, LLMUsage, Role
|
|||
@pytest.fixture
|
||||
def backend() -> FakeBackend:
|
||||
backend = FakeBackend(
|
||||
results=[
|
||||
LLMChunk(
|
||||
message=LLMMessage(role=Role.assistant, content="Hi"),
|
||||
finish_reason="end_turn",
|
||||
usage=LLMUsage(prompt_tokens=1, completion_tokens=1),
|
||||
)
|
||||
]
|
||||
LLMChunk(
|
||||
message=LLMMessage(role=Role.assistant, content="Hi"),
|
||||
usage=LLMUsage(prompt_tokens=1, completion_tokens=1),
|
||||
)
|
||||
)
|
||||
return backend
|
||||
|
||||
|
|
|
|||
|
|
@ -17,13 +17,10 @@ from vibe.core.types import LLMChunk, LLMMessage, LLMUsage, Role
|
|||
@pytest.fixture
|
||||
def backend() -> FakeBackend:
|
||||
backend = FakeBackend(
|
||||
results=[
|
||||
LLMChunk(
|
||||
message=LLMMessage(role=Role.assistant, content="Hi"),
|
||||
finish_reason="end_turn",
|
||||
usage=LLMUsage(prompt_tokens=1, completion_tokens=1),
|
||||
)
|
||||
]
|
||||
LLMChunk(
|
||||
message=LLMMessage(role=Role.assistant, content="Hi"),
|
||||
usage=LLMUsage(prompt_tokens=1, completion_tokens=1),
|
||||
)
|
||||
)
|
||||
return backend
|
||||
|
||||
|
|
|
|||
|
|
@ -17,13 +17,10 @@ from vibe.core.types import LLMChunk, LLMMessage, LLMUsage, Role
|
|||
@pytest.fixture
|
||||
def backend() -> FakeBackend:
|
||||
backend = FakeBackend(
|
||||
results=[
|
||||
LLMChunk(
|
||||
message=LLMMessage(role=Role.assistant, content="Hi"),
|
||||
finish_reason="end_turn",
|
||||
usage=LLMUsage(prompt_tokens=1, completion_tokens=1),
|
||||
)
|
||||
]
|
||||
LLMChunk(
|
||||
message=LLMMessage(role=Role.assistant, content="Hi"),
|
||||
usage=LLMUsage(prompt_tokens=1, completion_tokens=1),
|
||||
)
|
||||
)
|
||||
return backend
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ SIMPLE_CONVERSATION_PARAMS: list[tuple[Url, JsonResponse, ResultData]] = [
|
|||
},
|
||||
{
|
||||
"message": "Some content",
|
||||
"finish_reason": "stop",
|
||||
"usage": {
|
||||
"prompt_tokens": 100,
|
||||
"total_tokens": 300,
|
||||
|
|
@ -78,7 +77,6 @@ TOOL_CONVERSATION_PARAMS: list[tuple[Url, JsonResponse, ResultData]] = [
|
|||
},
|
||||
{
|
||||
"message": "",
|
||||
"finish_reason": "tool_calls",
|
||||
"tool_calls": [
|
||||
{
|
||||
"name": "some_tool",
|
||||
|
|
@ -102,26 +100,13 @@ STREAMED_SIMPLE_CONVERSATION_PARAMS: list[tuple[Url, list[Chunk], list[ResultDat
|
|||
rb"data: [DONE]",
|
||||
],
|
||||
[
|
||||
{
|
||||
"message": "",
|
||||
"finish_reason": None,
|
||||
"usage": {"prompt_tokens": 0, "completion_tokens": 0},
|
||||
},
|
||||
{
|
||||
"message": "",
|
||||
"finish_reason": None,
|
||||
"usage": {"prompt_tokens": 0, "completion_tokens": 0},
|
||||
},
|
||||
{"message": "", "usage": {"prompt_tokens": 0, "completion_tokens": 0}},
|
||||
{"message": "", "usage": {"prompt_tokens": 0, "completion_tokens": 0}},
|
||||
{
|
||||
"message": "Some content",
|
||||
"finish_reason": None,
|
||||
"usage": {"prompt_tokens": 0, "completion_tokens": 0},
|
||||
},
|
||||
{
|
||||
"message": "",
|
||||
"finish_reason": "stop",
|
||||
"usage": {"prompt_tokens": 100, "completion_tokens": 200},
|
||||
},
|
||||
{"message": "", "usage": {"prompt_tokens": 100, "completion_tokens": 200}},
|
||||
],
|
||||
)
|
||||
]
|
||||
|
|
@ -140,30 +125,19 @@ STREAMED_TOOL_CONVERSATION_PARAMS: list[tuple[Url, list[Chunk], list[ResultData]
|
|||
rb"data: [DONE]",
|
||||
],
|
||||
[
|
||||
{
|
||||
"message": "",
|
||||
"finish_reason": None,
|
||||
"usage": {"prompt_tokens": 0, "completion_tokens": 0},
|
||||
},
|
||||
{
|
||||
"message": "",
|
||||
"finish_reason": None,
|
||||
"usage": {"prompt_tokens": 0, "completion_tokens": 0},
|
||||
},
|
||||
{"message": "", "usage": {"prompt_tokens": 0, "completion_tokens": 0}},
|
||||
{"message": "", "usage": {"prompt_tokens": 0, "completion_tokens": 0}},
|
||||
{
|
||||
"message": "Some content",
|
||||
"finish_reason": None,
|
||||
"usage": {"prompt_tokens": 0, "completion_tokens": 0},
|
||||
},
|
||||
{
|
||||
"message": "",
|
||||
"finish_reason": None,
|
||||
"tool_calls": [{"name": "some_tool", "arguments": None, "index": 0}],
|
||||
"usage": {"prompt_tokens": 0, "completion_tokens": 0},
|
||||
},
|
||||
{
|
||||
"message": "",
|
||||
"finish_reason": None,
|
||||
"tool_calls": [
|
||||
{
|
||||
"name": None,
|
||||
|
|
@ -173,11 +147,7 @@ STREAMED_TOOL_CONVERSATION_PARAMS: list[tuple[Url, list[Chunk], list[ResultData]
|
|||
],
|
||||
"usage": {"prompt_tokens": 0, "completion_tokens": 0},
|
||||
},
|
||||
{
|
||||
"message": "",
|
||||
"finish_reason": "tool_calls",
|
||||
"usage": {"prompt_tokens": 100, "completion_tokens": 200},
|
||||
},
|
||||
{"message": "", "usage": {"prompt_tokens": 100, "completion_tokens": 200}},
|
||||
],
|
||||
)
|
||||
]
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ SIMPLE_CONVERSATION_PARAMS: list[tuple[Url, JsonResponse, ResultData]] = [
|
|||
},
|
||||
{
|
||||
"message": "Some content",
|
||||
"finish_reason": "stop",
|
||||
"usage": {
|
||||
"prompt_tokens": 100,
|
||||
"total_tokens": 300,
|
||||
|
|
@ -75,7 +74,6 @@ TOOL_CONVERSATION_PARAMS: list[tuple[Url, JsonResponse, ResultData]] = [
|
|||
},
|
||||
{
|
||||
"message": "Some content",
|
||||
"finish_reason": "tool_calls",
|
||||
"tool_calls": [
|
||||
{
|
||||
"name": "some_tool",
|
||||
|
|
@ -98,21 +96,12 @@ STREAMED_SIMPLE_CONVERSATION_PARAMS: list[tuple[Url, list[Chunk], list[ResultDat
|
|||
rb"data: [DONE]",
|
||||
],
|
||||
[
|
||||
{
|
||||
"message": "",
|
||||
"finish_reason": None,
|
||||
"usage": {"prompt_tokens": 0, "completion_tokens": 0},
|
||||
},
|
||||
{"message": "", "usage": {"prompt_tokens": 0, "completion_tokens": 0}},
|
||||
{
|
||||
"message": "Some content",
|
||||
"finish_reason": None,
|
||||
"usage": {"prompt_tokens": 0, "completion_tokens": 0},
|
||||
},
|
||||
{
|
||||
"message": "",
|
||||
"finish_reason": "stop",
|
||||
"usage": {"prompt_tokens": 100, "completion_tokens": 200},
|
||||
},
|
||||
{"message": "", "usage": {"prompt_tokens": 100, "completion_tokens": 200}},
|
||||
],
|
||||
)
|
||||
]
|
||||
|
|
@ -131,25 +120,18 @@ STREAMED_TOOL_CONVERSATION_PARAMS: list[tuple[Url, list[Chunk], list[ResultData]
|
|||
rb"data: [DONE]",
|
||||
],
|
||||
[
|
||||
{
|
||||
"message": "",
|
||||
"finish_reason": None,
|
||||
"usage": {"prompt_tokens": 0, "completion_tokens": 0},
|
||||
},
|
||||
{"message": "", "usage": {"prompt_tokens": 0, "completion_tokens": 0}},
|
||||
{
|
||||
"message": "Some content",
|
||||
"finish_reason": None,
|
||||
"usage": {"prompt_tokens": 0, "completion_tokens": 0},
|
||||
},
|
||||
{
|
||||
"message": "",
|
||||
"finish_reason": None,
|
||||
"tool_calls": [{"name": "some_tool", "arguments": "", "index": 0}],
|
||||
"usage": {"prompt_tokens": 0, "completion_tokens": 0},
|
||||
},
|
||||
{
|
||||
"message": "",
|
||||
"finish_reason": None,
|
||||
"tool_calls": [
|
||||
{"name": "", "arguments": '{"some_argument": ', "index": 0}
|
||||
],
|
||||
|
|
@ -157,17 +139,12 @@ STREAMED_TOOL_CONVERSATION_PARAMS: list[tuple[Url, list[Chunk], list[ResultData]
|
|||
},
|
||||
{
|
||||
"message": "",
|
||||
"finish_reason": None,
|
||||
"tool_calls": [
|
||||
{"name": "", "arguments": '"some_argument_value"}', "index": 0}
|
||||
],
|
||||
"usage": {"prompt_tokens": 0, "completion_tokens": 0},
|
||||
},
|
||||
{
|
||||
"message": "",
|
||||
"finish_reason": "tool_calls",
|
||||
"usage": {"prompt_tokens": 100, "completion_tokens": 200},
|
||||
},
|
||||
{"message": "", "usage": {"prompt_tokens": 100, "completion_tokens": 200}},
|
||||
],
|
||||
)
|
||||
]
|
||||
|
|
|
|||
|
|
@ -87,7 +87,6 @@ class TestBackend:
|
|||
)
|
||||
|
||||
assert result.message.content == result_data["message"]
|
||||
assert result.finish_reason == result_data["finish_reason"]
|
||||
assert result.usage is not None
|
||||
assert (
|
||||
result.usage.prompt_tokens == result_data["usage"]["prompt_tokens"]
|
||||
|
|
@ -165,7 +164,6 @@ class TestBackend:
|
|||
|
||||
for result, expected_result in zip(results, result_data, strict=True):
|
||||
assert result.message.content == expected_result["message"]
|
||||
assert result.finish_reason == expected_result["finish_reason"]
|
||||
assert result.usage is not None
|
||||
assert (
|
||||
result.usage.prompt_tokens
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ def mock_llm_chunk(
|
|||
tool_calls: list[ToolCall] | None = None,
|
||||
name: str | None = None,
|
||||
tool_call_id: str | None = None,
|
||||
finish_reason: str | None = None,
|
||||
prompt_tokens: int = 10,
|
||||
completion_tokens: int = 5,
|
||||
) -> LLMChunk:
|
||||
|
|
@ -29,7 +28,6 @@ def mock_llm_chunk(
|
|||
usage=LLMUsage(
|
||||
prompt_tokens=prompt_tokens, completion_tokens=completion_tokens
|
||||
),
|
||||
finish_reason=finish_reason,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -13,13 +13,11 @@ class SnapshotTestAppWithConversation(BaseSnapshotTestApp):
|
|||
def __init__(self) -> None:
|
||||
config = default_config()
|
||||
fake_backend = FakeBackend(
|
||||
results=[
|
||||
mock_llm_chunk(
|
||||
content="I'm the Vibe agent and I'm ready to help.",
|
||||
prompt_tokens=10_000,
|
||||
completion_tokens=2_500,
|
||||
)
|
||||
]
|
||||
mock_llm_chunk(
|
||||
content="I'm the Vibe agent and I'm ready to help.",
|
||||
prompt_tokens=10_000,
|
||||
completion_tokens=2_500,
|
||||
)
|
||||
)
|
||||
super().__init__(config=config)
|
||||
self.agent = Agent(
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import AsyncGenerator, Callable, Iterable
|
||||
from typing import cast
|
||||
|
||||
from tests.mock.utils import mock_llm_chunk
|
||||
from vibe.core.types import LLMChunk, LLMMessage
|
||||
from vibe.core.types import LLMChunk, LLMMessage, Role
|
||||
|
||||
|
||||
class FakeBackend:
|
||||
|
|
@ -15,18 +16,47 @@ class FakeBackend:
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
results: Iterable[LLMChunk] | None = None,
|
||||
chunks: LLMChunk
|
||||
| Iterable[LLMChunk]
|
||||
| Iterable[Iterable[LLMChunk]]
|
||||
| None = None,
|
||||
*,
|
||||
token_counter: Callable[[list[LLMMessage]], int] | None = None,
|
||||
exception_to_raise: Exception | None = None,
|
||||
) -> None:
|
||||
self._chunks = list(results or [])
|
||||
"""Fake backend that will output the given chunks in the order they are given.
|
||||
|
||||
chunks: A single chunk, a sequence of chunks, or a sequence of sequences of chunks.
|
||||
A single chunk would be outputted as such in complete / complete_streaming
|
||||
A sequence of chunks will is considered a single stream: a completion would output
|
||||
all chunks (either streaming or in an aggregated way)
|
||||
A sequence of sequences of chunks is considered a list of streams: each completion
|
||||
will output a stream (either streaming or in an aggregated way)
|
||||
"""
|
||||
self._requests_messages: list[list[LLMMessage]] = []
|
||||
self._requests_extra_headers: list[dict[str, str] | None] = []
|
||||
self._count_tokens_calls: list[list[LLMMessage]] = []
|
||||
self._token_counter = token_counter or self._default_token_counter
|
||||
self._exception_to_raise = exception_to_raise
|
||||
|
||||
self._streams: list[list[LLMChunk]]
|
||||
if chunks is None:
|
||||
self._streams = []
|
||||
return
|
||||
if isinstance(chunks, LLMChunk):
|
||||
self._streams = [[chunks]]
|
||||
return
|
||||
if all(isinstance(chunk, LLMChunk) for chunk in chunks):
|
||||
self._streams = [[cast(LLMChunk, chunk) for chunk in chunks]]
|
||||
return
|
||||
if any(isinstance(chunk, LLMChunk) for chunk in chunks):
|
||||
raise TypeError(
|
||||
f"Invalid type for chunks, expected a value of type "
|
||||
f"LLMChunk | Iterable[LLMChunk] | Iterable[Iterable[LLMChunk]], got {chunks!r}"
|
||||
)
|
||||
chunks = cast(Iterable[Iterable[LLMChunk]], chunks)
|
||||
self._streams = [[chunk for chunk in stream] for stream in chunks]
|
||||
|
||||
@property
|
||||
def requests_messages(self) -> list[list[LLMMessage]]:
|
||||
return self._requests_messages
|
||||
|
|
@ -61,12 +91,15 @@ class FakeBackend:
|
|||
|
||||
self._requests_messages.append(messages)
|
||||
self._requests_extra_headers.append(extra_headers)
|
||||
if self._chunks:
|
||||
chunk = self._chunks.pop(0)
|
||||
if not self._chunks:
|
||||
chunk = chunk.model_copy(update={"finish_reason": "stop"})
|
||||
return chunk
|
||||
return mock_llm_chunk(content="", finish_reason="stop")
|
||||
|
||||
if self._streams:
|
||||
stream = self._streams.pop(0)
|
||||
chunk_agg = LLMChunk(message=LLMMessage(role=Role.assistant))
|
||||
for chunk in stream:
|
||||
chunk_agg += chunk
|
||||
return chunk_agg
|
||||
|
||||
return mock_llm_chunk(content="")
|
||||
|
||||
async def complete_streaming(
|
||||
self,
|
||||
|
|
@ -84,22 +117,13 @@ class FakeBackend:
|
|||
|
||||
self._requests_messages.append(messages)
|
||||
self._requests_extra_headers.append(extra_headers)
|
||||
has_final_chunk = False
|
||||
while self._chunks:
|
||||
chunk = self._chunks.pop(0)
|
||||
is_last_provided_chunk = not self._chunks
|
||||
if is_last_provided_chunk:
|
||||
chunk = chunk.model_copy(update={"finish_reason": "stop"})
|
||||
|
||||
if chunk.finish_reason is not None:
|
||||
has_final_chunk = True
|
||||
|
||||
if self._streams:
|
||||
stream = list(self._streams.pop(0))
|
||||
else:
|
||||
stream = [mock_llm_chunk(content="")]
|
||||
for chunk in stream:
|
||||
yield chunk
|
||||
if has_final_chunk:
|
||||
break
|
||||
|
||||
if not has_final_chunk:
|
||||
yield mock_llm_chunk(content="", finish_reason="stop")
|
||||
|
||||
async def count_tokens(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -23,8 +23,8 @@ async def test_auto_compact_triggers_and_batches_observer() -> None:
|
|||
observed.append((msg.role, msg.content))
|
||||
|
||||
backend = FakeBackend([
|
||||
mock_llm_chunk(content="<summary>"),
|
||||
mock_llm_chunk(content="<final>"),
|
||||
[mock_llm_chunk(content="<summary>")],
|
||||
[mock_llm_chunk(content="<final>")],
|
||||
])
|
||||
cfg = VibeConfig(
|
||||
session_logging=SessionLoggingConfig(enabled=False), auto_compact_threshold=1
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ def vibe_config() -> VibeConfig:
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_passes_x_affinity_header_when_asking_an_answer(vibe_config: VibeConfig):
|
||||
backend = FakeBackend([mock_llm_chunk(content="Response", finish_reason="stop")])
|
||||
backend = FakeBackend([mock_llm_chunk(content="Response")])
|
||||
agent = Agent(vibe_config, backend=backend)
|
||||
|
||||
[_ async for _ in agent.act("Hello")]
|
||||
|
|
@ -31,7 +31,7 @@ async def test_passes_x_affinity_header_when_asking_an_answer(vibe_config: VibeC
|
|||
async def test_passes_x_affinity_header_when_asking_an_answer_streaming(
|
||||
vibe_config: VibeConfig,
|
||||
):
|
||||
backend = FakeBackend([mock_llm_chunk(content="Response", finish_reason="stop")])
|
||||
backend = FakeBackend([mock_llm_chunk(content="Response")])
|
||||
agent = Agent(vibe_config, backend=backend, enable_streaming=True)
|
||||
|
||||
[_ async for _ in agent.act("Hello")]
|
||||
|
|
@ -45,12 +45,7 @@ async def test_passes_x_affinity_header_when_asking_an_answer_streaming(
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_updates_tokens_stats_based_on_backend_response(vibe_config: VibeConfig):
|
||||
chunk = mock_llm_chunk(
|
||||
content="Response",
|
||||
finish_reason="stop",
|
||||
prompt_tokens=100,
|
||||
completion_tokens=50,
|
||||
)
|
||||
chunk = mock_llm_chunk(content="Response", prompt_tokens=100, completion_tokens=50)
|
||||
backend = FakeBackend([chunk])
|
||||
agent = Agent(vibe_config, backend=backend)
|
||||
|
||||
|
|
@ -64,10 +59,7 @@ async def test_updates_tokens_stats_based_on_backend_response_streaming(
|
|||
vibe_config: VibeConfig,
|
||||
):
|
||||
final_chunk = mock_llm_chunk(
|
||||
content="Complete",
|
||||
finish_reason="stop",
|
||||
prompt_tokens=200,
|
||||
completion_tokens=75,
|
||||
content="Complete", prompt_tokens=200, completion_tokens=75
|
||||
)
|
||||
backend = FakeBackend([final_chunk])
|
||||
agent = Agent(vibe_config, backend=backend, enable_streaming=True)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ from tests.mock.utils import mock_llm_chunk
|
|||
from tests.stubs.fake_backend import FakeBackend
|
||||
from vibe.core.agent import Agent
|
||||
from vibe.core.config import SessionLoggingConfig, VibeConfig
|
||||
from vibe.core.llm.types import BackendLike
|
||||
from vibe.core.middleware import (
|
||||
ConversationContext,
|
||||
MiddlewareAction,
|
||||
|
|
@ -25,7 +24,6 @@ from vibe.core.types import (
|
|||
ApprovalResponse,
|
||||
AssistantEvent,
|
||||
FunctionCall,
|
||||
LLMChunk,
|
||||
LLMMessage,
|
||||
Role,
|
||||
ToolCall,
|
||||
|
|
@ -177,10 +175,11 @@ async def test_act_handles_streaming_with_tool_call_events_in_sequence() -> None
|
|||
function=FunctionCall(name="todo", arguments='{"action": "read"}'),
|
||||
)
|
||||
backend = FakeBackend([
|
||||
mock_llm_chunk(content="Checking your todos."),
|
||||
mock_llm_chunk(content="", tool_calls=[todo_tool_call]),
|
||||
mock_llm_chunk(content="", finish_reason="stop"),
|
||||
mock_llm_chunk(content="Done reviewing todos."),
|
||||
[
|
||||
mock_llm_chunk(content="Checking your todos."),
|
||||
mock_llm_chunk(content="", tool_calls=[todo_tool_call]),
|
||||
],
|
||||
[mock_llm_chunk(content="Done reviewing todos.")],
|
||||
])
|
||||
agent = Agent(
|
||||
make_config(
|
||||
|
|
@ -222,7 +221,7 @@ async def test_act_handles_tool_call_chunk_with_content() -> None:
|
|||
backend = FakeBackend([
|
||||
mock_llm_chunk(content="Preparing "),
|
||||
mock_llm_chunk(content="todo request", tool_calls=[todo_tool_call]),
|
||||
mock_llm_chunk(content=" complete", finish_reason="stop"),
|
||||
mock_llm_chunk(content=" complete"),
|
||||
])
|
||||
agent = Agent(
|
||||
make_config(
|
||||
|
|
@ -237,15 +236,12 @@ async def test_act_handles_tool_call_chunk_with_content() -> None:
|
|||
events = [event async for event in agent.act("Check todos with content.")]
|
||||
|
||||
assert [type(event) for event in events] == [
|
||||
AssistantEvent,
|
||||
AssistantEvent,
|
||||
ToolCallEvent,
|
||||
ToolResultEvent,
|
||||
]
|
||||
assert isinstance(events[0], AssistantEvent)
|
||||
assert events[0].content == "Preparing todo request"
|
||||
assert isinstance(events[1], AssistantEvent)
|
||||
assert events[1].content == " complete"
|
||||
assert events[0].content == "Preparing todo request complete"
|
||||
assert any(
|
||||
m.role == Role.assistant and m.content == "Preparing todo request complete"
|
||||
for m in agent.messages
|
||||
|
|
@ -304,35 +300,6 @@ async def test_act_merges_streamed_tool_call_arguments() -> None:
|
|||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_act_raises_when_stream_never_signals_finish() -> None:
|
||||
class IncompleteStreamingBackend(BackendLike):
|
||||
def __init__(self, chunks: list[LLMChunk]) -> None:
|
||||
self._chunks = list(chunks)
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb):
|
||||
return None
|
||||
|
||||
async def complete_streaming(self, **_: object):
|
||||
while self._chunks:
|
||||
yield self._chunks.pop(0)
|
||||
|
||||
async def complete(self, **_: object):
|
||||
return mock_llm_chunk(content="", finish_reason="stop")
|
||||
|
||||
async def count_tokens(self, **_: object) -> int:
|
||||
return 0
|
||||
|
||||
backend = IncompleteStreamingBackend([mock_llm_chunk(content="partial")])
|
||||
agent = Agent(make_config(), backend=backend, enable_streaming=True)
|
||||
|
||||
with pytest.raises(RuntimeError, match="Streamed completion returned no chunks"):
|
||||
[event async for event in agent.act("Will this finish?")]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_act_handles_user_cancellation_during_streaming() -> None:
|
||||
class CountingMiddleware(MiddlewarePipeline):
|
||||
|
|
@ -359,7 +326,6 @@ async def test_act_handles_user_cancellation_during_streaming() -> None:
|
|||
backend = FakeBackend([
|
||||
mock_llm_chunk(content="Preparing "),
|
||||
mock_llm_chunk(content="todo request", tool_calls=[todo_tool_call]),
|
||||
mock_llm_chunk(content="", finish_reason="stop"),
|
||||
])
|
||||
agent = Agent(
|
||||
make_config(
|
||||
|
|
|
|||
|
|
@ -159,9 +159,7 @@ class TestAgentStatsHelpers:
|
|||
class TestReloadPreservesStats:
|
||||
@pytest.mark.asyncio
|
||||
async def test_reload_preserves_session_tokens(self) -> None:
|
||||
backend = FakeBackend([
|
||||
mock_llm_chunk(content="First response", finish_reason="stop")
|
||||
])
|
||||
backend = FakeBackend(mock_llm_chunk(content="First response"))
|
||||
agent = Agent(make_config(), backend=backend)
|
||||
|
||||
async for _ in agent.act("Hello"):
|
||||
|
|
@ -185,13 +183,14 @@ class TestReloadPreservesStats:
|
|||
tool_calls=[
|
||||
ToolCall(
|
||||
id="tc1",
|
||||
index=0,
|
||||
function=FunctionCall(
|
||||
name="todo", arguments='{"action": "read"}'
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
mock_llm_chunk(content="Done", finish_reason="stop"),
|
||||
mock_llm_chunk(content="Done"),
|
||||
])
|
||||
config = make_config(enabled_tools=["todo"])
|
||||
agent = Agent(config, mode=AgentMode.AUTO_APPROVE, backend=backend)
|
||||
|
|
@ -210,8 +209,8 @@ class TestReloadPreservesStats:
|
|||
@pytest.mark.asyncio
|
||||
async def test_reload_preserves_steps(self) -> None:
|
||||
backend = FakeBackend([
|
||||
mock_llm_chunk(content="R1", finish_reason="stop"),
|
||||
mock_llm_chunk(content="R2", finish_reason="stop"),
|
||||
[mock_llm_chunk(content="R1")],
|
||||
[mock_llm_chunk(content="R2")],
|
||||
])
|
||||
agent = Agent(make_config(), backend=backend)
|
||||
|
||||
|
|
@ -231,9 +230,7 @@ class TestReloadPreservesStats:
|
|||
async def test_reload_preserves_context_tokens_when_messages_preserved(
|
||||
self,
|
||||
) -> None:
|
||||
backend = FakeBackend([
|
||||
mock_llm_chunk(content="Response", finish_reason="stop")
|
||||
])
|
||||
backend = FakeBackend(mock_llm_chunk(content="Response"))
|
||||
agent = Agent(make_config(), backend=backend)
|
||||
[_ async for _ in agent.act("Hello")]
|
||||
assert agent.stats.context_tokens > 0
|
||||
|
|
@ -258,10 +255,10 @@ class TestReloadPreservesStats:
|
|||
assert agent.stats.context_tokens == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reload_preserves_context_tokens_when_messages_exist(self) -> None:
|
||||
backend = FakeBackend([
|
||||
mock_llm_chunk(content="Response", finish_reason="stop")
|
||||
])
|
||||
async def test_reload_resets_context_tokens_when_system_prompt_changes(
|
||||
self,
|
||||
) -> None:
|
||||
backend = FakeBackend(mock_llm_chunk(content="Response"))
|
||||
config1 = make_config(system_prompt_id="tests")
|
||||
config2 = make_config(system_prompt_id="cli")
|
||||
agent = Agent(config1, backend=backend)
|
||||
|
|
@ -279,9 +276,7 @@ class TestReloadPreservesStats:
|
|||
async def test_reload_updates_pricing_from_new_model(self, monkeypatch) -> None:
|
||||
monkeypatch.setenv("LECHAT_API_KEY", "mock-key")
|
||||
|
||||
backend = FakeBackend([
|
||||
mock_llm_chunk(content="Response", finish_reason="stop")
|
||||
])
|
||||
backend = FakeBackend(mock_llm_chunk(content="Response"))
|
||||
config_mistral = make_config(active_model="devstral-latest")
|
||||
agent = Agent(config_mistral, backend=backend)
|
||||
|
||||
|
|
@ -302,8 +297,8 @@ class TestReloadPreservesStats:
|
|||
monkeypatch.setenv("LECHAT_API_KEY", "mock-key")
|
||||
|
||||
backend = FakeBackend([
|
||||
mock_llm_chunk(content="First", finish_reason="stop"),
|
||||
mock_llm_chunk(content="After reload", finish_reason="stop"),
|
||||
[mock_llm_chunk(content="First")],
|
||||
[mock_llm_chunk(content="After reload")],
|
||||
])
|
||||
config1 = make_config(active_model="devstral-latest")
|
||||
agent = Agent(config1, backend=backend)
|
||||
|
|
@ -330,9 +325,7 @@ class TestReloadPreservesStats:
|
|||
class TestReloadPreservesMessages:
|
||||
@pytest.mark.asyncio
|
||||
async def test_reload_preserves_conversation_messages(self) -> None:
|
||||
backend = FakeBackend([
|
||||
mock_llm_chunk(content="Response", finish_reason="stop")
|
||||
])
|
||||
backend = FakeBackend(mock_llm_chunk(content="Response"))
|
||||
agent = Agent(make_config(), backend=backend)
|
||||
|
||||
async for _ in agent.act("Hello"):
|
||||
|
|
@ -353,9 +346,7 @@ class TestReloadPreservesMessages:
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reload_updates_system_prompt_preserves_rest(self) -> None:
|
||||
backend = FakeBackend([
|
||||
mock_llm_chunk(content="Response", finish_reason="stop")
|
||||
])
|
||||
backend = FakeBackend(mock_llm_chunk(content="Response"))
|
||||
config1 = make_config(system_prompt_id="tests")
|
||||
agent = Agent(config1, backend=backend)
|
||||
|
||||
|
|
@ -388,9 +379,7 @@ class TestReloadPreservesMessages:
|
|||
self, observer_capture
|
||||
) -> None:
|
||||
observed, observer = observer_capture
|
||||
backend = FakeBackend([
|
||||
mock_llm_chunk(content="Response", finish_reason="stop")
|
||||
])
|
||||
backend = FakeBackend(mock_llm_chunk(content="Response"))
|
||||
agent = Agent(make_config(), message_observer=observer, backend=backend)
|
||||
|
||||
async for _ in agent.act("Hello"):
|
||||
|
|
@ -410,8 +399,8 @@ class TestCompactStatsHandling:
|
|||
@pytest.mark.asyncio
|
||||
async def test_compact_preserves_cumulative_stats(self) -> None:
|
||||
backend = FakeBackend([
|
||||
mock_llm_chunk(content="First response", finish_reason="stop"),
|
||||
mock_llm_chunk(content="<summary>", finish_reason="stop"),
|
||||
[mock_llm_chunk(content="First response")],
|
||||
[mock_llm_chunk(content="<summary>")],
|
||||
])
|
||||
agent = Agent(make_config(), backend=backend)
|
||||
|
||||
|
|
@ -432,8 +421,8 @@ class TestCompactStatsHandling:
|
|||
@pytest.mark.asyncio
|
||||
async def test_compact_updates_context_tokens(self) -> None:
|
||||
backend = FakeBackend([
|
||||
mock_llm_chunk(content="Long response " * 100, finish_reason="stop"),
|
||||
mock_llm_chunk(content="<summary>", finish_reason="stop"),
|
||||
[mock_llm_chunk(content="Long response " * 100)],
|
||||
[mock_llm_chunk(content="<summary>")],
|
||||
])
|
||||
agent = Agent(make_config(), backend=backend)
|
||||
|
||||
|
|
@ -449,19 +438,22 @@ class TestCompactStatsHandling:
|
|||
@pytest.mark.asyncio
|
||||
async def test_compact_preserves_tool_call_stats(self) -> None:
|
||||
backend = FakeBackend([
|
||||
mock_llm_chunk(
|
||||
content="Using tool",
|
||||
tool_calls=[
|
||||
ToolCall(
|
||||
id="tc1",
|
||||
function=FunctionCall(
|
||||
name="todo", arguments='{"action": "read"}'
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
mock_llm_chunk(content="Done", finish_reason="stop"),
|
||||
mock_llm_chunk(content="<summary>", finish_reason="stop"),
|
||||
[
|
||||
mock_llm_chunk(
|
||||
content="Using tool",
|
||||
tool_calls=[
|
||||
ToolCall(
|
||||
id="tc1",
|
||||
index=0,
|
||||
function=FunctionCall(
|
||||
name="todo", arguments='{"action": "read"}'
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
mock_llm_chunk(content=" todo"),
|
||||
],
|
||||
[mock_llm_chunk(content="<summary>")],
|
||||
])
|
||||
config = make_config(enabled_tools=["todo"])
|
||||
agent = Agent(config, mode=AgentMode.AUTO_APPROVE, backend=backend)
|
||||
|
|
@ -478,8 +470,8 @@ class TestCompactStatsHandling:
|
|||
@pytest.mark.asyncio
|
||||
async def test_compact_resets_session_id(self) -> None:
|
||||
backend = FakeBackend([
|
||||
mock_llm_chunk(content="Long response " * 100, finish_reason="stop"),
|
||||
mock_llm_chunk(content="<summary>", finish_reason="stop"),
|
||||
[mock_llm_chunk(content="Long response " * 100)],
|
||||
[mock_llm_chunk(content="<summary>")],
|
||||
])
|
||||
agent = Agent(make_config(disable_logging=False), backend=backend)
|
||||
|
||||
|
|
@ -506,8 +498,8 @@ class TestAutoCompactIntegration:
|
|||
observed.append((msg.role, msg.content))
|
||||
|
||||
backend = FakeBackend([
|
||||
mock_llm_chunk(content="<summary>", finish_reason="stop"),
|
||||
mock_llm_chunk(content="<final>", finish_reason="stop"),
|
||||
[mock_llm_chunk(content="<summary>")],
|
||||
[mock_llm_chunk(content="<final>")],
|
||||
])
|
||||
cfg = VibeConfig(
|
||||
session_logging=SessionLoggingConfig(enabled=False),
|
||||
|
|
@ -544,9 +536,7 @@ class TestAutoCompactIntegration:
|
|||
class TestClearHistoryFullReset:
|
||||
@pytest.mark.asyncio
|
||||
async def test_clear_history_fully_resets_stats(self) -> None:
|
||||
backend = FakeBackend([
|
||||
mock_llm_chunk(content="Response", finish_reason="stop")
|
||||
])
|
||||
backend = FakeBackend(mock_llm_chunk(content="Response"))
|
||||
agent = Agent(make_config(), backend=backend)
|
||||
|
||||
async for _ in agent.act("Hello"):
|
||||
|
|
@ -563,9 +553,7 @@ class TestClearHistoryFullReset:
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_clear_history_preserves_pricing(self) -> None:
|
||||
backend = FakeBackend([
|
||||
mock_llm_chunk(content="Response", finish_reason="stop")
|
||||
])
|
||||
backend = FakeBackend(mock_llm_chunk(content="Response"))
|
||||
config = make_config(input_price=0.4, output_price=2.0)
|
||||
agent = Agent(config, backend=backend)
|
||||
|
||||
|
|
@ -579,9 +567,7 @@ class TestClearHistoryFullReset:
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_clear_history_removes_messages(self) -> None:
|
||||
backend = FakeBackend([
|
||||
mock_llm_chunk(content="Response", finish_reason="stop")
|
||||
])
|
||||
backend = FakeBackend(mock_llm_chunk(content="Response"))
|
||||
agent = Agent(make_config(), backend=backend)
|
||||
|
||||
async for _ in agent.act("Hello"):
|
||||
|
|
@ -596,9 +582,7 @@ class TestClearHistoryFullReset:
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_clear_history_resets_session_id(self) -> None:
|
||||
backend = FakeBackend([
|
||||
mock_llm_chunk(content="Response", finish_reason="stop")
|
||||
])
|
||||
backend = FakeBackend(mock_llm_chunk(content="Response"))
|
||||
agent = Agent(make_config(disable_logging=False), backend=backend)
|
||||
|
||||
original_session_id = agent.session_id
|
||||
|
|
@ -622,9 +606,7 @@ class TestStatsEdgeCases:
|
|||
) -> None:
|
||||
monkeypatch.setenv("LECHAT_API_KEY", "mock-key")
|
||||
|
||||
backend = FakeBackend([
|
||||
mock_llm_chunk(content="Response", finish_reason="stop")
|
||||
])
|
||||
backend = FakeBackend(mock_llm_chunk(content="Response"))
|
||||
config1 = make_config(active_model="devstral-latest")
|
||||
agent = Agent(config1, backend=backend)
|
||||
|
||||
|
|
@ -643,9 +625,9 @@ class TestStatsEdgeCases:
|
|||
@pytest.mark.asyncio
|
||||
async def test_multiple_reloads_accumulate_correctly(self) -> None:
|
||||
backend = FakeBackend([
|
||||
mock_llm_chunk(content="R1", finish_reason="stop"),
|
||||
mock_llm_chunk(content="R2", finish_reason="stop"),
|
||||
mock_llm_chunk(content="R3", finish_reason="stop"),
|
||||
[mock_llm_chunk(content="R1")],
|
||||
[mock_llm_chunk(content="R2")],
|
||||
[mock_llm_chunk(content="R3")],
|
||||
])
|
||||
agent = Agent(make_config(), backend=backend)
|
||||
|
||||
|
|
@ -668,9 +650,9 @@ class TestStatsEdgeCases:
|
|||
@pytest.mark.asyncio
|
||||
async def test_compact_then_reload_preserves_both(self) -> None:
|
||||
backend = FakeBackend([
|
||||
mock_llm_chunk(content="Initial response", finish_reason="stop"),
|
||||
mock_llm_chunk(content="<summary>", finish_reason="stop"),
|
||||
mock_llm_chunk(content="After reload", finish_reason="stop"),
|
||||
[mock_llm_chunk(content="Initial response")],
|
||||
[mock_llm_chunk(content="<summary>")],
|
||||
[mock_llm_chunk(content="After reload")],
|
||||
])
|
||||
agent = Agent(make_config(), backend=backend)
|
||||
|
||||
|
|
|
|||
|
|
@ -44,10 +44,12 @@ def make_config(todo_permission: ToolPermission = ToolPermission.ALWAYS) -> Vibe
|
|||
)
|
||||
|
||||
|
||||
def make_todo_tool_call(call_id: str, action: str = "read") -> ToolCall:
|
||||
def make_todo_tool_call(
|
||||
call_id: str, index: int = 0, arguments: str | None = None
|
||||
) -> ToolCall:
|
||||
args = arguments if arguments is not None else '{"action": "read"}'
|
||||
return ToolCall(
|
||||
id=call_id,
|
||||
function=FunctionCall(name="todo", arguments=f'{{"action": "{action}"}}'),
|
||||
id=call_id, index=index, function=FunctionCall(name="todo", arguments=args)
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -72,8 +74,8 @@ async def test_single_tool_call_executes_under_auto_approve() -> None:
|
|||
mocked_tool_call_id = "call_1"
|
||||
tool_call = make_todo_tool_call(mocked_tool_call_id)
|
||||
backend = FakeBackend([
|
||||
mock_llm_chunk(content="Let me check your todos.", tool_calls=[tool_call]),
|
||||
mock_llm_chunk(content="I retrieved 0 todos.", finish_reason="stop"),
|
||||
[mock_llm_chunk(content="Let me check your todos.", tool_calls=[tool_call])],
|
||||
[mock_llm_chunk(content="I retrieved 0 todos.")],
|
||||
])
|
||||
agent = make_agent(auto_approve=True, backend=backend)
|
||||
|
||||
|
|
@ -108,14 +110,13 @@ async def test_tool_call_requires_approval_if_not_auto_approved() -> None:
|
|||
auto_approve=False,
|
||||
todo_permission=ToolPermission.ASK,
|
||||
backend=FakeBackend([
|
||||
mock_llm_chunk(
|
||||
content="Let me check your todos.",
|
||||
tool_calls=[make_todo_tool_call("call_2")],
|
||||
),
|
||||
mock_llm_chunk(
|
||||
content="I cannot execute the tool without approval.",
|
||||
finish_reason="stop",
|
||||
),
|
||||
[
|
||||
mock_llm_chunk(
|
||||
content="Let me check your todos.",
|
||||
tool_calls=[make_todo_tool_call("call_2")],
|
||||
)
|
||||
],
|
||||
[mock_llm_chunk(content="I cannot execute the tool without approval.")],
|
||||
]),
|
||||
)
|
||||
|
||||
|
|
@ -148,11 +149,13 @@ async def test_tool_call_approved_by_callback() -> None:
|
|||
todo_permission=ToolPermission.ASK,
|
||||
approval_callback=approval_callback,
|
||||
backend=FakeBackend([
|
||||
mock_llm_chunk(
|
||||
content="Let me check your todos.",
|
||||
tool_calls=[make_todo_tool_call("call_3")],
|
||||
),
|
||||
mock_llm_chunk(content="I retrieved 0 todos.", finish_reason="stop"),
|
||||
[
|
||||
mock_llm_chunk(
|
||||
content="Let me check your todos.",
|
||||
tool_calls=[make_todo_tool_call("call_3")],
|
||||
)
|
||||
],
|
||||
[mock_llm_chunk(content="I retrieved 0 todos.")],
|
||||
]),
|
||||
)
|
||||
|
||||
|
|
@ -183,13 +186,13 @@ async def test_tool_call_rejected_when_auto_approve_disabled_and_rejected_by_cal
|
|||
todo_permission=ToolPermission.ASK,
|
||||
approval_callback=approval_callback,
|
||||
backend=FakeBackend([
|
||||
mock_llm_chunk(
|
||||
content="Let me check your todos.",
|
||||
tool_calls=[make_todo_tool_call("call_4")],
|
||||
),
|
||||
mock_llm_chunk(
|
||||
content="Understood, I won't check the todos.", finish_reason="stop"
|
||||
),
|
||||
[
|
||||
mock_llm_chunk(
|
||||
content="Let me check your todos.",
|
||||
tool_calls=[make_todo_tool_call("call_4")],
|
||||
)
|
||||
],
|
||||
[mock_llm_chunk(content="Understood, I won't check the todos.")],
|
||||
]),
|
||||
)
|
||||
|
||||
|
|
@ -211,11 +214,13 @@ async def test_tool_call_skipped_when_permission_is_never() -> None:
|
|||
auto_approve=False,
|
||||
todo_permission=ToolPermission.NEVER,
|
||||
backend=FakeBackend([
|
||||
mock_llm_chunk(
|
||||
content="Let me check your todos.",
|
||||
tool_calls=[make_todo_tool_call("call_never")],
|
||||
),
|
||||
mock_llm_chunk(content="Tool is disabled.", finish_reason="stop"),
|
||||
[
|
||||
mock_llm_chunk(
|
||||
content="Let me check your todos.",
|
||||
tool_calls=[make_todo_tool_call("call_never")],
|
||||
)
|
||||
],
|
||||
[mock_llm_chunk(content="Tool is disabled.")],
|
||||
]),
|
||||
)
|
||||
|
||||
|
|
@ -257,14 +262,20 @@ async def test_approval_always_sets_tool_permission_for_subsequent_calls() -> No
|
|||
todo_permission=ToolPermission.ASK,
|
||||
approval_callback=approval_callback,
|
||||
backend=FakeBackend([
|
||||
mock_llm_chunk(
|
||||
content="First check.", tool_calls=[make_todo_tool_call("call_first")]
|
||||
),
|
||||
mock_llm_chunk(content="First done.", finish_reason="stop"),
|
||||
mock_llm_chunk(
|
||||
content="Second check.", tool_calls=[make_todo_tool_call("call_second")]
|
||||
),
|
||||
mock_llm_chunk(content="Second done.", finish_reason="stop"),
|
||||
[
|
||||
mock_llm_chunk(
|
||||
content="First check.",
|
||||
tool_calls=[make_todo_tool_call("call_first")],
|
||||
)
|
||||
],
|
||||
[mock_llm_chunk(content="First done.")],
|
||||
[
|
||||
mock_llm_chunk(
|
||||
content="Second check.",
|
||||
tool_calls=[make_todo_tool_call("call_second")],
|
||||
)
|
||||
],
|
||||
[mock_llm_chunk(content="Second done.")],
|
||||
]),
|
||||
)
|
||||
agent_ref = agent
|
||||
|
|
@ -291,17 +302,16 @@ async def test_approval_always_sets_tool_permission_for_subsequent_calls() -> No
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tool_call_with_invalid_action() -> None:
|
||||
tool_call = ToolCall(
|
||||
id="call_5",
|
||||
function=FunctionCall(name="todo", arguments='{"action": "invalid_action"}'),
|
||||
)
|
||||
tool_call = make_todo_tool_call("call_5", arguments='{"action": "invalid_action"}')
|
||||
agent = make_agent(
|
||||
auto_approve=True,
|
||||
backend=FakeBackend([
|
||||
mock_llm_chunk(content="Let me check your todos.", tool_calls=[tool_call]),
|
||||
mock_llm_chunk(
|
||||
content="I encountered an error with the action.", finish_reason="stop"
|
||||
),
|
||||
[
|
||||
mock_llm_chunk(
|
||||
content="Let me check your todos.", tool_calls=[tool_call]
|
||||
)
|
||||
],
|
||||
[mock_llm_chunk(content="I encountered an error with the action.")],
|
||||
]),
|
||||
)
|
||||
|
||||
|
|
@ -320,24 +330,18 @@ async def test_tool_call_with_duplicate_todo_ids() -> None:
|
|||
TodoItem(id="duplicate", content="Task 1"),
|
||||
TodoItem(id="duplicate", content="Task 2"),
|
||||
]
|
||||
tool_call = ToolCall(
|
||||
id="call_6",
|
||||
function=FunctionCall(
|
||||
name="todo",
|
||||
arguments=json.dumps({
|
||||
"action": "write",
|
||||
"todos": [t.model_dump() for t in duplicate_todos],
|
||||
}),
|
||||
),
|
||||
tool_call = make_todo_tool_call(
|
||||
"call_6",
|
||||
arguments=json.dumps({
|
||||
"action": "write",
|
||||
"todos": [t.model_dump() for t in duplicate_todos],
|
||||
}),
|
||||
)
|
||||
agent = make_agent(
|
||||
auto_approve=True,
|
||||
backend=FakeBackend([
|
||||
mock_llm_chunk(content="Let me write todos.", tool_calls=[tool_call]),
|
||||
mock_llm_chunk(
|
||||
content="I couldn't write todos with duplicate IDs.",
|
||||
finish_reason="stop",
|
||||
),
|
||||
[mock_llm_chunk(content="Let me write todos.", tool_calls=[tool_call])],
|
||||
[mock_llm_chunk(content="I couldn't write todos with duplicate IDs.")],
|
||||
]),
|
||||
)
|
||||
|
||||
|
|
@ -353,23 +357,18 @@ async def test_tool_call_with_duplicate_todo_ids() -> None:
|
|||
@pytest.mark.asyncio
|
||||
async def test_tool_call_with_exceeding_max_todos() -> None:
|
||||
many_todos = [TodoItem(id=f"todo_{i}", content=f"Task {i}") for i in range(150)]
|
||||
tool_call = ToolCall(
|
||||
id="call_7",
|
||||
function=FunctionCall(
|
||||
name="todo",
|
||||
arguments=json.dumps({
|
||||
"action": "write",
|
||||
"todos": [t.model_dump() for t in many_todos],
|
||||
}),
|
||||
),
|
||||
tool_call = make_todo_tool_call(
|
||||
"call_7",
|
||||
arguments=json.dumps({
|
||||
"action": "write",
|
||||
"todos": [t.model_dump() for t in many_todos],
|
||||
}),
|
||||
)
|
||||
agent = make_agent(
|
||||
auto_approve=True,
|
||||
backend=FakeBackend([
|
||||
mock_llm_chunk(content="Let me write todos.", tool_calls=[tool_call]),
|
||||
mock_llm_chunk(
|
||||
content="I couldn't write that many todos.", finish_reason="stop"
|
||||
),
|
||||
[mock_llm_chunk(content="Let me write todos.", tool_calls=[tool_call])],
|
||||
[mock_llm_chunk(content="I couldn't write that many todos.")],
|
||||
]),
|
||||
)
|
||||
|
||||
|
|
@ -394,7 +393,7 @@ async def test_tool_call_can_be_interrupted(
|
|||
exception_class: type[BaseException],
|
||||
) -> None:
|
||||
tool_call = ToolCall(
|
||||
id="call_8", function=FunctionCall(name="stub_tool", arguments="{}")
|
||||
id="call_8", index=0, function=FunctionCall(name="stub_tool", arguments="{}")
|
||||
)
|
||||
config = VibeConfig(
|
||||
session_logging=SessionLoggingConfig(enabled=False),
|
||||
|
|
@ -405,8 +404,8 @@ async def test_tool_call_can_be_interrupted(
|
|||
config,
|
||||
mode=AgentMode.AUTO_APPROVE,
|
||||
backend=FakeBackend([
|
||||
mock_llm_chunk(content="Let me use the tool.", tool_calls=[tool_call]),
|
||||
mock_llm_chunk(content="Tool execution completed.", finish_reason="stop"),
|
||||
[mock_llm_chunk(content="Let me use the tool.", tool_calls=[tool_call])],
|
||||
[mock_llm_chunk(content="Tool execution completed.")],
|
||||
]),
|
||||
)
|
||||
# no dependency injection available => monkey patch
|
||||
|
|
@ -433,15 +432,11 @@ async def test_fill_missing_tool_responses_inserts_placeholders() -> None:
|
|||
agent = Agent(
|
||||
make_config(),
|
||||
mode=AgentMode.AUTO_APPROVE,
|
||||
backend=FakeBackend([mock_llm_chunk(content="ok", finish_reason="stop")]),
|
||||
backend=FakeBackend(mock_llm_chunk(content="ok")),
|
||||
)
|
||||
tool_calls_messages = [
|
||||
ToolCall(
|
||||
id="tc1", function=FunctionCall(name="todo", arguments='{"action": "read"}')
|
||||
),
|
||||
ToolCall(
|
||||
id="tc2", function=FunctionCall(name="todo", arguments='{"action": "read"}')
|
||||
),
|
||||
make_todo_tool_call("tc1", index=0),
|
||||
make_todo_tool_call("tc2", index=1),
|
||||
]
|
||||
assistant_msg = LLMMessage(
|
||||
role=Role.assistant, content="Calling tools...", tool_calls=tool_calls_messages
|
||||
|
|
@ -473,7 +468,7 @@ async def test_ensure_assistant_after_tool_appends_understood() -> None:
|
|||
agent = Agent(
|
||||
make_config(),
|
||||
mode=AgentMode.AUTO_APPROVE,
|
||||
backend=FakeBackend([mock_llm_chunk(content="ok", finish_reason="stop")]),
|
||||
backend=FakeBackend(mock_llm_chunk(content="ok")),
|
||||
)
|
||||
tool_msg = LLMMessage(
|
||||
role=Role.tool, tool_call_id="tc_z", name="todo", content="Done"
|
||||
|
|
|
|||
|
|
@ -34,12 +34,11 @@ def test_run_programmatic_preload_streaming_is_batched(
|
|||
|
||||
with mock_backend_factory(
|
||||
Backend.MISTRAL,
|
||||
lambda provider, **kwargs: FakeBackend([
|
||||
lambda provider, **kwargs: FakeBackend(
|
||||
mock_llm_chunk(
|
||||
content="Decorators are wrappers that modify function behavior.",
|
||||
finish_reason="stop",
|
||||
content="Decorators are wrappers that modify function behavior."
|
||||
)
|
||||
]),
|
||||
),
|
||||
):
|
||||
cfg = VibeConfig(
|
||||
session_logging=SessionLoggingConfig(enabled=False),
|
||||
|
|
|
|||
|
|
@ -157,7 +157,6 @@ class TestAgentSwitchMode:
|
|||
return FakeBackend([
|
||||
LLMChunk(
|
||||
message=LLMMessage(role=Role.assistant, content="Test response"),
|
||||
finish_reason="stop",
|
||||
usage=LLMUsage(prompt_tokens=10, completion_tokens=5),
|
||||
)
|
||||
])
|
||||
|
|
@ -274,7 +273,6 @@ class TestPlanModeToolRestriction:
|
|||
backend = FakeBackend([
|
||||
LLMChunk(
|
||||
message=LLMMessage(role=Role.assistant, content="ok"),
|
||||
finish_reason="stop",
|
||||
usage=LLMUsage(prompt_tokens=10, completion_tokens=5),
|
||||
)
|
||||
])
|
||||
|
|
@ -298,11 +296,12 @@ class TestPlanModeToolRestriction:
|
|||
async def test_plan_mode_rejects_non_plan_tool_call(self) -> None:
|
||||
tool_call = ToolCall(
|
||||
id="call_1",
|
||||
index=0,
|
||||
function=FunctionCall(name="bash", arguments='{"command": "ls"}'),
|
||||
)
|
||||
backend = FakeBackend([
|
||||
mock_llm_chunk(content="Let me run bash", tool_calls=[tool_call]),
|
||||
mock_llm_chunk(content="Tool not available", finish_reason="stop"),
|
||||
mock_llm_chunk(content="Tool not available"),
|
||||
])
|
||||
|
||||
config = VibeConfig(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue