Skip to content

Commit 0237276

Browse files
authored
Merge origin/main and resolve uv.lock conflict
2 parents 38ccf15 + 8f0b601 commit 0237276

148 files changed

Lines changed: 12869 additions & 1937 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ jobs:
8585
defaults:
8686
run:
8787
working-directory: ./
88+
env:
89+
PYTHONPATH: ${{ github.workspace }}/versioning/helper
8890

8991
strategy:
9092
matrix:
@@ -107,15 +109,13 @@ jobs:
107109
run: uv lock && uv sync --locked --all-extras --dev
108110

109111
- name: Verify centralized version constraints
110-
run: python scripts/verify_constraints.py
112+
run: uv run --frozen tox -e verify-constraints
111113

112114
- name: Check linting
113-
run: |
114-
uv run --frozen ruff check . --preview
115+
run: uv run --frozen tox -e lint
115116

116117
- name: Check formatting
117-
run: |
118-
uv run --frozen ruff format --check .
118+
run: uv run --frozen tox -e format
119119

120120
- name: Build package
121121
run: |
@@ -124,14 +124,12 @@ jobs:
124124
AGENT365_PYTHON_SDK_PACKAGE_VERSION: ${{ needs.version-number.outputs.PACKAGE_VERSION }}
125125

126126
- name: Run unit tests
127-
run: |
128-
uv run --frozen pytest tests/ -v --tb=short -m "not integration"
127+
run: uv run --frozen tox -e py3${{ matrix.python-version == '3.11' && '11' || '12' }}
129128

130129
- name: Run integration tests
131130
# Only run integration tests if secrets are available
132131
if: ${{ vars.RUN_INTEGRATION_TESTS == 'true' }}
133-
run: |
134-
uv run --frozen pytest -m integration -v --tb=short
132+
run: uv run --frozen tox -e integration
135133
env:
136134
AZURE_OPENAI_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY }}
137135
AZURE_OPENAI_ENDPOINT: ${{ vars.AZURE_OPENAI_ENDPOINT }}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ dist/
7272
build/
7373
.eggs/
7474
.pytest_cache/
75+
.tox/
7576
_version.py
7677

7778
# Test coverage and reports

CLAUDE.md

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,31 @@ pytest tests/ --cov=libraries --cov-report=html -v
6161
- `unit`: Fast, mocked tests (default)
6262
- `integration`: Slow tests requiring real services/API keys
6363

64+
### Running with tox
65+
66+
```bash
67+
# Run all default environments (lint, format, unit tests on 3.11 + 3.12)
68+
uv run tox
69+
70+
# Run a specific environment
71+
uv run tox -e lint
72+
uv run tox -e format
73+
uv run tox -e py311
74+
uv run tox -e py312
75+
76+
# Run integration tests (requires env vars)
77+
uv run tox -e integration
78+
79+
# Verify centralized dependency constraints
80+
uv run tox -e verify-constraints
81+
82+
# Pass extra args to pytest
83+
uv run tox -e py311 -- -k "environment"
84+
85+
# List all available environments
86+
uv run tox list
87+
```
88+
6489
### Linting and Formatting
6590

6691
```bash
@@ -194,6 +219,18 @@ Place it before imports with one blank line after.
194219
- **Never** use the keyword "Kairo" in code - it's a legacy reference that must be removed/replaced
195220
- If found during code review, flag for removal
196221

222+
### Observability Export Configuration — Coordinated Review Required
223+
224+
The following three constants must stay in sync. If a PR changes **any one** of them, the reviewer (human or Copilot) **must** ask the author to confirm the other two are still correct:
225+
226+
| Constant | Location |
227+
|---|---|
228+
| `PROD_OBSERVABILITY_SCOPE` | `libraries/microsoft-agents-a365-runtime/microsoft_agents_a365/runtime/environment_utils.py` |
229+
| `DEFAULT_ENDPOINT_URL` | `libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/exporters/agent365_exporter.py` |
230+
| Export URL path pattern | `build_export_url()` in `libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/exporters/utils.py` |
231+
232+
Snapshot tests in `tests/observability/core/test_export_config_consistency.py` will fail if any value drifts, but the developer must also verify the values are correct for the target environment — the tests only catch accidental drift, not intentional-but-incomplete updates.
233+
197234
### Python Conventions
198235

199236
- Type hints required on all function parameters and return types
@@ -241,7 +278,8 @@ Place it before imports with one blank line after.
241278
The `.github/workflows/ci.yml` pipeline:
242279
- Runs on pushes to `main` and `release/*` branches
243280
- Tests both Python 3.11 and 3.12
244-
- Executes: lint check → format check → build → unit tests → integration tests (if secrets available)
281+
- Uses **tox** (via `uv run --frozen tox -e <env>`) to run lint, format, test, and constraint verification steps
282+
- Executes: verify-constraints → lint → format → build → unit tests → integration tests (if secrets available)
245283
- Only publishes packages on `release/*` branches when SDK changes detected
246284
- Uses git-based versioning (tags on release branches = official versions, others = dev versions)
247285

docs/design.md

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -81,21 +81,23 @@ The foundation for distributed tracing in agent applications. Built on OpenTelem
8181

8282
| Class | Purpose |
8383
|-------|---------|
84-
| `InvokeAgentDetails` | Agent endpoint, session ID, and invocation metadata |
84+
| `InvokeAgentScopeDetails` | Agent endpoint and invocation metadata |
8585
| `AgentDetails` | Agent identification and metadata |
86-
| `TenantDetails` | Tenant identification for multi-tenant scenarios |
86+
| `UserDetails` | Human caller identification (user ID, email, name, IP) |
87+
| `CallerDetails` | Wrapper for user details and/or caller agent details |
88+
| `SpanDetails` | Parent context, timing, and span kind for custom spans |
8789
| `InferenceCallDetails` | Model name, tokens, provider information |
8890
| `ToolCallDetails` | Tool name, arguments, endpoint |
89-
| `Request` | Execution context and correlation ID |
91+
| `Request` | Content, correlation ID, and conversation ID |
9092

9193
**Usage Example:**
9294

9395
```python
9496
from microsoft_agents_a365.observability.core import (
9597
configure,
9698
InvokeAgentScope,
97-
InvokeAgentDetails,
98-
TenantDetails,
99+
InvokeAgentScopeDetails,
100+
AgentDetails,
99101
Request,
100102
BaggageBuilder,
101103
)
@@ -112,9 +114,9 @@ configure(
112114
with BaggageBuilder().tenant_id(tenant_id).agent_id(agent_id).build():
113115
# Trace agent invocation
114116
with InvokeAgentScope.start(
115-
invoke_agent_details=InvokeAgentDetails(...),
116-
tenant_details=TenantDetails(...),
117-
request=Request(...)
117+
request=Request(content="Hello"),
118+
invoke_scope_details=InvokeAgentScopeDetails(...),
119+
agent_details=AgentDetails(...),
118120
) as scope:
119121
# Agent logic here
120122
scope.record_response("result")
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
# Integrating with existing OpenTelemetry
2+
3+
This guide is for developers whose application **already** initializes OpenTelemetry — for example with `azure-monitor-opentelemetry`, an OTLP collector, or a vendor-specific exporter — and who want Agent 365 spans to flow alongside their existing telemetry. If you're starting fresh, see the [observability-core README](../libraries/microsoft-agents-a365-observability-core/README.md) for the standalone setup.
4+
5+
## The integration rule
6+
7+
> **Initialize your existing OpenTelemetry stack first, then call Agent 365's `configure()`.** The SDK detects the existing `TracerProvider` and adds its processors to it. Your existing backend receives every span; the Agent 365 backend also receives spans when `ENABLE_A365_OBSERVABILITY_EXPORTER=true` and a `token_resolver` is provided (otherwise `configure()` falls back to `ConsoleSpanExporter`).
8+
9+
The detection happens in [`config.py`](../libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/config.py): if a real (non-no-op) `TracerProvider` is already set (detected via a non-None `resource` attribute), `configure()` adds an `_EnrichingBatchSpanProcessor` (wrapping the configured exporter) and a custom `SpanProcessor` to that provider rather than creating a new one.
10+
11+
## Two minimal patterns
12+
13+
### Pattern A — `azure-monitor-opentelemetry`
14+
15+
```python
16+
import os
17+
18+
from azure.monitor.opentelemetry import configure_azure_monitor
19+
from microsoft_agents_a365.observability.core import configure
20+
21+
# 1. Existing OTel: Azure Monitor sets up a TracerProvider + AM exporter.
22+
configure_azure_monitor(connection_string=os.environ["APPLICATIONINSIGHTS_CONNECTION_STRING"])
23+
24+
# 2. Agent 365 attaches its processors to that same TracerProvider.
25+
configure(
26+
service_name="my-agent",
27+
service_namespace="my-namespace",
28+
token_resolver=my_token_resolver,
29+
)
30+
```
31+
32+
→ Runnable version: [`observability-with-azure-monitor`](https://github.com/microsoft/Agent365-Samples/tree/main/python/observability-with-azure-monitor) sample.
33+
34+
### Pattern B — manual OTel SDK + OTLP exporter
35+
36+
```python
37+
import os
38+
39+
from opentelemetry import trace
40+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
41+
from opentelemetry.sdk.resources import SERVICE_NAME, Resource
42+
from opentelemetry.sdk.trace import TracerProvider
43+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
44+
45+
from microsoft_agents_a365.observability.core import configure
46+
47+
# 1. Existing OTel: build provider + OTLP exporter explicitly.
48+
provider = TracerProvider(resource=Resource.create({SERVICE_NAME: "my-agent"}))
49+
provider.add_span_processor(
50+
BatchSpanProcessor(OTLPSpanExporter(endpoint=os.environ["OTEL_EXPORTER_OTLP_ENDPOINT"]))
51+
)
52+
trace.set_tracer_provider(provider)
53+
54+
# 2. Agent 365 attaches to that same provider.
55+
configure(
56+
service_name="my-agent",
57+
service_namespace="my-namespace",
58+
token_resolver=my_token_resolver,
59+
)
60+
```
61+
62+
→ Runnable version: [`observability-with-otlp`](https://github.com/microsoft/Agent365-Samples/tree/main/python/observability-with-otlp) sample (defaults to `ConsoleSpanExporter` for zero setup).
63+
64+
## Auto-instrumentation vs. manual instrumentation
65+
66+
The OTel **backend** (where spans go) and the **instrumentation style** (how spans are produced) are independent axes. You can mix them freely.
67+
68+
| | Auto (extension package) | Manual (`InvokeAgentScope` / `InferenceScope` / `ExecuteToolScope`) |
69+
|--------------------------|-------------------------------------------------------------------|--------------------------------------------------------------------|
70+
| **Azure Monitor** | Demonstrated by `observability-with-azure-monitor` sample | Same `configure()`; replace agent code with manual scope wrapping |
71+
| **OTLP / vendor-neutral** | Same `configure()`; install your framework's extension package | Demonstrated by `observability-with-otlp` sample |
72+
73+
For auto-instrumentation, install the framework-specific extension package — for example:
74+
75+
- OpenAI Agents SDK → `microsoft-agents-a365-observability-extensions-openai`
76+
- LangChain → `microsoft-agents-a365-observability-extensions-langchain`
77+
- Semantic Kernel → `microsoft-agents-a365-observability-extensions-semantickernel`
78+
- Microsoft Agent Framework → `microsoft-agents-a365-observability-extensions-agentframework`
79+
80+
For the OpenAI Agents SDK extension, instantiate `OpenAIAgentsTraceInstrumentor()` and call `.instrument()` **after** `configure()`. The instrumentor raises `RuntimeError` if Agent 365 isn't configured first.
81+
82+
## What spans should I expect to see?
83+
84+
The SDK produces three core span kinds. Your backend should show them in this typical hierarchy:
85+
86+
| `gen_ai.operation.name` | Produced by | Typical parent | Span name (default) | Notes |
87+
|-------------------------|---------------------------------------------------|----------------|---------------------|-------|
88+
| `invoke_agent` | `InvokeAgentScope` (one per user turn) | (root or app) | `invoke_agent <agent_name>` when set, else `invoke_agent` | |
89+
| (varies — see notes) | `InferenceScope` (one per LLM call) | `invoke_agent` | `<operation> <model>` | **Manual instrumentation** uses `InferenceOperationType.value` (currently `Chat` / `TextCompletion` / `GenerateContent`, capitalized). **Auto-instrumentation** (e.g. `OpenAIAgentsTraceInstrumentor`) uses lowercase per the [OTel GenAI semconv](https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-spans/) (e.g. `chat`). The two are inconsistent today. |
90+
| `execute_tool` | `ExecuteToolScope` (one per tool invocation) | `invoke_agent` | `execute_tool <tool_name>` (always includes the tool name) | Records tool name, args, and result. |
91+
92+
Filter your backend by the `gen_ai.operation.name` attribute or by span name. Note that `inference` is *not* the literal attribute value — manual instrumentation produces `Chat` / `TextCompletion` / `GenerateContent` (the `InferenceOperationType.value`), while auto-instrumentation extension packages produce the lowercase OTel-spec form (e.g. `chat`). This casing discrepancy is tracked as an SDK issue.
93+
94+
## Verifying the integration
95+
96+
If you've called `configure()` but don't see Agent 365 spans in your backend, isolate the problem by adding a `ConsoleSpanExporter` temporarily:
97+
98+
```python
99+
from opentelemetry import trace
100+
from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter
101+
102+
# After configure() has run:
103+
trace.get_tracer_provider().add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))
104+
```
105+
106+
Run a single turn. If you see `invoke_agent` / `Chat` (or `chat`) / `execute_tool` JSON dumps on stdout, the SDK is producing spans correctly — the issue is in your backend exporter (network, auth, sampling). If you don't see them, the integration itself is wrong; check the pitfalls below.
107+
108+
## Common pitfalls
109+
110+
### Pitfall 1: Calling `configure_azure_monitor()` after Agent 365 `configure()`
111+
112+
**Symptom:** Agent 365 spans don't appear in any backend.
113+
114+
**Cause:** `configure_azure_monitor` (and many vendor packages) replace the global `TracerProvider`. If they run *after* `configure()`, the provider with our processors is discarded.
115+
116+
**Fix:** Always initialize Azure Monitor (or any OTel setup) **before** calling Agent 365 `configure()`.
117+
118+
### Pitfall 2: Calling Agent 365 `configure()` before app's OTel setup
119+
120+
**Symptom:** Same as above — Agent 365 spans are missing.
121+
122+
**Cause:** `configure()` creates its own `TracerProvider` (no existing one detected). Your app's later OTel init replaces it, dropping our processors.
123+
124+
**Fix:** Same as Pitfall 1 — OTel first, then Agent 365.
125+
126+
### Pitfall 3: `OTEL_SDK_DISABLED=true` or `OTEL_TRACES_EXPORTER=none`
127+
128+
**Symptom:** Nothing exports — neither your existing backend nor Agent 365.
129+
130+
**Cause:** These environment variables disable OpenTelemetry SDK-wide. They suppress Agent 365 spans alongside everything else.
131+
132+
**Fix:** Use sampling (`OTEL_TRACES_SAMPLER`) or per-exporter configuration instead of the global disable. If you intentionally want to disable tracing in a particular environment, that's fine — just understand it disables Agent 365 too.
133+
134+
### Pitfall 4: `ENABLE_OBSERVABILITY` not set
135+
136+
**Symptom:** Your existing OTel backend works (Azure Monitor / OTLP / etc. show spans), but Agent 365 scope blocks (`InvokeAgentScope`, `InferenceScope`, `ExecuteToolScope`) produce zero spans. No errors, no warnings.
137+
138+
**Cause:** Agent 365's scope classes gate span creation on the `ENABLE_OBSERVABILITY` (or `ENABLE_A365_OBSERVABILITY`) environment variable. If neither is set to `true` / `1` / `yes` / `on`, every scope's `__init__` skips span creation entirely. This is **independent** of OTel's own enable/disable mechanism — your existing OTel telemetry continues to flow normally.
139+
140+
**Fix:** Set `ENABLE_OBSERVABILITY=true` (or `ENABLE_A365_OBSERVABILITY=true`) in your environment before creating any scopes / emitting spans (the check happens at scope construction time, not import time). Both runnable samples include this in their `.env.template`.
141+
142+
## Exporter combinations
143+
144+
| Combination | What's installed | What to call | Gotchas |
145+
|-----------------------------------------|---------------------------------------------------------------------------|---------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------|
146+
| Azure Monitor only | `azure-monitor-opentelemetry` | `configure_azure_monitor(...)` | Standard Azure Monitor — no Agent 365 spans flow. |
147+
| Azure Monitor + Agent 365 | `azure-monitor-opentelemetry`, `microsoft-agents-a365-observability-core` | `configure_azure_monitor(...)` then `configure(...)` | Order matters (see Pitfall 1). |
148+
| OTLP collector + Agent 365 | `opentelemetry-sdk`, `opentelemetry-exporter-otlp-*`, A365 core | Build provider + `BatchSpanProcessor(OTLPSpanExporter(...))` then `configure(...)` | Set `OTEL_EXPORTER_OTLP_ENDPOINT`; collector must be reachable. |
149+
| Agent 365 only | `microsoft-agents-a365-observability-core` | `configure(...)` only | SDK creates its own `TracerProvider`; spans go to Agent 365 backend only. |
150+
| OTLP + Azure Monitor + Agent 365 | All of the above | Configure Azure Monitor first; add OTLP `BatchSpanProcessor` to the provider; call `configure(...)` | All three exporters receive every span. Watch out for duplicate processors if Azure Monitor itself adds OTLP. |

0 commit comments

Comments
 (0)