From b2fabd71232699420624ff42c960ec58ec895474 Mon Sep 17 00:00:00 2001 From: Rick Gao Date: Tue, 28 Jul 2026 16:55:01 -0700 Subject: [PATCH] feat(weave): add provider name to turns --- AGENTS.md | 9 +++-- tests/conversation/test_conversation_otel.py | 35 ++++++++++++++++++++ weave/conversation/conversation.py | 19 +++++++++-- 3 files changed, 57 insertions(+), 6 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 8856d127391a..b54304053828 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -425,6 +425,9 @@ pnpm exec tsx examples/claudeAgents.ts - `Turn.messages` stores input messages, while `Turn.output_messages` stores the terminal agent response. `Turn.record(messages=..., output_messages=...)` replaces the two lists independently. +- `Turn.provider_name` is the typed source for `gen_ai.provider.name` on the + root `invoke_agent` span. Pass it through `start_turn`, `Turn.record`, or + `log_turn` instead of setting that semantic attribute with `set_attributes`. - Keep streaming and batch paths aligned: `Turn._build_attrs()` must apply content gating and PII redaction to both lists before passing them to `invoke_agent_attributes()`, and `log_turn()` must accept both fields. @@ -512,9 +515,9 @@ If there is something that doesn't make sense architecturally, devex-wise, or pr Think of this as the reverse-task assignment - a place where you can communicate back to us. - [ ] Add TypeScript testing guidelines -- [ ] Add `output_messages` to Python `Turn.record()` for parity with - TypeScript `Turn.record({outputMessages: ...})`; the lower-level Python - `invoke_agent_attributes()` builder already supports agent output. +- [ ] Reconcile Ruff 0.16's `RUF105` requirement to use `ruff: ignore` with + this file's instruction to use spot-level `# noqa` for intentional + `PLW` suppressions. - [ ] Repair the existing `pnpm run typecheck:examples` failures caused by OpenAI type drift in `examples/agent.ts`, `classesWithOps.ts`, `imageGeneration.ts`, `quickstart*.ts`, and `streamFunctionCalls.ts`. diff --git a/tests/conversation/test_conversation_otel.py b/tests/conversation/test_conversation_otel.py index 7428eb8f8f93..6808adbe252d 100644 --- a/tests/conversation/test_conversation_otel.py +++ b/tests/conversation/test_conversation_otel.py @@ -963,12 +963,29 @@ def test_start_turn_system_instructions_param_emitted_on_span( {"type": "text", "content": "You are a weather bot"}, ] + def test_start_turn_provider_name_param_emitted_on_span( + self, otel_spans: InMemorySpanExporter + ) -> None: + with Conversation(agent_name="bot", conversation_id="convo-provider") as s: + with s.start_turn(provider_name="anthropic"): + pass + + spans = otel_spans.get_finished_spans() + turn_spans = [sp for sp in spans if sp.name == "invoke_agent bot"] + assert len(turn_spans) == 1 + attrs = dict(turn_spans[0].attributes or {}) + assert attrs["gen_ai.provider.name"] == "anthropic" + def test_module_start_turn_system_instructions_param(self) -> None: # weave.start_turn(...) wires the field onto the returned Turn whether # or not a conversation is active (delegates to Conversation.start_turn when one is). turn = start_turn(system_instructions=["You are a weather bot"]) assert turn.system_instructions == ["You are a weather bot"] + def test_module_start_turn_provider_name_param(self) -> None: + turn = start_turn(provider_name="anthropic") + assert turn.provider_name == "anthropic" + def test_conversation_agent_identity_propagates_to_turn_span( self, otel_spans: InMemorySpanExporter ) -> None: @@ -1549,6 +1566,20 @@ def test_agent_identity_fields_emitted( assert attrs["gen_ai.agent.description"] == "A helpful bot" assert attrs["gen_ai.agent.version"] == "v3" + def test_provider_name_emitted(self, otel_spans: InMemorySpanExporter) -> None: + log_turn( + conversation_id="convo-provider", + agent_name="bot", + provider_name="anthropic", + started_at=_ts(0), + ended_at=_ts(1), + ) + spans = otel_spans.get_finished_spans() + turn_spans = [sp for sp in spans if sp.name == "invoke_agent bot"] + assert len(turn_spans) == 1 + attrs = dict(turn_spans[0].attributes or {}) + assert attrs["gen_ai.provider.name"] == "anthropic" + def test_subagent_system_instructions_emitted( self, otel_spans: InMemorySpanExporter ) -> None: @@ -1862,6 +1893,7 @@ def test_forwards_all_turn_fields(self, otel_spans: InMemorySpanExporter) -> Non turns=[ Turn( agent_name="bot", + provider_name="anthropic", agent_id="agent-123", agent_description="A helpful bot", agent_version="v2", @@ -1895,6 +1927,7 @@ def test_forwards_all_turn_fields(self, otel_spans: InMemorySpanExporter) -> Non assert attrs["gen_ai.agent.id"] == "agent-123" assert attrs["gen_ai.agent.description"] == "A helpful bot" assert attrs["gen_ai.agent.version"] == "v2" + assert attrs["gen_ai.provider.name"] == "anthropic" def test_agent_identity_defaults_applied( self, otel_spans: InMemorySpanExporter @@ -2243,6 +2276,7 @@ def test_sets_all_fields(self) -> None: output_messages=[Message.assistant("hello")], system_instructions=["sys"], agent_name="bot", + provider_name="openai", model="gpt-4o", agent_id="id-1", agent_description="desc", @@ -2252,6 +2286,7 @@ def test_sets_all_fields(self) -> None: assert turn.output_messages == [Message.assistant("hello")] assert turn.system_instructions == ["sys"] assert turn.agent_name == "bot" + assert turn.provider_name == "openai" assert turn.model == "gpt-4o" assert turn.agent_id == "id-1" assert turn.agent_description == "desc" diff --git a/weave/conversation/conversation.py b/weave/conversation/conversation.py index 6134c2cda8ff..f2827a04230b 100644 --- a/weave/conversation/conversation.py +++ b/weave/conversation/conversation.py @@ -961,6 +961,7 @@ class Turn(_SpanBase): """ agent_name: str = "" + provider_name: str = "" model: str = "" agent_id: str = "" agent_description: str = "" @@ -1078,6 +1079,7 @@ def record( output_messages: list[Message] | None = None, system_instructions: list[str] | None = None, agent_name: str | None = None, + provider_name: str | None = None, model: str | None = None, agent_id: str | None = None, agent_description: str | None = None, @@ -1108,6 +1110,8 @@ def record( self.system_instructions = system_instructions if agent_name is not None: self.agent_name = agent_name + if provider_name is not None: + self.provider_name = provider_name if model is not None: self.model = model if agent_id is not None: @@ -1148,6 +1152,7 @@ def _build_attrs( agent_name=self.agent_name, conversation_id=conversation_id, conversation_name=conversation_name, + provider_name=self.provider_name, model=self.model, input_messages=messages, output_messages=output_messages, @@ -1251,6 +1256,7 @@ def start_turn( user_message: str = "", model: str = "", agent_name: str = "", + provider_name: str = "", agent_id: str = "", agent_description: str = "", agent_version: str = "", @@ -1262,8 +1268,9 @@ def start_turn( ``get_current_turn()`` regardless of whether a context manager is used. Each of ``agent_name`` / ``model`` / ``agent_id`` / ``agent_description`` / ``agent_version`` falls back to the conversation's default when left - empty; ``continue_parent_trace`` is inherited. Override any of them later - via ``turn.record(...)``. + empty; ``provider_name`` is specific to this turn and + ``continue_parent_trace`` is inherited. Override any typed turn field + later via ``turn.record(...)``. ``system_instructions`` (the agent's system prompt) is carried on the turn's invoke_agent span; it can also be set later via attribute @@ -1273,6 +1280,7 @@ def start_turn( self._current_turn.end() turn = Turn( agent_name=agent_name or self.agent_name, + provider_name=provider_name, model=model or self.model, agent_id=agent_id or self.agent_id, agent_description=agent_description or self.agent_description, @@ -1367,6 +1375,7 @@ def start_turn( user_message: str = "", model: str = "", agent_name: str = "", + provider_name: str = "", system_instructions: list[str] | None = None, ) -> Turn: """Create and activate a turn. Uses the current conversation if available. @@ -1382,10 +1391,12 @@ def start_turn( user_message=user_message, model=model, agent_name=agent_name, + provider_name=provider_name, system_instructions=system_instructions, ) turn = Turn( agent_name=agent_name, + provider_name=provider_name, model=model, system_instructions=system_instructions or [], ) @@ -1655,6 +1666,7 @@ def log_turn( conversation_id: str, agent_name: str = "", conversation_name: str = "", + provider_name: str = "", model: str = "", agent_id: str = "", agent_description: str = "", @@ -1676,7 +1688,7 @@ def log_turn( ``ended_at`` set; the emitted OTel span timestamps come from those fields. Falls back to the earliest/latest child timestamp, then ``now()``, when the turn doesn't supply its own. ``agent_id`` / ``agent_description`` / - ``agent_version`` mirror the streaming path. + ``agent_version`` and ``provider_name`` mirror the streaming path. ``attributes`` are stamped on every emitted span; the streaming path reads these from the active conversation instead. Use custom, non-semconv keys: a @@ -1700,6 +1712,7 @@ def log_turn( ) turn = Turn( agent_name=agent_name, + provider_name=provider_name, model=model, agent_id=agent_id, agent_description=agent_description,