Skip to content
Merged
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
6 changes: 3 additions & 3 deletions .github/workflows/devflow-fix-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ jobs:
- if: ${{ steps.team.outputs.permitted == 'true' }}
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
version: '0.13.1'
version: '0.13.5'
enable-cache: false
- name: Install trusted controller dependencies
if: ${{ steps.team.outputs.permitted == 'true' }}
Expand Down Expand Up @@ -231,7 +231,7 @@ jobs:
python-version: '3.13'
- uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
version: '0.13.1'
version: '0.13.5'
enable-cache: false
- name: Install trusted controller dependencies
run: uv sync --frozen --directory "$DEVFLOW_PATH"
Expand Down Expand Up @@ -407,7 +407,7 @@ jobs:
python-version: '3.13'
- uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
version: '0.13.1'
version: '0.13.5'
enable-cache: false
- name: Install trusted controller dependencies
run: uv sync --frozen --directory "$DEVFLOW_PATH"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/devflow-pr-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ jobs:
- name: Set up uv
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
version: "0.13.1"
version: "0.13.5"
enable-cache: auto

- name: Install DevFlow dependencies
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/issue-triage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ jobs:
- name: Set up uv
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
version: "0.13.1"
version: "0.13.5"
enable-cache: true

- name: Install DevFlow dependencies
Expand Down
2 changes: 1 addition & 1 deletion python/.pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ repos:
additional_dependencies: ["bandit[toml]"]
- repo: https://github.com/astral-sh/uv-pre-commit
# uv version.
rev: 0.13.1
rev: 0.13.5
hooks:
# Update the uv lockfile
- id: uv-lock
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -648,9 +648,7 @@ def _emit_text(content: Content, flow: FlowState, skip_text: bool = False) -> li
flow.text_delta_count = 0
_open_text_segment(flow, flow.message_id)
events.append(TextMessageStartEvent(message_id=flow.message_id, role="assistant"))
elif flow.accumulated_text and content.text == flow.accumulated_text and (
flow.text_delta_count > 1
):
elif flow.accumulated_text and content.text == flow.accumulated_text and (flow.text_delta_count > 1):
# Guard against full-message replay chunks that can appear after streaming deltas.
# A duplicate equal to the whole accumulated text is only a safe replay signal when
# the text already streamed in more than one chunk. With a single prior chunk the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -465,9 +465,10 @@ async def failing(message: Any, ctx: WorkflowContext[Any, str]) -> None:
assert "workflow exploded" not in error.message
records = [record for record in caplog.records if record.name == "agent_framework_ag_ui._workflow_run"]
assert len(records) == 1
assert records[0].exc_info is not None
assert isinstance(records[0].exc_info[1], RuntimeError)
assert records[0].exc_info[2] is not None
exc_info = records[0].exc_info
assert exc_info
assert isinstance(exc_info[1], RuntimeError)
assert exc_info[2] is not None
assert "workflow exploded" in records[0].getMessage()


Expand Down
34 changes: 24 additions & 10 deletions python/packages/core/tests/core/test_observability.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@
get_function_span,
)


def _span_attributes(span: Any) -> dict[str, Any]:
attributes = span.attributes
return dict(attributes) if attributes is not None else {}


# region Test constants


Expand Down Expand Up @@ -2043,7 +2049,8 @@ async def test_enable_instrumentation_controls_message_event_emission(

spans = span_exporter.get_finished_spans()
assert len(spans) == 1
attributes = spans[0].attributes or {}
attributes = spans[0].attributes
assert attributes is not None
assert (OtelAttr.INPUT_MESSAGES in attributes) is (enable_sensitive_data and bool(semconv))
assert (OtelAttr.OUTPUT_MESSAGES in attributes) is (enable_sensitive_data and bool(semconv))

Expand Down Expand Up @@ -2528,7 +2535,8 @@ async def test_chat_client_observability_provider_name_under_baseline_semconv(

spans = span_exporter.get_finished_spans()
assert len(spans) == 1
attributes = spans[0].attributes or {}
attributes = spans[0].attributes
assert attributes is not None
assert OtelAttr.SYSTEM in attributes
assert OtelAttr.PROVIDER_NAME not in attributes

Expand Down Expand Up @@ -4026,7 +4034,8 @@ def test_capture_messages_baseline_semconv_emits_events_only(span_exporter: InMe

spans = span_exporter.get_finished_spans()
assert len(spans) == 1
attributes = spans[0].attributes or {}
attributes = spans[0].attributes
assert attributes is not None
assert OtelAttr.INPUT_MESSAGES not in attributes
mock_emit.assert_called_once()
assert mock_emit.call_args.kwargs["event_name"] == "gen_ai.user.message"
Expand Down Expand Up @@ -4061,7 +4070,8 @@ def test_capture_messages_latest_experimental_emits_span_attribute_only_when_eve

spans = span_exporter.get_finished_spans()
assert len(spans) == 1
attributes = spans[0].attributes or {}
attributes = spans[0].attributes
assert attributes is not None
input_messages = json.loads(cast(str, attributes[OtelAttr.INPUT_MESSAGES]))
assert [msg.get("role") for msg in input_messages] == ["user"]
assert mock_emit.call_count == 0
Expand Down Expand Up @@ -4094,7 +4104,8 @@ def test_capture_messages_defaults_emit_both_events_and_span_attribute(span_expo

spans = span_exporter.get_finished_spans()
assert len(spans) == 1
attributes = spans[0].attributes or {}
attributes = spans[0].attributes
assert attributes is not None
input_messages = json.loads(cast(str, attributes[OtelAttr.INPUT_MESSAGES]))
assert [msg.get("role") for msg in input_messages] == ["user"]
mock_emit.assert_called_once()
Expand Down Expand Up @@ -4162,7 +4173,8 @@ def test_capture_messages_preserves_custom_finish_reason(span_exporter: InMemory

assert mock_emit.call_args.kwargs["body"]["finish_reason"] == "guardrail"
spans = span_exporter.get_finished_spans()
attributes = spans[0].attributes or {}
attributes = spans[0].attributes
assert attributes is not None
output_messages = json.loads(cast(str, attributes[OtelAttr.OUTPUT_MESSAGES]))
assert output_messages[-1]["finish_reason"] == "guardrail"

Expand Down Expand Up @@ -4199,7 +4211,9 @@ def test_capture_messages_exports_native_otel_event(span_exporter: InMemorySpanE
record = readable_record.log_record
assert record.event_name == "gen_ai.user.message"
assert record.body == {"content": "Test"}
assert dict(record.attributes or {}) == {"gen_ai.system": "test_provider"}
record_attributes = record.attributes
assert record_attributes is not None
assert dict(record_attributes) == {"gen_ai.system": "test_provider"}
assert record.trace_id == span_context.trace_id
assert record.span_id == span_context.span_id

Expand Down Expand Up @@ -5749,7 +5763,7 @@ async def before_run(
agent_spans = [
span
for span in spans
if span.attributes and span.attributes.get(OtelAttr.OPERATION.value) == OtelAttr.AGENT_INVOKE_OPERATION
if _span_attributes(span).get(OtelAttr.OPERATION.value) == OtelAttr.AGENT_INVOKE_OPERATION
]
assert len(agent_spans) == 1

Expand Down Expand Up @@ -5813,13 +5827,13 @@ async def after_run(
agent_spans = [
span
for span in spans
if span.attributes and span.attributes.get(OtelAttr.OPERATION.value) == OtelAttr.AGENT_INVOKE_OPERATION
if _span_attributes(span).get(OtelAttr.OPERATION.value) == OtelAttr.AGENT_INVOKE_OPERATION
]
assert len(agent_spans) == 1
chat_spans = [
span
for span in spans
if span.attributes and span.attributes.get(OtelAttr.OPERATION.value) == OtelAttr.CHAT_COMPLETION_OPERATION
if _span_attributes(span).get(OtelAttr.OPERATION.value) == OtelAttr.CHAT_COMPLETION_OPERATION
]
assert len(chat_spans) == 2

Expand Down
23 changes: 14 additions & 9 deletions python/packages/core/tests/workflow/test_edge.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@
from agent_framework.observability import EdgeGroupDeliveryStatus, OtelAttr


def _span_attributes(span: Any) -> dict[str, Any]:
attributes = span.attributes
return dict(attributes) if attributes is not None else {}


@dataclass
class MockMessage:
"""A mock message for testing purposes."""
Expand Down Expand Up @@ -331,7 +336,7 @@ async def test_single_edge_group_tracing_success(span_exporter: InMemorySpanExpo
assert success is True

spans = span_exporter.get_finished_spans()
edge_group_spans = [s for s in spans if s.attributes and s.attributes.get("edge_group.type") is not None]
edge_group_spans = [s for s in spans if _span_attributes(s).get("edge_group.type") is not None]

assert len(edge_group_spans) == 1

Expand Down Expand Up @@ -376,7 +381,7 @@ async def test_single_edge_group_tracing_condition_failure(span_exporter: InMemo
assert success is True # Returns True but condition failed

spans = span_exporter.get_finished_spans()
edge_group_spans = [s for s in spans if s.attributes and s.attributes.get("edge_group.type") is not None]
edge_group_spans = [s for s in spans if _span_attributes(s).get("edge_group.type") is not None]

assert len(edge_group_spans) == 1

Expand Down Expand Up @@ -450,7 +455,7 @@ async def test_single_edge_group_tracing_type_mismatch(span_exporter: InMemorySp
assert success is False

spans = span_exporter.get_finished_spans()
edge_group_spans = [s for s in spans if s.attributes and s.attributes.get("edge_group.type") is not None]
edge_group_spans = [s for s in spans if _span_attributes(s).get("edge_group.type") is not None]

assert len(edge_group_spans) == 1

Expand Down Expand Up @@ -484,7 +489,7 @@ async def test_single_edge_group_tracing_target_mismatch(span_exporter: InMemory
assert success is False

spans = span_exporter.get_finished_spans()
edge_group_spans = [s for s in spans if s.attributes and s.attributes.get("edge_group.type") is not None]
edge_group_spans = [s for s in spans if _span_attributes(s).get("edge_group.type") is not None]

assert len(edge_group_spans) == 1

Expand Down Expand Up @@ -900,7 +905,7 @@ async def test_fan_out_edge_group_tracing_success(span_exporter: InMemorySpanExp
assert success is True

spans = span_exporter.get_finished_spans()
edge_group_spans = [s for s in spans if s.attributes and s.attributes.get("edge_group.type") is not None]
edge_group_spans = [s for s in spans if _span_attributes(s).get("edge_group.type") is not None]

assert len(edge_group_spans) == 1

Expand Down Expand Up @@ -956,7 +961,7 @@ async def test_fan_out_edge_group_tracing_with_target(span_exporter: InMemorySpa
assert success is True

spans = span_exporter.get_finished_spans()
edge_group_spans = [s for s in spans if s.attributes and s.attributes.get("edge_group.type") is not None]
edge_group_spans = [s for s in spans if _span_attributes(s).get("edge_group.type") is not None]

assert len(edge_group_spans) == 1

Expand Down Expand Up @@ -1126,7 +1131,7 @@ async def test_fan_in_edge_group_tracing_buffered(span_exporter: InMemorySpanExp
assert success is True

spans = span_exporter.get_finished_spans()
edge_group_spans = [s for s in spans if s.attributes and s.attributes.get("edge_group.type") is not None]
edge_group_spans = [s for s in spans if _span_attributes(s).get("edge_group.type") is not None]

assert len(edge_group_spans) == 1

Expand Down Expand Up @@ -1160,7 +1165,7 @@ async def test_fan_in_edge_group_tracing_buffered(span_exporter: InMemorySpanExp
assert success is True

spans = span_exporter.get_finished_spans()
edge_group_spans = [s for s in spans if s.attributes and s.attributes.get("edge_group.type") is not None]
edge_group_spans = [s for s in spans if _span_attributes(s).get("edge_group.type") is not None]

assert len(edge_group_spans) == 1

Expand Down Expand Up @@ -1206,7 +1211,7 @@ async def test_fan_in_edge_group_tracing_type_mismatch(span_exporter: InMemorySp
assert success is False

spans = span_exporter.get_finished_spans()
edge_group_spans = [s for s in spans if s.attributes and s.attributes.get("edge_group.type") is not None]
edge_group_spans = [s for s in spans if _span_attributes(s).get("edge_group.type") is not None]

assert len(edge_group_spans) == 1

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@
)


def _span_attributes(span: Any) -> dict[str, Any]:
attributes = span.attributes
return dict(attributes) if attributes is not None else {}


def _assert_workflow_error_details(
details: WorkflowErrorDetails,
private_error_detail: str,
Expand Down Expand Up @@ -228,7 +233,7 @@ async def test_trace_context_handling(span_exporter: InMemorySpanExporter) -> No
# Check that spans were created with proper attributes
spans = span_exporter.get_finished_spans()
# Processing spans now use executor_id as the span name
processing_spans = [s for s in spans if s.attributes and s.attributes.get("executor.id") == "test-executor"]
processing_spans = [s for s in spans if _span_attributes(s).get("executor.id") == "test-executor"]
sending_spans = [s for s in spans if s.name == "message.send"]

assert len(processing_spans) >= 1
Expand Down Expand Up @@ -355,7 +360,7 @@ async def test_end_to_end_workflow_tracing(span_exporter: InMemorySpanExporter)
# Should have workflow span, processing spans, and sending spans
# Processing spans now use executor_id as the span name, filter by executor.id attribute
workflow_spans = [s for s in spans if s.name == "workflow.run"]
processing_spans = [s for s in spans if s.attributes and s.attributes.get("executor.id") is not None]
processing_spans = [s for s in spans if _span_attributes(s).get("executor.id") is not None]
sending_spans = [s for s in spans if s.name == "message.send"]
build_spans_after_run = [s for s in spans if s.name == "workflow.build"]

Expand All @@ -379,7 +384,7 @@ async def test_end_to_end_workflow_tracing(span_exporter: InMemorySpanExporter)
assert "workflow.completed" in event_names

# Test fan-in span linking: find the aggregator's processing span
aggregator_spans = [s for s in processing_spans if s.attributes and s.attributes.get("executor.id") == "aggregator"]
aggregator_spans = [s for s in processing_spans if _span_attributes(s).get("executor.id") == "aggregator"]
assert len(aggregator_spans) == 1

aggregator_span = aggregator_spans[0]
Expand All @@ -388,12 +393,8 @@ async def test_end_to_end_workflow_tracing(span_exporter: InMemorySpanExporter)
assert aggregator_span.links is not None

# Find the sending spans from executor2 and executor3 by checking parent relationships
executor2_processing_spans = [
s for s in processing_spans if s.attributes and s.attributes.get("executor.id") == "executor2"
]
executor3_processing_spans = [
s for s in processing_spans if s.attributes and s.attributes.get("executor.id") == "executor3"
]
executor2_processing_spans = [s for s in processing_spans if _span_attributes(s).get("executor.id") == "executor2"]
executor3_processing_spans = [s for s in processing_spans if _span_attributes(s).get("executor.id") == "executor3"]

# Get span IDs from processing spans
executor2_processing_span_ids = {format(s.context.span_id, "016x") for s in executor2_processing_spans if s.context}
Expand Down Expand Up @@ -582,7 +583,9 @@ def test_workflow_span_lifecycle_exceptions_are_not_failures(
assert len(spans) == 1
span = spans[0]
assert span.status.status_code != trace.StatusCode.ERROR
assert not span.attributes or OtelAttr.ERROR_TYPE not in span.attributes
attributes = span.attributes
if attributes is not None:
assert OtelAttr.ERROR_TYPE not in attributes
assert all(event.name != "exception" for event in span.events)


Expand Down

Large diffs are not rendered by default.

92 changes: 46 additions & 46 deletions python/packages/devui/agent_framework_devui/ui/assets/index.js

Large diffs are not rendered by default.

Loading
Loading