Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
738034c
feat(backend-api): add OpenTelemetry span filters and event_utils helper
Shreyas-Microsoft May 6, 2026
6904ade
feat(backend-api): wire Azure Monitor + FastAPIInstrumentor in Applic…
Shreyas-Microsoft May 6, 2026
54f756d
feat(backend-api): emit structured success/error events on routers
Shreyas-Microsoft May 6, 2026
9923768
test(backend-api): cover track_event_if_configured Application Insigh…
Shreyas-Microsoft May 6, 2026
70fe7cb
fix(infra): remove redundant diagnosticSettings on Application Insigh…
Shreyas-Microsoft May 6, 2026
6589e61
fix(backend-api): include error message in CancelProcess/CancelStatus…
Shreyas-Microsoft May 19, 2026
2870a21
chore: Dev merge to Main
Avijit-Microsoft May 21, 2026
1bfdb56
token usage
Priyanka-Microsoft May 25, 2026
a31f8f0
optimize the code
Priyanka-Microsoft May 25, 2026
20d773e
Merge pull request #254 from microsoft/dev
May 27, 2026
f11248b
optimize the code
Priyanka-Microsoft May 27, 2026
5e7e467
chore: Dev merge to Main
Avijit-Microsoft Jun 1, 2026
73ef8a0
refactor: update llm_token_telemetry.py to latest cross-accelerator v…
Priyanka-Microsoft Jun 1, 2026
4393540
fix: update uv.lock to include azure-monitor-events-extension
Priyanka-Microsoft Jun 1, 2026
44f3e43
Merge dev into psl-sw/37816-app-insights-telemetry
Shreyas-Microsoft Jun 2, 2026
868bd5d
Merge pull request #273 from microsoft/dev
Roopan-Microsoft Jun 10, 2026
27261c7
increase the tokens
Prachig-Microsoft Jun 12, 2026
49eb5cd
Upgrade agent-framework to 1.3.0 and azure-ai-projects to 2.1.0
Prachig-Microsoft Jun 11, 2026
e67884a
Fix SharedMemoryContextProvider not iterable TypeError
Prachig-Microsoft Jun 12, 2026
e364793
Remove trailing blank line in azure_openai_response_retry.py (W391)
Prachig-Microsoft Jun 12, 2026
fa63bdf
Fix async for coroutine error in retry client streaming path
Prachig-Microsoft Jun 12, 2026
0f10ef0
Fix streaming: delegate to parent ResponseStream instead of async gen…
Prachig-Microsoft Jun 12, 2026
688b136
Guard against empty messages after context trimming
Prachig-Microsoft Jun 12, 2026
1d86176
Update AZURE_OPENAI_API_VERSION from 2025-03-01-preview to v1
Prachig-Microsoft Jun 12, 2026
6938b2b
Revert "Update AZURE_OPENAI_API_VERSION from 2025-03-01-preview to v1"
Prachig-Microsoft Jun 12, 2026
5961870
Switch default client to AzureOpenAIChatCompletionWithRetry (Chat Com…
Prachig-Microsoft Jun 12, 2026
3ab3076
Fix AzureOpenAIChatClientWithRetry streaming: sync def + stream param…
Prachig-Microsoft Jun 12, 2026
2caced5
Sanitize message author_name for OpenAI Chat Completions
Prachig-Microsoft Jun 12, 2026
7f8a04b
Demote empty-messages warning to debug
Prachig-Microsoft Jun 12, 2026
6d371fd
Fix Coordinator anti-loop: accept framework's response schema
Prachig-Microsoft Jun 12, 2026
d5da788
Sanitize message name at OpenAI wire format (defense in depth)
Prachig-Microsoft Jun 12, 2026
0da531f
Fix loop detection: don't count looped-on agent's runs as progress
Prachig-Microsoft Jun 12, 2026
9a85f00
Add agent_description for Analysis participants so Coordinator can ro…
Prachig-Microsoft Jun 12, 2026
76e9c17
Fix loop detection: key on agent name only, not (agent, instruction)
Prachig-Microsoft Jun 12, 2026
4de1e28
fix(workflow): reject incoherent ResultGenerator output across all steps
Prachig-Microsoft Jun 12, 2026
f74629b
fix(groupchat): resolve agent identity via author_name for af 1.3.0
Prachig-Microsoft Jun 13, 2026
42902ab
fix(groupchat): route WorkflowEvent payloads + enforce framework max_…
Prachig-Microsoft Jun 13, 2026
7a0212f
fix(groupchat): detect participant loops via executor_completed events
Prachig-Microsoft Jun 13, 2026
03387c4
Revert "fix(groupchat): detect participant loops via executor_complet…
Prachig-Microsoft Jun 13, 2026
14d52f2
chore(logging): suppress harmless empty-message-cache warning and dro…
Prachig-Microsoft Jun 13, 2026
25aeea5
ci: replace deprecated --exclude-mail with --exclude ^mailto: in lych…
Prachig-Microsoft Jun 16, 2026
db7c837
fix: reconstruct conversation from agent_responses on early termination
Prachig-Microsoft Jun 16, 2026
d0397f7
fix: use correct Message attributes in _validate_sign_offs (text/auth…
Prachig-Microsoft Jun 17, 2026
b9979a7
fix: add loop detection for same-agent back-to-back invocations in 1.3.0
Prachig-Microsoft Jun 17, 2026
4a8ee72
fix: address Copilot review - empty name handling and type annotation
Prachig-Microsoft Jun 17, 2026
488a290
fix: use selection_func to keep Coordinator visible in streaming loop
Prachig-Microsoft Jun 17, 2026
325bf39
diag: add Coordinator response logging for loop detection debugging
Prachig-Microsoft Jun 17, 2026
c5c3a34
fix: set ManagerSelectionResponse extra=forbid for Chat API structure…
Prachig-Microsoft Jun 17, 2026
89ec3fc
fix: prevent infinite loop when sign-off validation rejects finish
Prachig-Microsoft Jun 17, 2026
411c64b
fix: relax sign-off validation to only block on explicit PENDING/FAIL
Prachig-Microsoft Jun 17, 2026
3e1685e
fix: make sign-off validation log-only to prevent termination deadlock
Prachig-Microsoft Jun 17, 2026
a6d696f
Merge branch 'psl-sw/37816-app-insights-telemetry' into psl-pri/confl…
llPriyanka Jun 18, 2026
727bbb7
created new uv lock file
llPriyanka Jun 18, 2026
09abf26
Merge pull request #282 from microsoft/psl-upgrade-agent-framework-final
Roopan-Microsoft Jun 18, 2026
9ecb205
Merge pull request #294 from llPriyanka/psl-pri/conflict-resolve-toke…
Roopan-Microsoft Jun 18, 2026
21014d1
Merge branch 'dev' into app-insight-conflict-resolve
llPriyanka Jun 19, 2026
ce729c1
conflict resolve
llPriyanka Jun 19, 2026
38825e8
Merge branch 'dev' into app-insight-conflict-resolve
llPriyanka Jun 19, 2026
755bf8e
Enhance token usage tracking and emission in Azure OpenAI clients and…
Ayaz-Microsoft Jun 19, 2026
cddc99a
updated missing u
llPriyanka Jun 19, 2026
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
4 changes: 2 additions & 2 deletions .github/workflows/broken-links-checker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jobs:
uses: lycheeverse/lychee-action@v2.8.0
with:
args: >
--verbose --exclude-mail --no-progress --exclude ^https?://
--verbose --no-progress --exclude ^https?:// --exclude ^mailto:
${{ steps.changed-markdown-files.outputs.all_changed_files }}
failIfEmpty: false
env:
Expand All @@ -50,7 +50,7 @@ jobs:
uses: lycheeverse/lychee-action@v2.8.0
with:
args: >
--verbose --exclude-mail --no-progress --exclude ^https?://
--verbose --no-progress --exclude ^https?:// --exclude ^mailto:
'**/*.md'
failIfEmpty: false
env:
Expand Down
2 changes: 1 addition & 1 deletion docs/LocalDevelopmentSetup.md
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ py -3.12 -m uv venv .venv
py -3.12 -m uv sync --prerelease=allow
```

> **⚠️ Important**: This repo currently depends on a prerelease/dev version of Microsoft Agent Framework. Always run `uv sync --prerelease=allow` (or `py -3.12 -m uv sync --prerelease=allow` on Windows) after creating the virtual environment to install all required dependencies. Missing dependencies will cause runtime errors like `ModuleNotFoundError: No module named 'pydantic'` or DNS resolution failures.
> **⚠️ Important**: Always run `uv sync --prerelease=allow` (or `py -3.12 -m uv sync --prerelease=allow` on Windows) after creating the virtual environment to install all required dependencies. The `--prerelease=allow` flag is needed because some transitive dependencies may still use pre-release versions. Missing dependencies will cause runtime errors like `ModuleNotFoundError: No module named 'pydantic'` or DNS resolution failures.

### 5.4. Run the Processor

Expand Down
20 changes: 9 additions & 11 deletions docs/ProcessFrameworkGuide.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,23 +118,21 @@ Inside each step, the orchestrator can use multi-agent patterns (maker-checker l

- Implementation: [src/processor/src/steps/migration_processor.py](../src/processor/src/steps/migration_processor.py)
- The processor creates a workflow with `WorkflowBuilder`.
- It registers four executors, sets the start executor, and defines edges.
- It instantiates four executors, passes the start executor to `WorkflowBuilder`, and chains them with `add_chain`.

Example from the repo (simplified):

```python
from agent_framework import WorkflowBuilder

analysis_exec = AnalysisExecutor(id="analysis", app_context=app_context)
design_exec = DesignExecutor(id="design", app_context=app_context)
yaml_exec = YamlConvertExecutor(id="yaml", app_context=app_context)
docs_exec = DocumentationExecutor(id="documentation", app_context=app_context)

workflow = (
WorkflowBuilder()
.register_executor(lambda: AnalysisExecutor(id="analysis", app_context=app_context), name="analysis")
.register_executor(lambda: DesignExecutor(id="design", app_context=app_context), name="design")
.register_executor(lambda: YamlConvertExecutor(id="yaml", app_context=app_context), name="yaml")
.register_executor(lambda: DocumentationExecutor(id="documentation", app_context=app_context), name="documentation")
.set_start_executor("analysis")
.add_edge("analysis", "design")
.add_edge("design", "yaml")
.add_edge("yaml", "documentation")
WorkflowBuilder(start_executor=analysis_exec)
.add_chain([analysis_exec, design_exec, yaml_exec, docs_exec])
.build()
)
```
Expand Down Expand Up @@ -355,7 +353,7 @@ To run processor unit tests locally (example):

```bash
cd src/processor
uv run --prerelease=allow python -m pytest src/processor/src/tests/unit -v
uv run python -m pytest src/tests/unit -v
```

## Extending the pipeline
Expand Down
6 changes: 5 additions & 1 deletion infra/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -301,8 +301,12 @@ module applicationInsights 'br/public:avm/res/insights/component:0.6.0' = if (en
disableIpMasking: false
flowType: 'Bluefield'
// WAF aligned configuration for Monitoring
// The AVM `insights/component` module wires Application Insights to
// the Log Analytics workspace via `workspaceResourceId` (workspace-
// based App Insights). A separate `diagnosticSettings` entry on the
// SAME workspace causes duplicate ingestion of platform logs.
// Source: AB#37816 — see CKM #811 reference implementation.
workspaceResourceId: enableMonitoring ? logAnalyticsWorkspaceResourceId : ''
diagnosticSettings: enableMonitoring ? [{ workspaceResourceId: logAnalyticsWorkspaceResourceId }] : null
}
}

Expand Down
6 changes: 5 additions & 1 deletion infra/main_custom.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -279,8 +279,12 @@ module applicationInsights 'br/public:avm/res/insights/component:0.6.0' = if (en
disableIpMasking: false
flowType: 'Bluefield'
// WAF aligned configuration for Monitoring
// The AVM `insights/component` module wires Application Insights to
// the Log Analytics workspace via `workspaceResourceId` (workspace-
// based App Insights). A separate `diagnosticSettings` entry on the
// SAME workspace causes duplicate ingestion of platform logs.
// Source: AB#37816 — see CKM #811 reference implementation.
workspaceResourceId: enableMonitoring ? logAnalyticsWorkspaceResourceId : ''
diagnosticSettings: enableMonitoring ? [{ workspaceResourceId: logAnalyticsWorkspaceResourceId }] : null
}
}

Expand Down
19 changes: 13 additions & 6 deletions infra/vscode_web/codeSample.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from azure.ai.agents.models import ListSortOrder
from azure.ai.projects import AIProjectClient
from azure.identity import DefaultAzureCredential

Expand All @@ -7,19 +8,25 @@

agent = project_client.agents.get_agent("<%= agentId %>")

thread = project_client.agents.create_thread()
thread = project_client.agents.threads.create()
print(f"Created thread, ID: {thread.id}")

message = project_client.agents.create_message(
message = project_client.agents.messages.create(
thread_id=thread.id,
role="user",
content="<%= userMessage %>"
)

run = project_client.agents.create_and_process_run(
run = project_client.agents.runs.create_and_process(
thread_id=thread.id,
agent_id=agent.id)
messages = project_client.agents.list_messages(thread_id=thread.id)

for text_message in messages.text_messages:
print(text_message.as_dict())
if run.status == "failed":
print(f"Run failed: {run.last_error}")
else:
messages = project_client.agents.messages.list(
thread_id=thread.id, order=ListSortOrder.ASCENDING)

for message in messages:
if message.text_messages:
print(f"{message.role}: {message.text_messages[-1].text.value}")
2 changes: 1 addition & 1 deletion infra/vscode_web/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
azure-ai-projects==1.0.0b12
azure-ai-projects==2.1.0
azure-identity==1.20.0
ansible-core~=2.17.0
2 changes: 2 additions & 0 deletions src/backend-api/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ dependencies = [
"azure-ai-agents==1.2.0b6",
"azure-appconfiguration==1.8.0",
"azure-identity==1.25.3",
"azure-monitor-events-extension==0.1.0",
"azure-monitor-opentelemetry==1.8.7",
"azure-search-documents==11.7.0b2",
"azure-storage-blob==12.28.0",
"azure-storage-queue==12.15.0",
"fastapi[standard]==0.135.3",
"httpx==0.28.1",
"opentelemetry-instrumentation-fastapi==0.61b0",
"pydantic-settings==2.13.1",
"python-dotenv==1.2.2",
"python-multipart==0.0.27",
Expand Down
14 changes: 14 additions & 0 deletions src/backend-api/src/app/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,17 @@ APP_CONFIGURATION_URL=""
# AZURE_PACKAGE_LOGGING_LEVEL="WARNING" # Options: DEBUG, INFO, WARNING, ERROR, CRITICAL
# AZURE_LOGGING_PACKAGES="azure.core.pipeline.policies.http_logging_policy,azure.storage.blob,azure.storage.queue,azure.core,azure.identity,azure.storage,azure.core.pipeline,azure.core.pipeline.policies,azure.core.pipeline.transport,openai,openai._client,httpx,httpcore,semantic_kernel,urllib3,msal"

# ------------------------------------------------------------------
# Application Insights / OpenTelemetry
# ------------------------------------------------------------------
# When deployed via the bundled Bicep, this value is injected by
# `infra/main.bicep` -> `containerAppBackend` from the
# `applicationInsights.outputs.connectionString` output. Leave unset for
# local dev to skip telemetry export entirely (the app will log a single
# warning at startup and otherwise behave normally).
# APPLICATIONINSIGHTS_CONNECTION_STRING=""

# Optional: clamp basic logging level for App Insights ingestion.
# Defaults inherit from APP_LOGGING_LEVEL above.
# AZURE_BASIC_LOGGING_LEVEL="INFO"

96 changes: 96 additions & 0 deletions src/backend-api/src/app/application.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import logging
import os
from datetime import datetime

from fastapi.middleware.cors import CORSMiddleware
from libs.base.application_base import Application_Base
from libs.base.typed_fastapi import TypedFastAPI
from libs.logging.span_filters import (
DropASGIResponseBodySpanProcessor,
DropCosmosDependencySpanProcessor,
)
from libs.repositories.file_repository import FileRepository
from libs.repositories.process_repository import ProcessRepository
from libs.repositories.process_status_repository import ProcessStatusRepository
Expand All @@ -20,6 +25,14 @@
from routers import router_debug, router_files, router_process
from routers.http_probes import router as http_probes

logger = logging.getLogger(__name__)

# URLs (relative paths) that should NOT generate request telemetry.
# Matches the routes registered in `routers/http_probes.py`.
# `excluded_urls` is a comma-separated substring list per the
# OpenTelemetry FastAPI instrumentation contract.
_OTEL_EXCLUDED_URLS = "health,startup"


class Application(Application_Base):
"""
Expand Down Expand Up @@ -57,11 +70,94 @@ def initialize(self):
allow_headers=["*"],
)

# Wire up Azure Monitor / OpenTelemetry BEFORE the routers are
# included so that the FastAPI instrumentor can patch the app
# while the route table is still empty. `configure_azure_monitor`
# is a no-op (warns once) when the connection string env var is
# absent — see `libs/logging/event_utils.py`.
self._configure_azure_monitor()

self.app.include_router(http_probes)
self._register_dependencies()
self._config_routers()
# self._initialize_database()

# Instrumenting AFTER routers are registered means every route is
# automatically wrapped by the OTEL middleware.
self._instrument_fastapi()

def _configure_azure_monitor(self):
"""Initialise Azure Monitor OpenTelemetry exporter, if configured.

This is the App Insights "linkage" step required by AC #1 / AC #2:
if `APPLICATIONINSIGHTS_CONNECTION_STRING` is set in the
container app environment (wired by Bicep — see
`infra/main.bicep`), we hand it to `configure_azure_monitor`
along with our two noise-suppressing span processors.

Live Metrics is enabled so the team can watch the Maintenance
environment in real time during demo validation.
"""
connection_string = os.environ.get(
"APPLICATIONINSIGHTS_CONNECTION_STRING", ""
).strip()
if not connection_string:
logger.info(
"APPLICATIONINSIGHTS_CONNECTION_STRING not set; "
"skipping Azure Monitor OpenTelemetry configuration."
)
return

try:
from azure.monitor.opentelemetry import configure_azure_monitor

configure_azure_monitor(
connection_string=connection_string,
enable_live_metrics=True,
span_processors=[
DropASGIResponseBodySpanProcessor(),
DropCosmosDependencySpanProcessor(),
],
)
# Do NOT log the connection string itself — it contains the
# ingestion key. Logging only the fact of configuration.
logger.info(
"Azure Monitor OpenTelemetry configured (live metrics enabled)."
)
except Exception: # noqa: BLE001 — telemetry must never break startup
logger.exception(
"Failed to configure Azure Monitor OpenTelemetry; "
"continuing without App Insights export."
)

def _instrument_fastapi(self):
"""Apply the OpenTelemetry FastAPI instrumentation.

Excludes the liveness / startup probe routes (registered in
`routers/http_probes.py`) so probe traffic does not flood
Application Insights with no-information request rows.
"""
if not os.environ.get(
"APPLICATIONINSIGHTS_CONNECTION_STRING", ""
).strip():
# No exporter wired up; instrumenting is a wasted import.
return
try:
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor

FastAPIInstrumentor.instrument_app(
self.app, excluded_urls=_OTEL_EXCLUDED_URLS
)
logger.info(
"FastAPIInstrumentor attached (excluded_urls=%s).",
_OTEL_EXCLUDED_URLS,
)
except Exception: # noqa: BLE001
logger.exception(
"Failed to attach FastAPIInstrumentor; "
"continuing without per-request telemetry."
)

def _config_routers(self):
"""
Configure routers for the FastAPI application.
Expand Down
24 changes: 24 additions & 0 deletions src/backend-api/src/app/libs/base/application_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,19 @@
from libs.application.application_context import AppContext
from libs.azure.app_configuration import AppConfigurationHelper

# Logger packages that emit at INFO/DEBUG levels often enough to drown
# out signal in Application Insights logs when the OTEL log handler is
# attached. We force them to WARNING regardless of caller config.
# This is a backstop in addition to (not a replacement for) the
# `AZURE_LOGGING_PACKAGES` env-var driven filtering — anything listed
# here is ALWAYS clamped to WARNING.
_NOISY_LOGGER_PACKAGES = (
"azure.core.pipeline.policies.http_logging_policy",
"azure.cosmos",
"opentelemetry.sdk",
"azure.monitor.opentelemetry.exporter.export._base",
)


class Application_Base(ABC):
application_context: AppContext = None
Expand Down Expand Up @@ -78,6 +91,17 @@ def __init__(self, env_file_path: str | None = None, **data):
):
logging.getLogger(logger_name).setLevel(azure_level)

# Hard-suppress known noisy packages regardless of operator
# config. Without this, the App Insights logs view is
# dominated by per-request HTTP policy logs and per-call
# Cosmos diagnostics — see AC #3 / AC #4 of AB#37816.
# We never lower a logger that the operator has explicitly
# raised below WARNING.
for noisy_pkg in _NOISY_LOGGER_PACKAGES:
noisy_logger = logging.getLogger(noisy_pkg)
if noisy_logger.level == logging.NOTSET or noisy_logger.level < logging.WARNING:
noisy_logger.setLevel(logging.WARNING)

# Initialize the application
self.initialize()

Expand Down
23 changes: 23 additions & 0 deletions src/backend-api/src/app/libs/logging/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
"""Application-level logging and telemetry helpers.

This subpackage hosts the small Application Insights / OpenTelemetry
integration helpers used by the backend-api application:

- ``event_utils`` — a tiny wrapper around
``azure.monitor.events.extension.track_event`` that no-ops when the
``APPLICATIONINSIGHTS_CONNECTION_STRING`` environment variable is not
configured. Callers can therefore emit structured events from
any router/service without conditionally guarding each call site.
- ``span_filters`` — custom OpenTelemetry ``SpanProcessor`` implementations
that drop noisy spans before they are exported to Application Insights
(per-chunk ASGI ``http.response.body`` spans and Cosmos DB dependency
spans). These keep the App Insights ingestion cost and the
end-to-end transaction view clean for the Container Migration workflow.

Nothing in this subpackage imports Azure SDKs at module-import time, so
it is safe to import from contexts where the App Insights SDK may not be
fully wired up yet (e.g. application bootstrap before
``configure_azure_monitor`` has run).
"""
Loading
Loading