feat(agent): control reasoning visibility
This commit is contained in:
@@ -3,6 +3,7 @@ from decimal import Decimal
|
||||
import json
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.pool import StaticPool
|
||||
@@ -10,7 +11,7 @@ from sqlalchemy.pool import StaticPool
|
||||
from app.api.admin_settings import _debug_agent_stream, get_agent_runtime_config, save_agent_runtime_config
|
||||
from app.models import Base
|
||||
from app.models.admin import Admin
|
||||
from app.models.ai_config import ModelConfig
|
||||
from app.models.ai_config import ModelConfig, SystemConfig
|
||||
from app.schemas.admin import AgentDebugRequest, AgentRuntimeConfigSaveRequest
|
||||
from app.services.model_stream_service import (
|
||||
AsyncStreamingModelResponse,
|
||||
@@ -57,7 +58,11 @@ def test_runtime_config_defaults_to_long_answer_safe_max_tokens():
|
||||
with _database() as db:
|
||||
admin = _admin()
|
||||
model = _model()
|
||||
db.add_all([admin, model])
|
||||
db.add_all([
|
||||
admin,
|
||||
model,
|
||||
SystemConfig(id=1, config_key="show_model_reasoning", config_value="false"),
|
||||
])
|
||||
db.commit()
|
||||
|
||||
result = get_agent_runtime_config(db=db, current_admin=admin)["data"]
|
||||
@@ -66,6 +71,7 @@ def test_runtime_config_defaults_to_long_answer_safe_max_tokens():
|
||||
assert result["modelName"] == "正式模型"
|
||||
assert result["maxToken"] == 8192
|
||||
assert result["streamEnabled"] == 1
|
||||
assert result["reasoningVisible"] == 0
|
||||
assert _max_output_tokens(model) == 8192
|
||||
|
||||
|
||||
@@ -73,7 +79,11 @@ def test_saved_runtime_config_is_persisted_on_enabled_model():
|
||||
with _database() as db:
|
||||
admin = _admin()
|
||||
model = _model()
|
||||
db.add_all([admin, model])
|
||||
db.add_all([
|
||||
admin,
|
||||
model,
|
||||
SystemConfig(id=1, config_key="show_model_reasoning", config_value="false"),
|
||||
])
|
||||
db.commit()
|
||||
|
||||
result = save_agent_runtime_config(
|
||||
@@ -85,6 +95,7 @@ def test_saved_runtime_config_is_persisted_on_enabled_model():
|
||||
frequencyPenalty=0.4,
|
||||
maxToken=12000,
|
||||
streamEnabled=0,
|
||||
reasoningVisible=1,
|
||||
),
|
||||
db=db,
|
||||
current_admin=admin,
|
||||
@@ -100,6 +111,8 @@ def test_saved_runtime_config_is_persisted_on_enabled_model():
|
||||
assert model.frequency_penalty == Decimal("0.40")
|
||||
assert model.stream_enabled == 0
|
||||
assert result["streamEnabled"] == 0
|
||||
assert result["reasoningVisible"] == 1
|
||||
assert db.query(SystemConfig).filter_by(config_key="show_model_reasoning").one().config_value == "true"
|
||||
|
||||
payload = _openai_stream_payload(
|
||||
model,
|
||||
@@ -133,7 +146,8 @@ def test_disabled_stream_returns_one_complete_chunk():
|
||||
assert chunks == [answer]
|
||||
|
||||
|
||||
def test_agent_debug_stream_emits_status_content_and_trace():
|
||||
@pytest.mark.parametrize("reasoning_visible", [True, False])
|
||||
def test_agent_debug_stream_respects_reasoning_visibility(reasoning_visible):
|
||||
async def chunks():
|
||||
yield "<think>内部思考</think>"
|
||||
yield "- **第一步**:停一下"
|
||||
@@ -162,6 +176,11 @@ def test_agent_debug_stream_emits_status_content_and_trace():
|
||||
admin = _admin()
|
||||
model = _model()
|
||||
db.add_all([admin, model])
|
||||
db.add(SystemConfig(
|
||||
id=1,
|
||||
config_key="show_model_reasoning",
|
||||
config_value="true" if reasoning_visible else "false",
|
||||
))
|
||||
db.commit()
|
||||
payload = AgentDebugRequest(
|
||||
promptContent="你是测试助手",
|
||||
@@ -187,10 +206,18 @@ def test_agent_debug_stream_emits_status_content_and_trace():
|
||||
for event in events
|
||||
if event != "data: [DONE]\n\n"
|
||||
]
|
||||
assert [event["type"] for event in decoded] == ["status", "content", "content", "complete"]
|
||||
assert decoded[1]["content"].startswith("<think>")
|
||||
assert decoded[2]["content"] == "- **第一步**:停一下"
|
||||
assert decoded[3]["retrievalTrace"][0]["tool"] == "KnowledgeSearch"
|
||||
expected_types = (
|
||||
["status", "reasoning", "content", "complete"]
|
||||
if reasoning_visible
|
||||
else ["status", "content", "complete"]
|
||||
)
|
||||
assert [event["type"] for event in decoded] == expected_types
|
||||
assert decoded[0]["reasoningVisible"] is reasoning_visible
|
||||
if reasoning_visible:
|
||||
assert decoded[1]["content"] == "内部思考"
|
||||
content_event = next(event for event in decoded if event["type"] == "content")
|
||||
assert content_event["content"] == "- **第一步**:停一下"
|
||||
assert decoded[-1]["retrievalTrace"][0]["tool"] == "KnowledgeSearch"
|
||||
|
||||
|
||||
def test_agent_debug_does_not_truncate_long_answer(monkeypatch):
|
||||
|
||||
@@ -101,6 +101,7 @@ def test_queued_chat_reports_position_and_completes(monkeypatch):
|
||||
monkeypatch.setattr(chat, "wait_for_chat_slot", wait_slot)
|
||||
monkeypatch.setattr(chat, "release_chat_slot", release_slot)
|
||||
monkeypatch.setattr(chat.ChatStreamService, "stream_answer_async", stream_answer)
|
||||
monkeypatch.setattr(chat.ReasoningPolicyService, "is_visible", lambda _db: False)
|
||||
|
||||
payload = type("Payload", (), {"sessionId": 1, "message": "问题"})()
|
||||
async def collect_events():
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
from app.services.reasoning_policy_service import ReasoningPolicyService, ReasoningStreamParser
|
||||
|
||||
|
||||
def test_reasoning_parser_handles_tags_split_across_chunks():
|
||||
parser = ReasoningStreamParser()
|
||||
segments = []
|
||||
for chunk in ["<thi", "nk>内部", "思考</th", "ink>## 回答"]:
|
||||
segments.extend(parser.feed(chunk))
|
||||
segments.extend(parser.finish())
|
||||
|
||||
assert "".join(item.content for item in segments if item.kind == "reasoning") == "内部思考"
|
||||
assert "".join(item.content for item in segments if item.kind == "content") == "## 回答"
|
||||
|
||||
|
||||
def test_reasoning_is_removed_from_history_text_when_policy_is_off():
|
||||
text = "<think>不应透出</think>\n正式回答"
|
||||
|
||||
assert ReasoningPolicyService.strip_reasoning(text) == "\n正式回答"
|
||||
Reference in New Issue
Block a user