Skip to content
Open
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
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
/cli/azd/extensions/azure.ai.connections/ @JeffreyCA @glharper @trangevi @trrwilson @therealjohn @huimiu @hund030 @m5i-work @v1212
/cli/azd/extensions/azure.ai.finetune/ @JeffreyCA @trangevi @achauhan-scc @kingernupur @saanikaguptamicrosoft
/cli/azd/extensions/azure.ai.inspector/ @JeffreyCA @glharper @trangevi @trrwilson @therealjohn @anchenyi @XiaofuHuang
/cli/azd/extensions/azure.ai.loom/ @hnamburi @savitam
/cli/azd/extensions/azure.ai.models/ @JeffreyCA @trangevi @achauhan-scc @kingernupur @saanikaguptamicrosoft
/cli/azd/extensions/azure.ai.projects/ @JeffreyCA @glharper @trangevi @trrwilson @therealjohn @huimiu @hund030 @m5i-work @v1212
/cli/azd/extensions/azure.ai.rle/ @JeffreyCA @glharper @trangevi @trrwilson @therealjohn @huimiu @hund030 @m5i-work @v1212
Expand Down
22 changes: 22 additions & 0 deletions .github/workflows/lint-ext-azure-ai-loom.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: ext-azure-ai-loom-ci

on:
pull_request:
paths:
- "cli/azd/extensions/azure.ai.loom/**"
- ".github/workflows/lint-ext-azure-ai-loom.yml"
branches: [main]

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true

permissions:
contents: read
pull-requests: write

jobs:
lint:
uses: ./.github/workflows/lint-go.yml
with:
working-directory: cli/azd/extensions/azure.ai.loom
6 changes: 4 additions & 2 deletions cli/azd/docs/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,12 +196,14 @@ Metadata requests are unauthenticated when no matching token is set.
> **Note**: These variables are defined and consumed by individual azd extensions. As the extension
> ecosystem grows, extension-specific variables may move to each extension's own documentation.

### azure.ai.agents
### Microsoft Foundry extensions

| Variable | Description |
| --- | --- |
| `AZURE_AI_PROJECT_ID` | The Microsoft Foundry project resource ID used by the `azure.ai.agents` extension. |
| `FOUNDRY_PROJECT_ENDPOINT` | The Microsoft Foundry project endpoint used by the `azure.ai.agents` extension. Read first from the active azd environment and, if not present, from the host shell environment as an endpoint-resolution fallback. |
| `AZURE_AI_PROJECT_API_KEY` | A Microsoft Foundry account API key accepted by the project data plane and used by `azure.ai.loom` experiment-tracking commands. When set in the host process, it takes precedence over bearer authentication. Do not persist this value in project files or source control. |
| `FOUNDRY_PROJECT_ENDPOINT` | The Microsoft Foundry project endpoint used by `azure.ai.projects`, `azure.ai.loom`, and `azure.ai.agents`. The projects and Loom resolvers check the active azd environment before global project configuration and use the host shell environment as their final endpoint fallback. |
| `AZURE_AI_PROJECT_ENDPOINT` | Deprecated compatibility fallback for the Microsoft Foundry project endpoint. The Loom resolver checks it after `FOUNDRY_PROJECT_ENDPOINT` in both the active azd environment and the host shell. |
| `AZURE_AI_PROJECT_PRINCIPAL_ID` | The principal ID associated with the Microsoft Foundry project identity. |
| `AZURE_AI_ACCOUNT_NAME` | The Microsoft Foundry account name associated with the project. |
| `AZURE_AI_PROJECT_NAME` | The Microsoft Foundry project name. |
Expand Down
28 changes: 28 additions & 0 deletions cli/azd/extensions/azure.ai.loom/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Build output directory
bin/

# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Dependency directories (remove the comment below to include it)
# vendor/

# Go workspace file
go.work
go.work.sum

# env file
.env
17 changes: 17 additions & 0 deletions cli/azd/extensions/azure.ai.loom/.golangci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
version: "2"

linters:
default: none
enable:
- gosec
- lll
- unused
- errorlint
settings:
lll:
line-length: 220
tab-width: 4

formatters:
enable:
- gofmt
8 changes: 8 additions & 0 deletions cli/azd/extensions/azure.ai.loom/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Release History

## 0.1.0-preview (Unreleased)

### Features Added

- Added commands under `azd ai loom run` for Foundry experiment runs, traces,
spans, OpenTelemetry ingestion, and W&B-compatible APIs.
125 changes: 125 additions & 0 deletions cli/azd/extensions/azure.ai.loom/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Foundry Loom

Inspect and ingest Microsoft Foundry experiment-tracking data from `azd`. (Preview)

## Installation

```sh
azd extension install azure.ai.loom
```

The extension can reuse the project endpoint persisted by `azure.ai.projects`,
but it can also run independently with `--project-endpoint` or an environment
variable.

## Authentication and project resolution

Commands authenticate through `azd auth login` using the
`https://ai.azure.com/.default` scope. To use a Foundry account API key accepted
by the project data plane, set `AZURE_AI_PROJECT_API_KEY` in the current process.
The API key takes precedence over bearer authentication and must not be stored
in project files or source control.

The project endpoint is resolved in this order:

1. `--project-endpoint`
2. `FOUNDRY_PROJECT_ENDPOINT` or `AZURE_AI_PROJECT_ENDPOINT` in the active azd environment
3. The endpoint saved by `azd ai project set <endpoint>`
4. `FOUNDRY_PROJECT_ENDPOINT` or `AZURE_AI_PROJECT_ENDPOINT` in the host shell

The project ID is derived from `/api/projects/<project>` in the endpoint. Use
`--project-id` only when an API-compatible endpoint requires an override.

## Commands

### Inspect runs

```sh
azd ai loom run list
azd ai loom run history-keys --run-id <run-id>
azd ai loom run summary --run-id <run-id>
azd ai loom run metrics --run-id <run-id>
azd ai loom run system-metrics --run-id <run-id> --name system/cpu
azd ai loom run logs --run-id <run-id>
azd ai loom run log-records --run-id <run-id>
azd ai loom run compare \
--run-id <first-run> --run-id <second-run> \
--metric loss --min 0 --max 100
```

### Traces and spans

```sh
azd ai loom run trace list --run-id <run-id>
azd ai loom run trace show --run-id <run-id> --trace-id <trace-id>
azd ai loom run trace chat --run-id <run-id> --trace-id <trace-id>
azd ai loom run span query \
--run-id <run-id> \
--filter-file ./span-filter.json \
--include-details \
--limit 10
```

A span filter contains the query expression only:

```json
{
"$expr": {
"$eq": [
{ "$getField": "span_name" },
{ "$literal": "chat" }
]
}
}
```

When no filter is provided, the command uses `{"$expr":true}`. Use
`--request-file` to send a complete span-query or trace-chat request body.

### Ingest OpenTelemetry data

```sh
azd ai loom run ingest metrics --run-id <run-id> --file ./metrics.pb
azd ai loom run ingest logs --run-id <run-id> --file ./logs.pb
azd ai loom run ingest traces --run-id <run-id> --file ./traces.pb
azd ai loom run ingest agent-traces --run-id <run-id> --file ./agent-traces.json
```

The OTLP metrics, logs, and traces commands require binary protobuf payloads.
Agent traces require JSON. Use `--file -` to read from stdin. Empty payloads are
rejected before a service request is made.

### W&B compatibility

```sh
azd ai loom run wandb graphql --file ./graphql-request.json
azd ai loom run wandb file-stream \
--run-id <run-id> \
--file ./file-stream-request.json
```

All commands emit complete JSON responses for automation.

## Development

```sh
azd x build
go test ./... -count=1
```

To exercise every command against a project from PowerShell:

```powershell
.\test-all.ps1 `
-ProjectEndpoint "https://<account>.services.ai.azure.com/api/projects/<project-id>" `
-RunId "<run-id>" `
-SecondRunId "<second-run-id>" `
-TraceId "<trace-id>"
```

The script builds and installs the extension, runs all commands, and prints a
pass/fail summary. Set `AZURE_AI_PROJECT_API_KEY` before running it to use API
key authentication. Otherwise, it uses the current `azd auth login` session.
It generates temporary synthetic OTLP protobuf, agent-trace, GraphQL, and W&B
file-stream payloads for the ingestion tests and removes them afterward. Use
`-SkipWriteOperations` to test only inspection, trace, and span commands.
78 changes: 78 additions & 0 deletions cli/azd/extensions/azure.ai.loom/build.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Ensure script fails on any error
$ErrorActionPreference = 'Stop'

# Get the directory of the script
$EXTENSION_DIR = Split-Path -Parent $MyInvocation.MyCommand.Path

# Change to the script directory
Set-Location -Path $EXTENSION_DIR

# Create a safe version of EXTENSION_ID replacing dots with dashes
$EXTENSION_ID_SAFE = $env:EXTENSION_ID -replace '\.', '-'

# Define output directory
$OUTPUT_DIR = if ($env:OUTPUT_DIR) { $env:OUTPUT_DIR } else { Join-Path $EXTENSION_DIR "bin" }

# Create output directory if it doesn't exist
if (-not (Test-Path -Path $OUTPUT_DIR)) {
New-Item -ItemType Directory -Path $OUTPUT_DIR | Out-Null
}

# Get Git commit hash and build date
$COMMIT = git rev-parse HEAD
if ($LASTEXITCODE -ne 0) {
Write-Host "Error: Failed to get git commit hash"
exit 1
}
$BUILD_DATE = (Get-Date -Format "yyyy-MM-ddTHH:mm:ssZ")

# List of OS and architecture combinations
if ($env:EXTENSION_PLATFORM) {
$PLATFORMS = @($env:EXTENSION_PLATFORM)
}
else {
$PLATFORMS = @(
"windows/amd64",
"windows/arm64",
"darwin/amd64",
"darwin/arm64",
"linux/amd64",
"linux/arm64"
)
}

$APP_PATH = "$env:EXTENSION_ID/internal/cmd"

# Loop through platforms and build
foreach ($PLATFORM in $PLATFORMS) {
$OS, $ARCH = $PLATFORM -split '/'

$OUTPUT_NAME = Join-Path $OUTPUT_DIR "$EXTENSION_ID_SAFE-$OS-$ARCH"

if ($OS -eq "windows") {
$OUTPUT_NAME += ".exe"
}

Write-Host "Building for $OS/$ARCH..."

# Delete the output file if it already exists
if (Test-Path -Path $OUTPUT_NAME) {
Remove-Item -Path $OUTPUT_NAME -Force
}

# Set environment variables for Go build
$env:GOOS = $OS
$env:GOARCH = $ARCH

go build `
-ldflags="-X '$APP_PATH.Version=$env:EXTENSION_VERSION' -X '$APP_PATH.Commit=$COMMIT' -X '$APP_PATH.BuildDate=$BUILD_DATE'" `
-o $OUTPUT_NAME

if ($LASTEXITCODE -ne 0) {
Write-Host "An error occurred while building for $OS/$ARCH"
exit 1
}
}

Write-Host "Build completed successfully!"
Write-Host "Binaries are located in the $OUTPUT_DIR directory."
66 changes: 66 additions & 0 deletions cli/azd/extensions/azure.ai.loom/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#!/bin/bash

# Get the directory of the script
EXTENSION_DIR="$(cd "$(dirname "$0")" && pwd)"

# Change to the script directory
cd "$EXTENSION_DIR" || exit

# Create a safe version of EXTENSION_ID replacing dots with dashes
EXTENSION_ID_SAFE="${EXTENSION_ID//./-}"

# Define output directory
OUTPUT_DIR="${OUTPUT_DIR:-$EXTENSION_DIR/bin}"

# Create output and target directories if they don't exist
mkdir -p "$OUTPUT_DIR"

# Get Git commit hash and build date
COMMIT=$(git rev-parse HEAD)
BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)

# List of OS and architecture combinations
if [ -n "$EXTENSION_PLATFORM" ]; then
PLATFORMS=("$EXTENSION_PLATFORM")
else
PLATFORMS=(
"windows/amd64"
"windows/arm64"
"darwin/amd64"
"darwin/arm64"
"linux/amd64"
"linux/arm64"
)
fi

APP_PATH="$EXTENSION_ID/internal/cmd"

# Loop through platforms and build
for PLATFORM in "${PLATFORMS[@]}"; do
OS=$(echo "$PLATFORM" | cut -d'/' -f1)
ARCH=$(echo "$PLATFORM" | cut -d'/' -f2)

OUTPUT_NAME="$OUTPUT_DIR/$EXTENSION_ID_SAFE-$OS-$ARCH"

if [ "$OS" = "windows" ]; then
OUTPUT_NAME+='.exe'
fi

echo "Building for $OS/$ARCH..."

# Delete the output file if it already exists
[ -f "$OUTPUT_NAME" ] && rm -f "$OUTPUT_NAME"

# Set environment variables for Go build
GOOS=$OS GOARCH=$ARCH go build \
-ldflags="-X '$APP_PATH.Version=$EXTENSION_VERSION' -X '$APP_PATH.Commit=$COMMIT' -X '$APP_PATH.BuildDate=$BUILD_DATE'" \
-o "$OUTPUT_NAME"

if [ $? -ne 0 ]; then
echo "An error occurred while building for $OS/$ARCH"
exit 1
fi
done

echo "Build completed successfully!"
echo "Binaries are located in the $OUTPUT_DIR directory."
Loading
Loading