9185eab8c9
Add faithful single-choice chat results, tool stream activity, error-frame retries, and isolated protocol caching. Reject empty cached completions with the same value predicate as transport, add regression coverage, and adopt Apache-2.0.
115 lines
4.2 KiB
Python
115 lines
4.2 KiB
Python
"""真实网关工具双轮冒烟;复用安全 Markdown 报告,不运行工具副作用。"""
|
|
|
|
import dataclasses
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
from uuid import uuid4
|
|
|
|
import pytest
|
|
from dotenv import dotenv_values
|
|
|
|
from polygateway import GatewayClient, GatewaySettings
|
|
from tests.live_evidence import write_live_round
|
|
|
|
_ENV = {k: v for k, v in {**dotenv_values(".env"), **os.environ}.items() if v is not None}
|
|
pytestmark = [pytest.mark.slow, pytest.mark.requires_llm]
|
|
|
|
|
|
@pytest.mark.parametrize("stream", [True, False])
|
|
async def test_tools_two_rounds(stream):
|
|
"""已配置源中选首源,只改变测试隔离与 raw-only 意图,不收紧生产超时。"""
|
|
if not any(k.startswith("LLM__") and k.endswith("__API_KEY") for k in _ENV):
|
|
pytest.skip("未配置真实源,未覆盖")
|
|
settings = GatewaySettings.from_env("LLM", env=_ENV)
|
|
source = dataclasses.replace(settings.sources[0], enable_thinking=None, reasoning_effort=None)
|
|
settings = dataclasses.replace(
|
|
settings,
|
|
sources=(source,),
|
|
cache_backend="none",
|
|
cache_namespace=None,
|
|
cache_ttl_s=None,
|
|
telemetry_backend="none",
|
|
telemetry_auto_migrate=False,
|
|
limiter_backend="memory",
|
|
breaker_backend="memory",
|
|
hedge_after_s=None,
|
|
hedge_stall_after_s=None,
|
|
)
|
|
tools = [
|
|
{
|
|
"type": "function",
|
|
"function": {
|
|
"name": "echo_value",
|
|
"description": "Return the supplied value",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {"value": {"type": "string"}},
|
|
"required": ["value"],
|
|
},
|
|
},
|
|
}
|
|
]
|
|
messages = [
|
|
{
|
|
"role": "user",
|
|
"content": "Call echo_value with value hello. After receiving the tool result, reply with hello.",
|
|
}
|
|
]
|
|
status, error_type, counts = "FAIL", None, {}
|
|
try:
|
|
async with GatewayClient.from_settings(settings) as client:
|
|
first = await client.chat(
|
|
messages,
|
|
stream=stream,
|
|
response_mode="openai",
|
|
overlay={
|
|
"tools": tools,
|
|
"tool_choice": {"type": "function", "function": {"name": "echo_value"}},
|
|
},
|
|
)
|
|
completion = first.completion
|
|
assert completion is not None and completion.message["role"] == "assistant"
|
|
assert completion.response_id and completion.finish_reason == "tool_calls"
|
|
calls = completion.message["tool_calls"]
|
|
counts["tools"] = len(calls)
|
|
assert calls
|
|
messages.append(completion.message)
|
|
for call in calls:
|
|
assert call["function"]["name"] == "echo_value"
|
|
arguments = json.loads(call["function"]["arguments"])
|
|
assert arguments["value"] == "hello"
|
|
messages.append(
|
|
{"role": "tool", "tool_call_id": call["id"], "content": arguments["value"]}
|
|
)
|
|
second = await client.chat(
|
|
messages,
|
|
stream=stream,
|
|
response_mode="openai",
|
|
overlay={"tools": tools, "tool_choice": "none"},
|
|
)
|
|
assert (
|
|
second.completion is not None and second.completion.message["role"] == "assistant"
|
|
)
|
|
assert "hello" in (second.completion.message.get("content") or "").lower()
|
|
counts["attempts"] = first.call_stats.attempts + second.call_stats.attempts
|
|
status = "PASS"
|
|
except Exception as exc:
|
|
error_type = type(exc).__name__
|
|
raise
|
|
finally:
|
|
write_live_round(
|
|
Path("tests/outputs/issue29/live"),
|
|
run_id=uuid4().hex,
|
|
matrix_id="tools",
|
|
round_index=1,
|
|
safe_fields={
|
|
"status": status,
|
|
"stream": stream,
|
|
"error_type": error_type,
|
|
"counts": counts,
|
|
"planned_rounds": 2,
|
|
"completed_rounds": 2 if status == "PASS" else 0,
|
|
},
|
|
)
|