169 lines
5.8 KiB
Python
169 lines
5.8 KiB
Python
from __future__ import annotations
|
|
|
|
from app.ai_provider.llm.stream import StreamFactory, StreamHandler, ToolCallAccumulator
|
|
from app.ai_provider.types.chat import ChatChunk, TokenUsage
|
|
|
|
|
|
class TestToolCallAccumulator:
|
|
def test_accumulate_simple(self):
|
|
acc = ToolCallAccumulator()
|
|
acc.add_chunk(ChatChunk(delta_content="Hello"))
|
|
assert acc.content == "Hello"
|
|
|
|
def test_accumulate_reasoning(self):
|
|
acc = ToolCallAccumulator()
|
|
acc.add_chunk(ChatChunk(delta_reasoning="I think"))
|
|
acc.add_chunk(ChatChunk(delta_reasoning=" therefore"))
|
|
assert acc.reasoning == "I think therefore"
|
|
|
|
def test_accumulate_tool_call(self):
|
|
acc = ToolCallAccumulator()
|
|
acc.add_chunk(
|
|
ChatChunk(
|
|
tool_call_chunks=[
|
|
{
|
|
"index": 0,
|
|
"id": "call_1",
|
|
"name": "read_file",
|
|
"args": '{"path": "test.txt"}',
|
|
},
|
|
]
|
|
)
|
|
)
|
|
result = acc.drain()
|
|
assert len(result) == 1
|
|
assert result[0]["name"] == "read_file"
|
|
|
|
def test_accumulate_tool_call_args(self):
|
|
acc = ToolCallAccumulator()
|
|
acc.add_chunk(ChatChunk(tool_call_chunks=[{"index": 0, "args": '{"path": "'}]))
|
|
acc.add_chunk(ChatChunk(tool_call_chunks=[{"index": 0, "args": 'test.txt"}'}]))
|
|
result = acc.drain()
|
|
assert result[0]["args"] == '{"path": "test.txt"}'
|
|
|
|
def test_accumulate_usage(self):
|
|
acc = ToolCallAccumulator()
|
|
acc.add_chunk(
|
|
ChatChunk(usage=TokenUsage(prompt_tokens=10, completion_tokens=5, total_tokens=15))
|
|
)
|
|
assert acc.usage is not None
|
|
assert acc.usage.prompt_tokens == 10
|
|
|
|
def test_empty_drain(self):
|
|
acc = ToolCallAccumulator()
|
|
result = acc.drain()
|
|
assert result == []
|
|
|
|
def test_accumulate_multiple_tool_calls(self):
|
|
acc = ToolCallAccumulator()
|
|
acc.add_chunk(
|
|
ChatChunk(
|
|
tool_call_chunks=[
|
|
{"index": 0, "id": "call_1", "name": "read_file", "args": '{"path": "'},
|
|
{"index": 1, "id": "call_2", "name": "list_dir", "args": '{"path": "'},
|
|
]
|
|
)
|
|
)
|
|
acc.add_chunk(
|
|
ChatChunk(
|
|
tool_call_chunks=[
|
|
{"index": 0, "args": 'x.txt"}'},
|
|
{"index": 1, "args": '/tmp"}'},
|
|
]
|
|
)
|
|
)
|
|
result = acc.drain()
|
|
assert len(result) == 2
|
|
assert result[0]["name"] == "read_file"
|
|
assert result[0]["args"] == '{"path": "x.txt"}'
|
|
assert result[1]["name"] == "list_dir"
|
|
assert result[1]["args"] == '{"path": "/tmp"}'
|
|
|
|
|
|
class TestStreamHandler:
|
|
def test_empty_result(self):
|
|
handler = StreamHandler()
|
|
result = handler.to_result(model_name="test-model")
|
|
assert result.content == ""
|
|
assert result.reasoning == ""
|
|
assert result.tool_calls == []
|
|
assert result.usage is None
|
|
assert result.model_name == "test-model"
|
|
|
|
def test_stream_to_result(self):
|
|
handler = StreamHandler()
|
|
handler.add_chunk(ChatChunk(delta_content="Hello world"))
|
|
handler.add_chunk(ChatChunk(delta_reasoning="Thinking..."))
|
|
handler.add_chunk(
|
|
ChatChunk(usage=TokenUsage(prompt_tokens=5, completion_tokens=3, total_tokens=8))
|
|
)
|
|
result = handler.to_result(model_name="test-model")
|
|
assert result.content == "Hello world"
|
|
assert result.reasoning == "Thinking..."
|
|
assert result.usage is not None
|
|
assert result.usage.total_tokens == 8
|
|
assert result.model_name == "test-model"
|
|
|
|
def test_stream_with_tool_calls(self):
|
|
handler = StreamHandler()
|
|
handler.add_chunk(
|
|
ChatChunk(
|
|
tool_call_chunks=[
|
|
{"index": 0, "id": "call_1", "name": "read_file", "args": '{"path": "'},
|
|
]
|
|
)
|
|
)
|
|
handler.add_chunk(
|
|
ChatChunk(
|
|
tool_call_chunks=[
|
|
{"index": 0, "args": 'test.txt"}'},
|
|
]
|
|
)
|
|
)
|
|
result = handler.to_result(model_name="test-model")
|
|
assert len(result.tool_calls) == 1
|
|
assert result.tool_calls[0]["name"] == "read_file"
|
|
assert result.tool_calls[0]["args"] == {"path": "test.txt"}
|
|
|
|
def test_chunks_property(self):
|
|
handler = StreamHandler()
|
|
handler.add_chunk(ChatChunk(delta_content="Hello"))
|
|
handler.add_chunk(ChatChunk(delta_content=" world"))
|
|
assert len(handler.chunks) == 2
|
|
|
|
def test_stream_content_and_tool_calls_together(self):
|
|
handler = StreamHandler()
|
|
handler.add_chunk(ChatChunk(delta_content="I will read the file."))
|
|
handler.add_chunk(
|
|
ChatChunk(
|
|
tool_call_chunks=[
|
|
{"index": 0, "id": "call_1", "name": "read_file", "args": "{}"},
|
|
]
|
|
)
|
|
)
|
|
result = handler.to_result()
|
|
assert result.content == "I will read the file."
|
|
assert len(result.tool_calls) == 1
|
|
|
|
def test_non_json_args_fallback(self):
|
|
handler = StreamHandler()
|
|
handler.add_chunk(
|
|
ChatChunk(
|
|
tool_call_chunks=[
|
|
{"index": 0, "id": "call_1", "name": "read_file", "args": "not valid json"},
|
|
]
|
|
)
|
|
)
|
|
result = handler.to_result()
|
|
assert result.tool_calls[0]["args"] == {"_raw": "not valid json"}
|
|
|
|
|
|
class TestStreamFactory:
|
|
def test_create_handler(self):
|
|
handler = StreamFactory.create_handler()
|
|
assert isinstance(handler, StreamHandler)
|
|
|
|
def test_create_accumulator(self):
|
|
acc = StreamFactory.create_accumulator()
|
|
assert isinstance(acc, ToolCallAccumulator)
|