Skip to content
Closed
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
9 changes: 6 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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`.
Expand Down
35 changes: 35 additions & 0 deletions tests/conversation/test_conversation_otel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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",
Expand All @@ -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"
Expand Down
19 changes: 16 additions & 3 deletions weave/conversation/conversation.py
Original file line number Diff line number Diff line change
Expand Up @@ -961,6 +961,7 @@ class Turn(_SpanBase):
"""

agent_name: str = ""
provider_name: str = ""
model: str = ""
agent_id: str = ""
agent_description: str = ""
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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,

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.

Seems the current standard has removed this label? open-telemetry/semantic-conventions-genai#289

Curious about what should gen_ai.provider.name represent on this internal invoke_agent span when one turn can use multiple model providers?

could setting it here, for example, conflict with the current OTel convention, which removed this attribute from internal agent spans?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

oh true, good findings!

From PR's design, the turn's provider name should propagate down to the children spans, and the children spans can override the provider name.

From current state, it wouldn't make sense to show this info at the turn level since one turn can clearly use multiple model providers.

I am okay dropping this PR as well!

model=self.model,
input_messages=messages,
output_messages=output_messages,
Expand Down Expand Up @@ -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 = "",
Expand All @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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.
Expand All @@ -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 [],
)
Expand Down Expand Up @@ -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 = "",
Expand All @@ -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
Expand All @@ -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,
Expand Down
Loading