From b39323848b284492ebb878184e676be0dc997182 Mon Sep 17 00:00:00 2001 From: weimch Date: Thu, 23 Jul 2026 16:21:03 +0800 Subject: [PATCH] =?UTF-8?q?Feature:=20=E6=94=AF=E6=8C=81trace=E4=B8=8A?= =?UTF-8?q?=E6=8A=A5=E6=80=9D=E8=80=83=E6=96=87=E6=9C=AC=E6=94=BE=E5=9D=97=E9=87=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 方便区分思考文本和正常文本,同时保持兼容性 --- tests/telemetry/test_trace.py | 47 +++++++++++++++++++++++++++++- trpc_agent_sdk/telemetry/_trace.py | 41 +++++++++++++++++++++----- 2 files changed, 80 insertions(+), 8 deletions(-) diff --git a/tests/telemetry/test_trace.py b/tests/telemetry/test_trace.py index 27cf2a35..cce8dad0 100644 --- a/tests/telemetry/test_trace.py +++ b/tests/telemetry/test_trace.py @@ -50,12 +50,13 @@ def _mock_span(): return span -def _make_part(text=None, function_call=None, function_response=None, inline_data=None): +def _make_part(text=None, function_call=None, function_response=None, inline_data=None, thought=False): part = MagicMock() part.text = text part.function_call = function_call part.function_response = function_response part.inline_data = inline_data + part.thought = thought return part @@ -229,6 +230,31 @@ def test_with_new_message(self, mock_get_span): span.set_attribute.assert_any_call("trpc.python.agent.runner.input", "hello\nworld") + @patch("trpc_agent_sdk.telemetry._trace.trace.get_current_span") + def test_with_thought_parts(self, mock_get_span): + span = _mock_span() + mock_get_span.return_value = span + ctx = _make_invocation_context() + + parts = [ + _make_part(text="thinking...", thought=True), + _make_part(text="final answer"), + ] + content = _make_content(parts=parts) + + trace_runner( + app_name="app", + user_id="u", + session_id="s", + invocation_context=ctx, + new_message=content, + ) + + span.set_attribute.assert_any_call( + "trpc.python.agent.runner.input", + "thinking...\nfinal answer", + ) + @patch("trpc_agent_sdk.telemetry._trace.trace.get_current_span") def test_with_none_text_parts(self, mock_get_span): span = _mock_span() @@ -544,6 +570,25 @@ def test_with_user_content(self, mock_get_span): span.set_attribute.assert_any_call("trpc.python.agent.agent.input", "hello\n agent") + @patch("trpc_agent_sdk.telemetry._trace.trace.get_current_span") + def test_with_thought_user_content(self, mock_get_span): + span = _mock_span() + mock_get_span.return_value = span + + parts = [ + _make_part(text="reasoning", thought=True), + _make_part(text="answer"), + ] + user_content = _make_content(parts=parts) + ctx = _make_invocation_context(user_content=user_content) + + trace_agent(ctx) + + span.set_attribute.assert_any_call( + "trpc.python.agent.agent.input", + "reasoning\nanswer", + ) + @patch("trpc_agent_sdk.telemetry._trace.trace.get_current_span") def test_without_user_content(self, mock_get_span): span = _mock_span() diff --git a/trpc_agent_sdk/telemetry/_trace.py b/trpc_agent_sdk/telemetry/_trace.py index 22ff2ad2..9c725906 100644 --- a/trpc_agent_sdk/telemetry/_trace.py +++ b/trpc_agent_sdk/telemetry/_trace.py @@ -62,6 +62,30 @@ def get_trpc_agent_span_name() -> str: return _trpc_agent_span_name +def _join_parts_with_thought_tag(parts) -> str: + """Join part texts, wrapping thought parts in tags. + + Parts with ``thought=True`` (model reasoning/thinking) are wrapped in + ``...`` tags so downstream consumers (e.g. + zhiyan-llm exporter) can distinguish and optionally strip them. + + Args: + parts: A sequence of Part objects with ``text`` and optional + ``thought`` attributes. + + Returns: + The joined text string with thought parts tagged. + """ + result = [] + for part in parts: + text = part.text or "" + if getattr(part, "thought", False): + result.append(f"{text}") + else: + result.append(text) + return "\n".join(result) + + def _safe_json_serialize(obj) -> str: """Convert any Python object to a JSON-serializable type or string. @@ -116,11 +140,11 @@ def trace_runner( span.set_attribute(f"{_trpc_agent_span_name}.runner.session_id", session_id) input_str = "" if new_message and new_message.parts: - input_str = "\n".join([part.text or "" for part in new_message.parts]) + input_str = _join_parts_with_thought_tag(new_message.parts) span.set_attribute(f"{_trpc_agent_span_name}.runner.input", input_str) output_str = "" if last_event and last_event.content and last_event.content.parts: - output_str = "\n".join([part.text or "" for part in last_event.content.parts]) + output_str = _join_parts_with_thought_tag(last_event.content.parts) span.set_attribute(f"{_trpc_agent_span_name}.runner.output", output_str) # Set state attributes for begin and end @@ -181,7 +205,7 @@ def trace_cancellation( # Input (prefixed with [CANCELLED] marker) input_str = "" if new_message and new_message.parts: - input_str += "\n".join([part.text or "" for part in new_message.parts]) + input_str += _join_parts_with_thought_tag(new_message.parts) span.set_attribute(f"{_trpc_agent_span_name}.runner.input", input_str) # Output (prefixed with [CANCELLED] marker, includes partial text or last event) @@ -189,7 +213,7 @@ def trace_cancellation( if partial_text: output_str += partial_text elif last_event and last_event.content and last_event.content.parts: - output_str += "\n".join([part.text or "" for part in last_event.content.parts]) + output_str += _join_parts_with_thought_tag(last_event.content.parts) span.set_attribute(f"{_trpc_agent_span_name}.runner.output", output_str) # Cancellation-specific attributes @@ -245,11 +269,14 @@ def trace_agent( for content in override_messages: if content and content.parts: for part in content.parts: - if part.text and not part.thought: - text_parts.append(part.text) + if part.text: + if getattr(part, "thought", False): + text_parts.append(f"{part.text}") + else: + text_parts.append(part.text) input_str = "\n".join(text_parts) elif invocation_context.user_content and invocation_context.user_content.parts: - input_str = "\n".join([part.text or "" for part in invocation_context.user_content.parts]) + input_str = _join_parts_with_thought_tag(invocation_context.user_content.parts) span.set_attribute(f"{_trpc_agent_span_name}.agent.input", input_str) span.set_attribute(f"{_trpc_agent_span_name}.agent.output", agent_action)