Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 21 additions & 20 deletions astrbot/core/agent/runners/tool_loop_agent_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -794,27 +794,28 @@ async def step(self):
await self._complete_with_assistant_response(llm_resp)

# 返回 LLM 结果
if llm_resp.reasoning_content:
yield AgentResponse(
type="llm_result",
data=AgentResponseData(
chain=MessageChain(type="reasoning").message(
llm_resp.reasoning_content,
if not llm_resp.tools_call_name:
if llm_resp.reasoning_content:
yield AgentResponse(
type="llm_result",
data=AgentResponseData(
chain=MessageChain(type="reasoning").message(
llm_resp.reasoning_content,
),
),
),
)
if llm_resp.result_chain:
yield AgentResponse(
type="llm_result",
data=AgentResponseData(chain=llm_resp.result_chain),
)
elif llm_resp.completion_text:
yield AgentResponse(
type="llm_result",
data=AgentResponseData(
chain=MessageChain().message(llm_resp.completion_text),
),
)
)
if llm_resp.result_chain:
yield AgentResponse(
type="llm_result",
data=AgentResponseData(chain=llm_resp.result_chain),
)
elif llm_resp.completion_text:
yield AgentResponse(
type="llm_result",
data=AgentResponseData(
chain=MessageChain().message(llm_resp.completion_text),
),
)

# 如果有工具调用,还需处理工具调用
if llm_resp.tools_call_name:
Expand Down
14 changes: 14 additions & 0 deletions astrbot/core/provider/sources/openai_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -769,6 +769,20 @@ def _normalize_content(raw_content: Any, strip: bool = True) -> str:

if isinstance(raw_content, str):
content = raw_content.strip() if strip else raw_content
repr_like_prefix = "[{text="
repr_like_suffix = ", type=text}]"
repr_like_max_len = 8192
check_content = raw_content.strip()
while (
check_content.startswith(repr_like_prefix)
and check_content.endswith(repr_like_suffix)
and len(check_content) < repr_like_max_len
):
check_content = check_content[
len(repr_like_prefix) : -len(repr_like_suffix)
].strip()
if check_content != raw_content.strip():
return check_content.strip() if strip else check_content
Comment on lines +775 to +785

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For better readability and maintainability, it's a good practice to extract magic strings and numbers into named constants. This makes the code easier to understand and modify in the future if these values need to be changed.

            check_content = raw_content.strip()
            # Handle non-standard repr-like strings from some providers, e.g., "[{text=..., type=text}]"
            REPR_LIKE_PREFIX = "[{text=""
            REPR_LIKE_SUFFIX = ", type=text}]"
            REPR_LIKE_MAX_LEN = 8192
            while (
                check_content.startswith(REPR_LIKE_PREFIX)
                and check_content.endswith(REPR_LIKE_SUFFIX)
                and len(check_content) < REPR_LIKE_MAX_LEN
            ):
                check_content = check_content[
                    len(REPR_LIKE_PREFIX) : -len(REPR_LIKE_SUFFIX)
                ].strip()
            if check_content != raw_content.strip():
                return check_content.strip() if strip else check_content

# Check if the string is a JSON-encoded list (e.g., "[{'type': 'text', ...}]")
# This can happen when streaming concatenates content that was originally list format
# Only check if it looks like a complete JSON array (requires strip for check)
Expand Down
28 changes: 28 additions & 0 deletions tests/test_openai_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,34 @@ def _make_groq_provider(overrides: dict | None = None) -> ProviderGroq:
)


def test_normalize_content_handles_text_part_repr_string():
raw = "[{text=I will use the file reading tool., type=text}]"

assert ProviderOpenAIOfficial._normalize_content(raw) == (
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
"I will use the file reading tool."
)


def test_normalize_content_handles_text_part_repr_string_without_strip():
raw = " [{text=I will use the file reading tool., type=text}] "

assert ProviderOpenAIOfficial._normalize_content(raw, strip=False) == (
"I will use the file reading tool."
)


def test_normalize_content_drops_empty_nested_text_part_repr_string():
raw = "[{text=[{text=[{text=, type=text}], type=text}], type=text}]"

assert ProviderOpenAIOfficial._normalize_content(raw) == ""
Comment thread
sourcery-ai[bot] marked this conversation as resolved.


def test_normalize_content_handles_non_empty_nested_text_part_repr_string():
raw = "[{text=[{text=[{text=hello, type=text}], type=text}], type=text}]"

assert ProviderOpenAIOfficial._normalize_content(raw) == "hello"


def test_create_http_client_uses_openai_httpx_module(monkeypatch):
captured: dict[str, object] = {}

Expand Down
32 changes: 32 additions & 0 deletions tests/test_tool_loop_agent_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,38 @@ async def snapshot_context_manager(messages, trusted_token_usage=0):
assert final_contexts[-1].content == runner.MAX_STEPS_REACHED_PROMPT


@pytest.mark.asyncio
async def test_tool_call_assistant_preface_is_not_sent_as_llm_result(
runner, provider_request, mock_tool_executor, mock_hooks, mock_provider
):
"""Assistant preface text on a tool-call turn must stay internal."""
mock_provider.should_call_tools = True
mock_provider.max_calls_before_normal_response = 1

await runner.reset(
provider=mock_provider,
request=provider_request,
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
run_context=ContextWrapper(context=None),
tool_executor=mock_tool_executor,
agent_hooks=mock_hooks,
streaming=False,
)

responses = []
async for response in runner.step_until_done(3):
responses.append(response)

Comment thread
sourcery-ai[bot] marked this conversation as resolved.
response_types = [response.type for response in responses]
assert response_types[-1] == "llm_result"
assert "llm_result" not in response_types[:-1]

llm_results = [response for response in responses if response.type == "llm_result"]
llm_texts = [response.data["chain"].get_plain_text() for response in llm_results]
assert len(llm_results) == 1
assert "我需要使用工具来帮助您" not in llm_texts
assert llm_texts == ["这是我的最终回答"]


@pytest.mark.asyncio
async def test_tool_loop_next_request_includes_tool_result(
runner, provider_request, mock_tool_executor, mock_hooks
Expand Down