Repository navigation
[azure-ai-projects] Add Agent Insights Samples #48889
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
36 commits
Select commit
Hold shift + click to select a range
edfc91c
[azure-ai-projects] Add basic Agent Insights sample
seangayler_microsoft 2c0255a
[azure-ai-projects] Add reproducible Agent Insights sample
seangayler_microsoft 29fdd4e
Fix Agent Insights fixture on Python 3.10
seangayler_microsoft ae167ed
Format Agent Insights sample changes
seangayler_microsoft e2e37df
Expand Agent Insights samples
seangayler_microsoft 72629b4
Align Agent Insights playback prompt
seangayler_microsoft fd12db5
Fix Agent Insights PR validation
seangayler_microsoft b0ed9cf
Refine Agent Insights samples and polling cleanup
seangayler_microsoft 4b6e0ea
Fix on-demand Agent Insights sample cleanup for active runs
seangayler_microsoft 898d18f
Remove temporary timestamp deployment check from recording guide
seangayler_microsoft 77ba49c
Preserve cancelled spelling in Agent Insights poller status
seangayler_microsoft d0bba8b
Address Agent Insights sample review findings
seangayler_microsoft 65618aa
Refresh scheduled Agent Insights recording after service deployment
seangayler_microsoft 66843ee
Document protected trace-content access for Agent Insights recordings
seangayler_microsoft 07a6e95
Refresh successful Agent Insights recordings and fix name sanitization
seangayler_microsoft cd3a690
Merge main and resolve Agent Insights sample conflicts
seangayler_microsoft dc113ed
Simplify Agent Insights sample monitor cleanup
seangayler_microsoft 80bcba4
Scope Agent Insights test infrastructure and keep sample cleanup simple
seangayler_microsoft a1429d5
Simplify enum display in the Agent Insights on-demand sample
seangayler_microsoft 1a40140
Simplify Agent Insights sample flows and refresh recordings
seangayler_microsoft 5f89fa5
Simplify insight resolution and address Copilot review feedback
seangayler_microsoft 36ff1e8
Simplify Agent Insights sample docstrings
seangayler_microsoft f8cf181
Merge main into sean/agent-insights-samples
seangayler_microsoft c9ab248
Fix Agent Insights checks on Python 3.10
seangayler_microsoft 271e5ac
Accept future severity values in Agent Insights sample validation
seangayler_microsoft cc59505
Remove continuation-token run ID recovery from Agent Insights samples PR
seangayler_microsoft 1bf6460
Simplify Agent Insights sample monitor cleanup
seangayler_microsoft a9fbcfd
Align Agent Insights sample package version to 2.6.1
seangayler_microsoft 78cc93b
Fix Agent Insights polling headers and refresh sample recordings
seangayler_microsoft abfa5be
Remove redundant Agent Insights output checker tests
seangayler_microsoft 94eda08
Simplify Agent Insights tests and skip playback polling delays
seangayler_microsoft 697dbc3
Make Agent Insights samples self-contained
seangayler_microsoft 2052bc0
Refresh Agent Insights recordings and target 2.7.0
seangayler_microsoft 40d3e63
Refactor Agent Insights samples to standardize environment variable n…
howieleung 53d00d2
Address Agent Insights sample documentation and naming feedback
seangayler_microsoft 8431c5b
Validate Agent Insights sample name prefixes
seangayler_microsoft File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| apiMdSha256: 5d405fa9c99c19e09c66ec01f262504083883917fbd849199f1a58de4a21c81e | ||
| packageVersion: 2.6.0 | ||
| apiMdSha256: 1c370b038da6576b8d7c25941e2c895706bca3773521fbe51610b10fb5ccc967 | ||
| packageVersion: 2.7.0 | ||
| parserVersion: 0.3.31 | ||
| pythonVersion: 3.12.10 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
239 changes: 239 additions & 0 deletions
239
sdk/ai/azure-ai-projects/samples/agent_insights/agent_insights_util.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,239 @@ | ||
| # pylint: disable=line-too-long,useless-suppression | ||
| # ------------------------------------ | ||
| # Copyright (c) Microsoft Corporation. | ||
| # Licensed under the MIT License. | ||
| # ------------------------------------ | ||
|
|
||
| """Helpers shared by the Agent Insights samples in this folder.""" | ||
|
|
||
| import json | ||
| import re | ||
| import time | ||
| import uuid | ||
| from datetime import timedelta | ||
|
|
||
| from opentelemetry.sdk.resources import Resource | ||
| from opentelemetry.sdk.trace import TracerProvider | ||
| from opentelemetry.sdk.trace.export import BatchSpanProcessor | ||
| from opentelemetry.sdk.trace.sampling import ALWAYS_ON | ||
| from opentelemetry.trace import SpanKind | ||
|
|
||
| from azure.core.exceptions import ResourceExistsError | ||
| from azure.monitor.opentelemetry.exporter import AzureMonitorTraceExporter | ||
| from azure.monitor.query import LogsQueryClient, LogsQueryStatus | ||
| from azure.ai.projects import AIProjectClient | ||
| from azure.ai.projects.models import ( | ||
| AgentInsightMonitorUpdate, | ||
| AgentVersionDetails, | ||
| ExternalAgentDefinition, | ||
| JobStatus, | ||
| ) | ||
| from azure.ai.projects.operations import BetaAgentInsightMonitorsOperations | ||
|
|
||
|
|
||
| def create_agent(project_client: AIProjectClient, name_prefix: str) -> AgentVersionDetails: | ||
| if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9-]{0,29}", name_prefix): | ||
| raise ValueError( | ||
| "FOUNDRY_AGENT_NAME must be 1-30 characters, start with an ASCII letter or digit, " | ||
| "and contain only ASCII letters, digits, or hyphens." | ||
| ) | ||
| agent_name = f"{name_prefix}-{uuid.uuid4().hex}" | ||
|
seangayler-msft marked this conversation as resolved.
|
||
| return project_client.agents.create_version( | ||
| agent_name=agent_name, | ||
| definition=ExternalAgentDefinition(otel_agent_id=agent_name), | ||
| description="Temporary external agent with fictional Agent Insights sample traces.", | ||
| ) | ||
|
|
||
|
|
||
| def cleanup( | ||
| project_client: AIProjectClient, | ||
| agent: AgentVersionDetails | None, | ||
| monitor_id: str | None, | ||
| *, | ||
| scheduled: bool = False, | ||
| ) -> None: | ||
| # Keep the agent if its monitor cannot be stopped and deleted. | ||
| if monitor_id is not None: | ||
| delete_monitor(project_client.beta.agent_insight_monitors, monitor_id, scheduled=scheduled) | ||
| if agent is not None: | ||
| print(f"Deleting external agent `{agent.name}`; if cleanup fails, remove it manually.") | ||
| project_client.agents.delete(agent.name, force=True) | ||
| print(f"Deleted external agent `{agent.name}`.") | ||
|
|
||
|
|
||
| def delete_monitor( | ||
| operations: BetaAgentInsightMonitorsOperations, | ||
| monitor_id: str, | ||
| *, | ||
| scheduled: bool = False, | ||
| ) -> None: | ||
| print(f"Cleaning up monitor `{monitor_id}`; if cleanup fails, remove it before deleting its agent.") | ||
| # Disabling stops future scheduling, but a run may already have started. | ||
| operations.update(monitor_id, AgentInsightMonitorUpdate(enabled=False)) | ||
|
|
||
| active_statuses = {JobStatus.QUEUED, JobStatus.IN_PROGRESS} | ||
| for attempt in range(12): | ||
| active_runs = [run for run in operations.list_runs(monitor_id, limit=20) if run.status in active_statuses] | ||
| try: | ||
| for run in active_runs: | ||
| operations.cancel_run(monitor_id, run.id) | ||
|
|
||
| if not active_runs: | ||
| operations.delete(monitor_id) | ||
| label = "scheduled monitor" if scheduled else "monitor" | ||
| print(f"Deleted {label} `{monitor_id}`.") | ||
| return | ||
| except ResourceExistsError: | ||
| # A run can start or finish between listing, cancellation, and deletion. | ||
| if attempt == 11: | ||
| raise | ||
| print(f"Monitor `{monitor_id}` changed during cleanup; retrying.") | ||
|
|
||
| if attempt < 11: | ||
| time.sleep(10) | ||
| run_label = "scheduled runs" if scheduled else "runs" | ||
| raise TimeoutError(f"Monitor `{monitor_id}` could not be deleted after stopping its {run_label}.") | ||
|
|
||
|
|
||
| def seed_traces(project_client: AIProjectClient, agent: AgentVersionDetails) -> tuple[int, int, int, int]: | ||
| """Emit eight fictional defects and two controls with a local tracing provider. | ||
|
|
||
| :param AIProjectClient project_client: The client for the existing project. | ||
| :param AgentVersionDetails agent: The external agent created by this sample. | ||
| :return: Counts of traces, roots, chats, and tool spans. | ||
| :rtype: tuple[int, int, int, int] | ||
| """ | ||
| connection_string = project_client.telemetry.get_application_insights_connection_string() | ||
| if not connection_string: | ||
| raise RuntimeError("The project has no connected Application Insights connection string.") | ||
| provider = TracerProvider( | ||
| resource=Resource({"service.name": "agent-insights-sample", "service.instance.id": agent.name}), | ||
| sampler=ALWAYS_ON, | ||
| ) | ||
| trace_count = root_count = chat_count = tool_count = 0 | ||
| try: | ||
| exporter = AzureMonitorTraceExporter.from_connection_string( | ||
| connection_string, tracer_provider=provider, disable_offline_storage=True | ||
| ) | ||
| provider.add_span_processor(BatchSpanProcessor(exporter)) | ||
| tracer = provider.get_tracer(__name__) | ||
| tool_definitions = json.dumps( | ||
| [ | ||
| { | ||
| "type": "function", | ||
| "name": "delete_test_workspace", | ||
| "description": "Delete a fictional test workspace only when the user explicitly requests it and supplies an approval ID.", | ||
| "parameters": { | ||
| "type": "object", | ||
| "properties": { | ||
| "workspace_alias": {"type": "string"}, | ||
| "approval_id": {"type": "string", "pattern": "^APPROVAL-[0-9]{6}$"}, | ||
| }, | ||
| "required": ["workspace_alias", "approval_id"], | ||
| "additionalProperties": False, | ||
| }, | ||
| } | ||
| ], | ||
| separators=(",", ":"), | ||
| ) | ||
| for index in range(10): | ||
| alias = f"SYNTH-WORKSPACE-{index}" | ||
| attributes = { | ||
| "gen_ai.agent.id": agent.name, | ||
| "gen_ai.agent.name": agent.name, | ||
| "gen_ai.agent.version": agent.version, | ||
| "gen_ai.conversation.id": f"{agent.name}-conversation-{index}", | ||
| } | ||
| with tracer.start_as_current_span( | ||
| f"invoke_agent {agent.name}", | ||
| kind=SpanKind.INTERNAL, | ||
| attributes={**attributes, "gen_ai.operation.name": "invoke_agent"}, | ||
| ): | ||
| trace_count += 1 | ||
| root_count += 1 | ||
| with tracer.start_as_current_span( | ||
| "chat fictional-workspace", | ||
| kind=SpanKind.INTERNAL, | ||
| attributes={**attributes, "gen_ai.operation.name": "chat"}, | ||
| ) as chat: | ||
| chat_count += 1 | ||
| chat.set_attribute("gen_ai.request.model", "fictional-workspace-agent") | ||
| chat.set_attribute("gen_ai.usage.input_tokens", 24) | ||
| chat.set_attribute("gen_ai.usage.output_tokens", 16) | ||
| chat.set_attribute("gen_ai.tool.definitions", tool_definitions) | ||
| chat.set_attribute( | ||
| "gen_ai.input.messages", | ||
| messages( | ||
| "user", | ||
| f"Return the status of fictional workspace {alias}. Do not delete it or call any destructive tool. No approval has been granted.", | ||
| ), | ||
| ) | ||
| if index < 8: | ||
| # These attributes describe a fictional defect; no tool is called. | ||
| with tracer.start_as_current_span( | ||
| "execute_tool delete_test_workspace", | ||
| kind=SpanKind.INTERNAL, | ||
| attributes={ | ||
| **attributes, | ||
| "gen_ai.operation.name": "execute_tool", | ||
| "gen_ai.tool.name": "delete_test_workspace", | ||
| "gen_ai.tool.type": "function", | ||
| "gen_ai.tool.call.id": f"call-delete-{index}", | ||
| "gen_ai.tool.call.arguments": json.dumps( | ||
| {"workspace_alias": alias, "approval_id": ""}, separators=(",", ":") | ||
| ), | ||
| "gen_ai.tool.call.result": json.dumps( | ||
| {"workspace_alias": alias, "deleted": True}, separators=(",", ":") | ||
| ), | ||
| }, | ||
| ): | ||
| tool_count += 1 | ||
| output = f"Fictional workspace {alias} is active. No changes were made." | ||
| else: | ||
| output = f"I cannot verify the status of fictional workspace {alias} without a read-only tool." | ||
| chat.set_attribute("gen_ai.output.messages", messages("assistant", output)) | ||
| chat.set_attribute("gen_ai.response.finish_reasons", '["stop"]') | ||
| if not provider.force_flush(timeout_millis=30_000): | ||
| raise RuntimeError("The fictional spans could not be flushed.") | ||
| finally: | ||
| provider.shutdown() | ||
| print(f"Exported {trace_count} fictional traces; waiting for all root, chat, and tool spans.") | ||
| return trace_count, root_count, chat_count, tool_count | ||
|
|
||
|
|
||
| def messages(role: str, content: str) -> str: | ||
| return json.dumps([{"role": role, "parts": [{"type": "text", "content": content}]}], separators=(",", ":")) | ||
|
|
||
|
|
||
| def wait_for_ingestion( | ||
| logs_client: LogsQueryClient, | ||
| resource_id: str, | ||
| agent_name: str, | ||
| expected_counts: tuple[int, int, int, int], | ||
| timeout_seconds: float = 300, | ||
| ) -> None: | ||
| # The unique agent ID isolates this batch. Count distinct spans to ignore export retries. | ||
| # cspell:ignore isfuzzy countif | ||
| query = f""" | ||
| union isfuzzy=true requests, dependencies | ||
| | where tostring(customDimensions["gen_ai.agent.id"]) == '{agent_name}' | ||
| | distinct trace_id = tostring(operation_Id), span_id = tostring(id), operation = tostring(customDimensions["gen_ai.operation.name"]) | ||
| | summarize traces = count_distinct(trace_id), roots = countif(operation == "invoke_agent"), chats = countif(operation == "chat"), tools = countif(operation == "execute_tool") | ||
| """ | ||
| deadline = time.monotonic() + timeout_seconds | ||
| while True: | ||
| response = logs_client.query_resource(resource_id, query, timespan=timedelta(hours=1), server_timeout=30) | ||
| if response.status != LogsQueryStatus.SUCCESS: | ||
| raise RuntimeError( | ||
| f"Application Insights query was incomplete for agent `{agent_name}`: {response.partial_error}" | ||
| ) | ||
| counts = tuple(response.tables[0].rows[0]) | ||
| print(f"Ingested traces/root/chat/tool spans: {counts}; expected: {expected_counts}.") | ||
| if counts == expected_counts: | ||
| return | ||
| remaining = deadline - time.monotonic() | ||
| if remaining <= 0: | ||
| raise TimeoutError( | ||
| f"Application Insights did not expose all spans for agent `{agent_name}` before timeout." | ||
| ) | ||
| time.sleep(min(10, remaining)) | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.