diff --git a/.github/.sync-sha b/.github/.sync-sha index 542225cef..a1a755f86 100644 --- a/.github/.sync-sha +++ b/.github/.sync-sha @@ -1 +1 @@ -8dc749def5ba8aaf43b57bab480fac44cfb95dcc +fa8eb002b35d34038f5752da6bfdb81291e6587d diff --git a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/modules-network-secured/ai-project-identity.bicep b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/modules-network-secured/ai-project-identity.bicep index 90aebfbd3..8e7cee7d5 100644 --- a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/modules-network-secured/ai-project-identity.bicep +++ b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/modules-network-secured/ai-project-identity.bicep @@ -72,6 +72,7 @@ resource project 'Microsoft.CognitiveServices/accounts/projects@2025-04-01-previ location: storageAccount.location } } + dependsOn: [project_connection_cosmosdb_account] } resource project_connection_azureai_search 'connections@2025-04-01-preview' = { @@ -86,6 +87,7 @@ resource project 'Microsoft.CognitiveServices/accounts/projects@2025-04-01-previ location: searchService.location } } + dependsOn: [project_connection_azure_storage] } } diff --git a/migration/README.md b/migration/README.md index 83dce8149..f1e317dd9 100644 --- a/migration/README.md +++ b/migration/README.md @@ -1,486 +1,179 @@ -# V1 to V2 Assistant Migration Tool +# V1 โ†’ V2 Agent Migration Tool -A comprehensive tool for migrating OpenAI v1 assistants to Azure AI v2 agents with Docker containerization support, extensive test capabilities, and cross-platform compatibility. +Migrate classic Azure AI assistants (v1 `asst_...` items) to v2 named agents. -## ๐Ÿš€ Quick Start +--- -### Prerequisites +## Quickstart -- **Docker Desktop** installed and running -- **Azure CLI** installed on your host system -- **Azure account** with appropriate permissions -- **Python 3.11+** (if running locally without Docker) +### Step 1 โ€” Download Docker Desktop -### Authentication Setup +[Download Docker Desktop](https://www.docker.com/products/docker-desktop/) -Choose your platform and run the authentication setup script: +Start it and wait for the whale icon to appear in your system tray. -**Windows (PowerShell):** -```powershell -.\setup-azure-auth.bat -``` +### Step 2 โ€” Install Azure CLI -**Linux/macOS (Bash):** -```bash -./setup-azure-auth.sh -``` +Windows: -This script will: -- โœ… Verify Docker is running -- โœ… Check Azure CLI installation -- โœ… Handle Azure authentication -- โœ… Build the Docker image -- โœ… Test authentication in container +[Install Azure CLI for Windows](https://learn.microsoft.com/cli/azure/install-azure-cli-windows) -### Running Migrations +If `winget` is available, `migrate-docker.ps1` will try to install Azure CLI for you automatically when it is missing. -After authentication setup, use the platform-specific runner: +### Step 3 โ€” Run -**Windows (PowerShell):** ```powershell -# Regular output -.\run-migration.bat --help +cd migration -# Verbose output -.\run-migration-verbose.bat --help - -# Docker-based with automatic authentication (recommended) -.\run-migration-docker-auth.ps1 --help -``` - -**Linux/macOS (Bash):** -```bash -./run-migration.sh --help +.\migrate-docker.ps1 --resource-id "" --list ``` -## ๐Ÿ“‹ Usage Examples - -### 1. Production Migration with Dual-Tenant Authentication (REQUIRED) +That's it. No Python install and no `pip install`. +The migration runtime stays inside Docker, but Azure sign-in happens on the Windows host via Azure CLI so corporate Conditional Access can validate your compliant device. -**All migrations require production parameters:** +The `--list` flag is read-only โ€” it shows what would be migrated without making any changes. +When you're ready to migrate for real, drop `--list`: ```powershell -# Windows PowerShell - Migrate from v1 API to production v2 API -.\run-migration-docker-auth.ps1 ` - --use-api ` - --source-tenant "72f988bf-86f1-41af-91ab-2d7cd011db47" ` - --production-resource "nextgen-eastus" ` - --production-subscription "b1615458-c1ea-49bc-8526-cafc948d3c25" ` - --production-tenant "33e577a9-b1b8-4126-87c0-673f197bf624" ` - asst_abc123def456 - -# Linux/macOS - Same command structure -./run-migration-docker-auth.sh \ - --use-api \ - --source-tenant "72f988bf-86f1-41af-91ab-2d7cd011db47" \ - --production-resource "nextgen-eastus" \ - --production-subscription "b1615458-c1ea-49bc-8526-cafc948d3c25" \ - --production-tenant "33e577a9-b1b8-4126-87c0-673f197bf624" \ - asst_abc123def456 -``` - -**Required Parameters:** -- `--production-resource`: Azure AI resource name (e.g., "nextgen-eastus") -- `--production-subscription`: Subscription ID for production tenant -- `--production-tenant`: Production tenant ID for writing agents -- `--source-tenant`: Source tenant ID for reading assistants (optional, defaults to Microsoft tenant) - -### 2. Migrate Using Project Connection String (Beta) -```bash -# Connection string format: region.api.azureml.ms;subscription-id;resource-group;project-name -./run-migration-docker-auth.sh \ - --project-connection-string "eastus.api.azureml.ms;abc-123;my-rg;my-project" \ - --production-resource "nextgen-eastus" \ - --production-subscription "b1615458-c1ea-49bc-8526-cafc948d3c25" \ - --production-tenant "33e577a9-b1b8-4126-87c0-673f197bf624" \ - asst_abc123def456 - -# Windows PowerShell -.\run-migration-docker-auth.ps1 ` - --project-connection-string "eastus.api.azureml.ms;abc-123;my-rg;my-project" ` - --production-resource "nextgen-eastus" ` - --production-subscription "b1615458-c1ea-49bc-8526-cafc948d3c25" ` - --production-tenant "33e577a9-b1b8-4126-87c0-673f197bf624" ` - asst_abc123def456 -``` -> **Note**: Connection string support requires `azure-ai-projects==1.0.0b10` (beta). The script automatically detects and installs the correct version. Production parameters are always required. - -### 3. Migrate from Project Endpoint to Production v2 API -```bash -# Using project endpoint (production parameters required) -./run-migration-docker-auth.sh \ - --project-endpoint "https://your-project.cognitiveservices.azure.com" \ - --production-resource "nextgen-eastus" \ - --production-subscription "b1615458-c1ea-49bc-8526-cafc948d3c25" \ - --production-tenant "33e577a9-b1b8-4126-87c0-673f197bf624" \ - assistant-id -``` - -### 4. Add Test Tools to Migration -```bash -# Add function calling test (production parameters required) -./run-migration-docker-auth.sh \ - --use-api \ - --add-test-function \ - --production-resource "nextgen-eastus" \ - --production-subscription "b1615458-c1ea-49bc-8526-cafc948d3c25" \ - --production-tenant "33e577a9-b1b8-4126-87c0-673f197bf624" \ - assistant-id - -# Add multiple test tools -./run-migration-docker-auth.sh \ - --use-api \ - --add-test-function \ - --add-test-mcp \ - --add-test-computer \ - --production-resource "nextgen-eastus" \ - --production-subscription "b1615458-c1ea-49bc-8526-cafc948d3c25" \ - --production-tenant "33e577a9-b1b8-4126-87c0-673f197bf624" \ - assistant-id -``` - -### 5. Environment Variables Support -Create a `.env` file in the project directory: -```env -# Azure Project Configuration -PROJECT_ENDPOINT_URL=https://your-project.cognitiveservices.azure.com -PROJECT_CONNECTION_STRING=your-connection-string - -# Cosmos DB Configuration (optional) -# Use either COSMOS_CONNECTION_STRING (recommended) or individual parameters -COSMOS_CONNECTION_STRING=AccountEndpoint=https://...;AccountKey=...; -# OR (legacy - still supported) -COSMOS_DB_CONNECTION_STRING=your-cosmos-connection-string -COSMOS_DB_DATABASE_NAME=your-database -COSMOS_DB_CONTAINER_NAME=your-container - -# v1 API Configuration (optional) -ASSISTANT_API_BASE=https://api.openai.com/v1 -ASSISTANT_API_KEY=your-openai-key -ASSISTANT_API_VERSION=v1 - -# v2 API Configuration (optional) -V2_API_BASE=https://your-v2-api.cognitiveservices.azure.com -V2_API_KEY=your-v2-key -V2_API_VERSION=2024-05-01-preview - -# Azure Authentication (optional) -AZURE_TENANT_ID=your-tenant-id -AZURE_CLIENT_ID=your-client-id -AZURE_CLIENT_SECRET=your-client-secret -AZURE_SUBSCRIPTION_ID=your-subscription-id -AZURE_RESOURCE_GROUP=your-resource-group -AZURE_PROJECT_NAME=your-project-name +.\migrate-docker.ps1 --resource-id "" ``` -## ๐Ÿ› ๏ธ Command Line Options - -### Input Methods (choose one) -- `--use-api` - Read from v1 API (recommended) -- `--project-endpoint URL` - Use Azure AI Project endpoint -- `--project-connection-string STRING` - Use Azure AI Project connection string -- `--cosmos` - Read from Cosmos DB (legacy) - -### Output Methods -- Always uses **production v2 API** (requires production parameters) - -### Test Tool Options (optional, can use multiple) -- `--add-test-tool function` - Add function calling test -- `--add-test-tool mcp` - Add Model Context Protocol test -- `--add-test-tool computer-use` - Add computer use test -- `--add-test-tool image-gen` - Add image generation test -- `--add-test-tool azure-function` - Add Azure Function test - -### Production Migration Options (REQUIRED for Docker Auth Scripts) -- `--production-resource RESOURCE_NAME` - **REQUIRED** Production Azure AI resource name (e.g., "nextgen-eastus") -- `--production-subscription SUBSCRIPTION_ID` - **REQUIRED** Production subscription ID -- `--production-tenant TENANT_ID` - **REQUIRED** Production tenant for writing agents -- `--source-tenant TENANT_ID` - *Optional* Source tenant for reading assistants (defaults to Microsoft tenant: 72f988bf-86f1-41af-91ab-2d7cd011db47) +--- -### Configuration Options -- `--v1-api-version VERSION` - v1 API version (default: v1) -- `--v2-api-version VERSION` - v2 API version (default: 2024-05-01-preview) -- `--cosmos-database DATABASE` - Cosmos database name -- `--cosmos-container CONTAINER` - Cosmos container name +## Where to find your Resource ID -## ๏ฟฝ Unsupported Classic Assistant Features +**Azure Portal** โ†’ your AI Services resource โ†’ **Properties** โ†’ **Resource ID** -The migration tool will **continue migration** for classic assistants (v1) that use features not supported in new agents (v2), but will **skip the unsupported tools** and display warnings: +or **Foundry portal** โ†’ Project settings โ†’ **Resource ID** -### Connected Agent Tool -``` -โš ๏ธ WARNING: Your classic agent includes connected agents, which aren't supported in the new experience. -โ„น๏ธ These connected agents won't be carried over when you create the new agent. -๐Ÿ’ก To orchestrate multiple agents, use a workflow instead. -๐Ÿ“‹ Unsupported tools that will be skipped: connected_agent +```text +/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.CognitiveServices/accounts/{resource}/projects/{project} ``` -**What happens**: The connected_agent tool is skipped during migration. The new agent is created successfully without this tool. -**Recommendation**: Use **new agent workflows** to connect multiple agents together. +### Your current values (validated) -### Event Binding Tool -``` -โš ๏ธ WARNING: Your classic agent uses 'event_binding' which isn't supported in the new experience. -โ„น๏ธ This tool won't be carried over when you create the new agent. -๐Ÿ“‹ Unsupported tools that will be skipped: event_binding -``` -**What happens**: The event_binding tool is skipped during migration. The new agent is created successfully without this tool. +| | | +| --- | --- | +| **Full Resource ID** | `/subscriptions/47f1c914-e299-4953-a99d-3e34644cfe1c/resourceGroups/rg-nikhowlett-6102/providers/Microsoft.CognitiveServices/accounts/nikhowlett-6102-resource/projects/nikhowlett-6102` | +| Subscription | `47f1c914-e299-4953-a99d-3e34644cfe1c` | +| Tenant | `72f988bf-86f1-41af-91ab-2d7cd011db47` | +| Resource group | `rg-nikhowlett-6102` | +| Resource name | `nikhowlett-6102-resource` | +| Project name | `nikhowlett-6102` | +| Account | `nikhowlett@microsoft.com` | -**Recommendation**: This feature has no direct equivalent in new agents. +Ready-to-run: -### Output Binding Tool -``` -โš ๏ธ WARNING: Your classic agent uses 'output_binding' which isn't supported in the new experience. -โ„น๏ธ This tool won't be carried over when you create the new agent. -๐Ÿ’ก Consider using 'capture_structured_outputs' in your new agent instead. -๐Ÿ“‹ Unsupported tools that will be skipped: output_binding +```powershell +.\migrate-docker.ps1 --resource-id "/subscriptions/47f1c914-e299-4953-a99d-3e34644cfe1c/resourceGroups/rg-nikhowlett-6102/providers/Microsoft.CognitiveServices/accounts/nikhowlett-6102-resource/projects/nikhowlett-6102" --list ``` -**What happens**: The output_binding tool is skipped during migration. The new agent is created successfully without this tool. - -**Recommendation**: Use **`capture_structured_outputs`** in new agents for structured output capture. - -> **Note**: Migration completes successfully even when unsupported tools are present. Only the unsupported tools are excluded from the new agent. All other tools and properties are migrated normally. - -## ๏ฟฝ๐Ÿณ Docker Architecture - -### Container Features -- **Base Image**: Python 3.11-slim with Azure CLI -- **Non-root User**: Runs as `migration` user for security -- **Volume Mounts**: Azure CLI config directory for authentication -- **Network**: Host networking for localhost API access -- **Environment**: Comprehensive environment variable support -### Dynamic Package Installation -The container automatically installs the correct `azure-ai-projects` package version based on your usage: - -- **Standard version (1.0.0)**: Used for project endpoints and most scenarios -- **Beta version (1.0.0b10)**: Automatically installed when using `--project-connection-string` - - Required for `from_connection_string()` method support - - Detection and installation happens at container startup - - No manual intervention needed - -The script detects connection string usage and sets the `NEED_BETA_VERSION` flag automatically. - -### Dual-Tenant Authentication (REQUIRED) -The `run-migration-docker-auth.ps1` and `run-migration-docker-auth.sh` scripts require production parameters for all migrations: - -- **Production-First Architecture**: All migrations write to production v2 API (no localhost mode) -- **Required Production Parameters**: Must specify production resource, subscription, and tenant -- **Source Tenant Authentication**: Reads assistants from source tenant (defaults to Microsoft tenant) -- **Production Tenant Authentication**: Writes agents to production tenant -- **Automatic Token Management**: Generates and manages separate tokens for each tenant -- **Seamless Tenant Switching**: Handles Azure CLI tenant switching automatically -- **Token Isolation**: Source and production tokens are isolated for security -- **Cross-Platform Support**: Both PowerShell (Windows) and Bash (Linux/macOS) versions - -### Security Considerations -- Non-root container execution -- Read-write Azure CLI directory for token management -- Environment variable injection for sensitive data -- Host network isolation when possible +--- -## ๐Ÿงช Test Tool Capabilities +## RBAC โ€” Required permissions -The migration tool can inject various test tools into migrated agents: +The signed-in user needs **Azure AI User** on the AI Services resource. +Without this role you will see `401` or `403` errors. -### Function Calling Test -```python -def get_weather(location: str) -> str: - """Get current weather for a location.""" - return f"Weather in {location}: 72ยฐF, sunny" -``` +### Check your current roles -### Model Context Protocol (MCP) Test -```python -def mcp_filesystem_tool(action: str, path: str) -> str: - """MCP filesystem operations.""" - return f"MCP {action} operation on {path} completed" -``` - -### Computer Use Test -```python -def computer_screenshot() -> str: - """Take a screenshot of the current screen.""" - return "Screenshot taken: desktop_1024x768.png" +```powershell +az role assignment list --assignee "nikhowlett@microsoft.com" ` + --scope "/subscriptions/47f1c914-e299-4953-a99d-3e34644cfe1c/resourceGroups/rg-nikhowlett-6102/providers/Microsoft.CognitiveServices/accounts/nikhowlett-6102-resource" ` + -o table ``` -### Image Generation Test -```python -def generate_image(prompt: str) -> str: - """Generate an image from a text prompt.""" - return f"Generated image for: {prompt}" -``` +### Grant the role (requires Owner or User Access Admin on the resource) -### Azure Function Test -```python -def azure_function_call(function_name: str, data: dict) -> str: - """Call an Azure Function.""" - return f"Azure Function {function_name} called with {data}" +```powershell +az role assignment create ` + --role "Azure AI User" ` + --assignee "nikhowlett@microsoft.com" ` + --scope "/subscriptions/47f1c914-e299-4953-a99d-3e34644cfe1c/resourceGroups/rg-nikhowlett-6102/providers/Microsoft.CognitiveServices/accounts/nikhowlett-6102-resource" ``` -## ๐Ÿ—๏ธ Architecture Details +๐Ÿ“– [Foundry RBAC docs](https://learn.microsoft.com/azure/ai-foundry/concepts/rbac-ai-foundry) -### Core Components - -1. **Migration Engine** (`v1_to_v2_migration.py`) - - Smart parameter extraction from endpoints - - Multi-input/output method support - - Comprehensive error handling - - Test tool injection capabilities - -2. **Docker Container** (`Dockerfile`) - - Multi-stage build process - - Azure CLI integration - - Proper user permissions - - Environment configuration +--- -3. **Platform Scripts** - - Windows: `setup-azure-auth.bat`, `run-migration.bat`, `run-migration-verbose.bat` - - Linux/macOS: `setup-azure-auth.sh`, `run-migration.sh` +## Common commands -### Data Flow +```powershell +# List everything (read-only) +.\migrate-docker.ps1 --resource-id "" --list -``` -Input Sources โ†’ Migration Engine โ†’ Output Destinations - โ†“ โ†“ โ†“ -- Cosmos DB โ†’ Transform v1โ†’v2 โ†’ - Cosmos DB -- v1 API โ†’ Add test tools โ†’ - v2 API -- Project EP โ†’ Parameter extract -- Connection โ†’ Error handling -``` +# Migrate ALL items +.\migrate-docker.ps1 --resource-id "" -### Parameter Extraction +# Migrate only items with tools (bing, code_interpreter, etc.) +.\migrate-docker.ps1 --resource-id "" --only-with-tools -The tool automatically extracts Azure AI project parameters from endpoints and connection strings: +# Migrate only plain assistants (no tools) +.\migrate-docker.ps1 --resource-id "" --only-without-tools -**From Project Endpoint:** -``` -https://projectname-region.cognitiveservices.azure.com -โ†’ subscription_id, resource_group_name, project_name -``` +# Migrate one item by ID +.\migrate-docker.ps1 --resource-id "" asst_abc123def456 -**From Connection String:** -``` -endpoint=https://...;subscriptionid=...;resourcegroupname=...;projectname=... -โ†’ Parsed individual components +# Cross-project: read from one project, write to another +.\migrate-docker.ps1 --source-resource-id "" --resource-id "" ``` -## ๐Ÿ”ง Troubleshooting - -### Common Issues - -1. **Docker Not Running** - ``` - โŒ Docker is not running. Please start Docker and try again. - ``` - **Solution**: Start Docker Desktop - -2. **Azure CLI Not Authenticated** - ``` - โš ๏ธ Not authenticated to Azure CLI - ``` - **Solution**: Run `az login` or use the setup script - -3. **Unsupported Tool Types** - ``` - WARNING: Your classic agent includes connected agents... - ``` - **Solution**: Migration will continue but unsupported tools will be skipped. See the "Unsupported Classic Assistant Features" section above for details and alternatives - -4. **Connection String Format** - ``` - Failed to parse connection string - ``` - **Solution**: Use format `region.api.azureml.ms;subscription-id;resource-group;project-name` - -5. **Dual-Tenant Authentication Issues** - ``` - Token tenant does not match resource tenant - ``` - **Solution**: Ensure correct source and production tenant IDs are specified - -6. **Agent Name Case Sensitivity** - ``` - 400 Bad Request on production endpoint - ``` - **Solution**: Agent names are automatically converted to lowercase with proper formatting - -5. **Container Authentication Fails** - ``` - Authentication test failed - ``` - **Solution**: Ensure Azure CLI directory has proper permissions - -6. **Network Connection Issues** - ``` - Connection refused to localhost - ``` - **Solution**: Use `--network host` flag (included in scripts) - -### Debug Mode - -Use verbose scripts for detailed output: -```powershell -# Windows -.\run-migration-verbose.bat --help +--- -# Linux/macOS -./run-migration.sh --help # Already verbose -``` +## What `--list` actually shows -### Manual Docker Commands +The tool checks **two separate backends** โ€” the Foundry portal stores v1 items in two different places depending on how they were created: -If scripts fail, run manually: -```bash -# Build image -docker build -t v1-to-v2-migration . +| Portal page | Endpoint queried | +| --- | --- | +| **Agents** page | `{resource}.services.ai.azure.com/api/projects/{project}/assistants` | +| **Assistants** page | `{resource}.cognitiveservices.azure.com/openai/assistants` | -# Run with debugging -docker run --rm -it \ - --network host \ - -v ~/.azure:/home/migration/.azure \ - v1-to-v2-migration \ - /bin/bash -``` +All `asst_...` items on either endpoint are v1 and need migration. +Real v2 agents are named (`agent.name = "MyAgent"`, `agent.version = "1"`) โ€” `--list` will never show those. -## ๐Ÿ“š API Compatibility +--- -### Supported Azure AI Project APIs -- **2024-05-01-preview** (default) -- **2024-02-15-preview** -- **2023-12-01-preview** +## Don't have Docker? Use migrate.ps1 instead -### Supported OpenAI v1 APIs -- **OpenAI API v1** -- **Azure OpenAI v1** -- **Compatible third-party APIs** +If you already have **Python 3.10+** and **Azure CLI** installed locally, you can skip Docker: -## ๐Ÿค Contributing +```powershell +pip install -r requirements.txt # one-time -### Development Setup -1. Clone repository -2. Install dependencies: `pip install -r requirements.txt` -3. Run tests: `python -m pytest` -4. Build Docker: `docker build -t v1-to-v2-migration .` +.\migrate.ps1 --resource-id "" --list +.\migrate.ps1 --resource-id "" +``` -### Adding New Test Tools -1. Add tool definition to `TEST_TOOLS` dictionary -2. Update `inject_test_tools()` function -3. Add command-line option handling -4. Update documentation +`migrate.ps1` handles Azure login automatically โ€” if you're not signed in it starts device-code flow. -## ๐Ÿ“„ License +Linux / macOS: use `./migrate.sh` with the same flags. -MIT License - see LICENSE file for details +--- -## ๐Ÿ†˜ Support +## Troubleshooting -For issues and questions: -1. Check troubleshooting section -2. Run with verbose output -3. Check Docker and Azure CLI status -4. Verify authentication setup +| Error | Fix | +| --- | --- | +| `failed to connect to the docker API` | Docker Desktop is not running โ€” start it and wait for the whale icon | +| `AADSTS53003` or `You don't have access to this` | Corporate Conditional Access blocked sign-in inside Linux containers โ€” use host Azure CLI login; `migrate-docker.ps1` now does this automatically | +| `az : The term 'az' is not recognized` | Install Azure CLI, or rerun `migrate-docker.ps1` on Windows and let it install Azure CLI with `winget` | +| `401 Unauthorized` | Check RBAC โ€” you need **Azure AI User** on the resource (see above) | +| `403 Forbidden` | Assign **Azure AI User** role (see RBAC section above) | +| `Could not switch to subscription` | Run `az login --tenant 72f988bf-86f1-41af-91ab-2d7cd011db47` | +| Items missing from `--list` | Both endpoints are checked; if still missing the item may have been deleted | +| Docker Desktop crashes on startup | Delete `%USERPROFILE%\.docker\contexts\meta` and restart | --- -**Made with โค๏ธ for seamless AI agent migration** \ No newline at end of file +## Files in this folder + +| File | Purpose | +| --- | --- | +| `migrate-docker.ps1` | **Main entry point** โ€” Docker wrapper, no local Python needed | +| `migrate.ps1` | Local Python alternative (requires Python + Azure CLI) | +| `migrate.sh` | Bash version of `migrate.ps1` | +| `v1_to_v2_migration.py` | Core migration engine | +| `requirements.txt` | Python deps (baked into the Docker image, also used by `migrate.ps1`) | +| `Dockerfile` | Container: Python 3.11 + Azure CLI + requirements | +| `entrypoint-login.sh` | Helper entrypoint for Azure-auth scenarios inside the container | diff --git a/migration/v1_to_v2_migration.py b/migration/v1_to_v2_migration.py index 5b12a545d..840b32701 100644 --- a/migration/v1_to_v2_migration.py +++ b/migration/v1_to_v2_migration.py @@ -1,5 +1,6 @@ -import os, sys, time, json, argparse, subprocess, requests +import os, sys, time, json, argparse, subprocess, requests, re from typing import List, Dict, Any, Optional +from urllib.parse import urlparse from azure.cosmos import CosmosClient, exceptions from azure.ai.agents.models import AzureFunctionStorageQueue, AzureFunctionTool @@ -30,6 +31,10 @@ WORKSPACE = os.getenv("AGENTS_WORKSPACE") or "basicaccountjqqa@e2e-tests@AML" WORKSPACE_V2 = os.getenv("AGENTS_WORKSPACE_V2") or "e2e-tests-westus2-account@e2e-tests-westus2@AML" API_VERSION = os.getenv("AGENTS_API_VERSION") or "2025-05-15-preview" +# SOURCE_API_VERSION is used when reading from legacy openai.azure.com endpoints. +# Legacy OpenAI-kind resources only support older API versions (e.g. 2024-05-01-preview), +# not the newer 2025-xx-xx-preview Azure ML / AIServices versions. +SOURCE_API_VERSION = os.getenv("SOURCE_API_VERSION") or None # None = auto-detect per endpoint TOKEN = os.getenv("AZ_TOKEN") # Source Tenant Configuration (for reading v1 assistants from source tenant) @@ -100,6 +105,65 @@ def get_production_v2_base_url(resource_name: str, subscription_id: str, project hostname = f"{resource_name}-resource.services.ai.azure.com" return f"https://{hostname}/api/projects/{project_name}" + +def sanitize_agent_name(agent_name: str) -> str: + """ + Normalize an agent name to the v2 API constraints. + + The v2 API accepts only lowercase alphanumerics and hyphens, with a + maximum length of 63 characters. + """ + normalized_name = (agent_name or "").lower().replace('_', '-') + normalized_name = re.sub(r"[^a-z0-9-]", "-", normalized_name) + normalized_name = re.sub(r"-+", "-", normalized_name).strip('-') + normalized_name = normalized_name[:63].rstrip('-') + return normalized_name or "agent" + + +def get_target_openai_endpoint(production_resource: Optional[str] = None, production_endpoint: Optional[str] = None) -> Optional[str]: + """ + Derive the OpenAI-compatible endpoint (``*.openai.azure.com/openai``) + for the target resource. Kept for reference / fallback. + """ + if production_endpoint: + import re + m = re.match(r"https://([^.]+)\.", production_endpoint) + if m: + return f"https://{m.group(1)}.openai.azure.com/openai" + if production_resource: + if production_resource.endswith("-resource"): + hostname = production_resource + else: + hostname = f"{production_resource}-resource" + return f"https://{hostname}.openai.azure.com/openai" + return None + + +def get_target_foundry_endpoint(production_resource: Optional[str] = None, production_endpoint: Optional[str] = None) -> Optional[str]: + """ + Return the Foundry endpoint for file uploads and vector-store creation. + + Files and vector stores **must** be created through the Foundry + (``*.services.ai.azure.com``) endpoint so they live in the same + namespace the Foundry agent runtime uses. Resources created via the + legacy OpenAI endpoint (``*.openai.azure.com``) are invisible to agents + running in the Foundry portal. + """ + if production_endpoint: + # Already a Foundry URL โ€” return it directly + # e.g. "https://res-resource.services.ai.azure.com/api/projects/proj" + return production_endpoint.rstrip('/') + if production_resource: + if production_resource.endswith("-resource"): + hostname = production_resource + else: + hostname = f"{production_resource}-resource" + # Default project name = resource name without the -resource suffix + project = production_resource.replace("-resource", "") if production_resource.endswith("-resource") else production_resource + return f"https://{hostname}.services.ai.azure.com/api/projects/{project}" + return None + + # Production token handling removed - now handled by PowerShell wrapper # which provides PRODUCTION_TOKEN environment variable @@ -1145,7 +1209,343 @@ def list_assistants_from_project_connection(project_connection_string: str) -> L agent_list.append(agent_dict) return agent_list -def get_assistant_from_project(project_endpoint: str, assistant_id: str, subscription_id: Optional[str] = None, resource_group_name: Optional[str] = None, project_name: Optional[str] = None) -> Dict[str, Any]: +def _get_source_api_version(project_endpoint: str) -> str: + """ + Return the API version to use when reading from a source project endpoint. + Legacy openai.azure.com endpoints only support older API versions. + """ + if SOURCE_API_VERSION: + return SOURCE_API_VERSION + # Parse the endpoint to extract the hostname for a safe check. + # A plain substring test (e.g. '"openai.azure.com" in url') would + # also match crafted URLs like evil-openai.azure.com.attacker.com. + parsed = urlparse(project_endpoint) + host = parsed.hostname if parsed.hostname else project_endpoint.strip('/') + if host == "openai.azure.com" or host.endswith(".openai.azure.com"): + # Legacy OpenAI-kind resource โ€” use the last known compatible version + return "2024-05-01-preview" + return API_VERSION + + +# โ”€โ”€ File Migration Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# These functions handle downloading files from a v1 source environment +# and re-uploading them to the v2 target, including vector-store creation. +# Without this step, migrated agents that use file_search or +# code_interpreter will have broken references (source file IDs / vector +# store IDs don't exist on the target). +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +def download_file_from_source( + source_endpoint: str, + file_id: str, + source_token: Optional[str] = None, +) -> Optional[Dict[str, Any]]: + """ + Download a file's metadata and content from the source endpoint. + + Returns a dict with keys ``id``, ``filename``, ``bytes``, ``purpose``, + and ``content`` (raw bytes), or *None* on failure. + """ + token = source_token or TOKEN + if not token: + print(f" โš ๏ธ No token available for file download") + return None + + api_version = _get_source_api_version(source_endpoint) + base = source_endpoint.rstrip('/') + headers = {"Authorization": f"Bearer {token}"} + params = {"api-version": api_version} + + # 1. Get metadata + try: + meta_r = requests.get(f"{base}/files/{file_id}", headers=headers, params=params, timeout=30) + meta_r.raise_for_status() + meta = meta_r.json() + except Exception as e: + print(f" โŒ Failed to get metadata for file {file_id}: {e}") + return None + + # 2. Download content + try: + content_r = requests.get(f"{base}/files/{file_id}/content", headers=headers, params=params, timeout=60) + content_r.raise_for_status() + except Exception as e: + print(f" โŒ Failed to download content for file {file_id}: {e}") + return None + + result = { + "id": file_id, + "filename": meta.get("filename", f"file_{file_id}"), + "bytes": meta.get("bytes", len(content_r.content)), + "purpose": meta.get("purpose", "assistants"), + "content": content_r.content, + } + print(f" ๐Ÿ“ฅ Downloaded {result['filename']} ({len(content_r.content)}B)") + return result + + +def upload_file_to_target( + target_endpoint: str, + filename: str, + content: bytes, + purpose: str = "assistants", + target_token: Optional[str] = None, +) -> Optional[str]: + """ + Upload a file to the target endpoint. + + Returns the new file ID on success, or *None* on failure. + """ + token = target_token or PRODUCTION_TOKEN or TOKEN + if not token: + print(f" โš ๏ธ No token available for file upload") + return None + + base = target_endpoint.rstrip('/') + headers = {"Authorization": f"Bearer {token}"} + # Use the target API version (not the source one). + # API_VERSION is 2025-05-15-preview for the Foundry project endpoint (*.services.ai.azure.com). + # The REST quickstart uses api-version=v1 but targets the legacy OpenAI-compatible endpoint (*.openai.azure.com), + # which is a different API surface. + params = {"api-version": API_VERSION} + + try: + resp = requests.post( + f"{base}/files", + headers=headers, + params=params, + data={"purpose": purpose}, + files={"file": (filename, content)}, + timeout=60, + ) + resp.raise_for_status() + new_id = resp.json().get("id") + print(f" ๐Ÿ“ค Uploaded {filename} -> {new_id}") + return new_id + except Exception as e: + print(f" โŒ Failed to upload {filename}: {e}") + if hasattr(e, 'response') and e.response is not None: + print(f" {e.response.text[:300]}") + return None + + +def list_vector_store_files( + source_endpoint: str, + vector_store_id: str, + source_token: Optional[str] = None, +) -> List[str]: + """ + Return the list of file IDs inside a source vector store. + """ + token = source_token or TOKEN + if not token: + return [] + + api_version = _get_source_api_version(source_endpoint) + base = source_endpoint.rstrip('/') + headers = {"Authorization": f"Bearer {token}"} + params = {"api-version": api_version} + + try: + resp = requests.get( + f"{base}/vector_stores/{vector_store_id}/files", + headers=headers, params=params, timeout=30, + ) + resp.raise_for_status() + data = resp.json().get("data", []) + file_ids = [f.get("id") for f in data if f.get("id")] + print(f" ๐Ÿ“‹ Vector store {vector_store_id} contains {len(file_ids)} file(s)") + return file_ids + except Exception as e: + print(f" โš ๏ธ Could not list files in vector store {vector_store_id}: {e}") + return [] + + +def create_vector_store_on_target( + target_endpoint: str, + file_ids: List[str], + name: str = "migrated-vector-store", + target_token: Optional[str] = None, +) -> Optional[str]: + """ + Create a vector store on the target and attach *file_ids* to it. + + Polls until the vector store reaches ``completed`` status. + Returns the new vector-store ID, or *None* on failure. + """ + token = target_token or PRODUCTION_TOKEN or TOKEN + if not token or not file_ids: + return None + + base = target_endpoint.rstrip('/') + headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + # Use the target API version for the Foundry project endpoint (*.services.ai.azure.com). + # This is 2025-05-15-preview, not the legacy api-version=v1 from the REST quickstart + # (which targets a different OpenAI-compatible endpoint). + params = {"api-version": API_VERSION} + + try: + resp = requests.post( + f"{base}/vector_stores", + headers=headers, params=params, + json={"name": name, "file_ids": file_ids}, + timeout=30, + ) + resp.raise_for_status() + vs = resp.json() + vs_id = vs.get("id") + print(f" ๐Ÿ—„๏ธ Created vector store {vs_id} (status: {vs.get('status')})") + except Exception as e: + print(f" โŒ Failed to create vector store: {e}") + if hasattr(e, 'response') and e.response is not None: + print(f" {e.response.text[:300]}") + return None + + # Poll until completed + import time as _time + for _ in range(20): + _time.sleep(2) + try: + check = requests.get(f"{base}/vector_stores/{vs_id}", headers=headers, params=params, timeout=15) + status = check.json().get("status", "unknown") + fc = check.json().get("file_counts", {}) + if status == "completed": + print(f" โœ… Vector store ready (completed={fc.get('completed', 0)})") + return vs_id + if status == "failed": + print(f" โŒ Vector store indexing failed: {check.json()}") + return vs_id # Return anyway โ€” caller can decide + except Exception: + pass + print(f" โš ๏ธ Vector store polling timed out (returning {vs_id})") + return vs_id + + +def migrate_assistant_files( + source_endpoint: str, + target_endpoint: str, + v1_assistant: Dict[str, Any], + source_token: Optional[str] = None, + target_token: Optional[str] = None, +) -> Dict[str, Any]: + """ + Download every file referenced by *v1_assistant* from *source_endpoint* + and re-upload to *target_endpoint*. Creates new vector stores on the + target for any ``file_search`` vector-store references. + + Returns a **remapping dict**:: + + { + "file_id_map": {old_id: new_id, ...}, + "vs_id_map": {old_vs_id: new_vs_id, ...}, + } + + The caller should use these maps to rewrite IDs in the v2 tool + definitions before posting to the target API. + """ + file_id_map: Dict[str, str] = {} + vs_id_map: Dict[str, str] = {} + + tool_resources = v1_assistant.get("tool_resources", {}) + if isinstance(tool_resources, str): + try: + tool_resources = json.loads(tool_resources) + except (json.JSONDecodeError, TypeError): + tool_resources = {} + if not isinstance(tool_resources, dict): + tool_resources = {} + + print(f"\n ๐Ÿ“ฆ Migrating files for assistant {v1_assistant.get('id', '?')}") + + # โ”€โ”€ 1. code_interpreter files โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + ci_file_ids = tool_resources.get("code_interpreter", {}).get("file_ids", []) + if ci_file_ids: + print(f" ๐Ÿ”ง code_interpreter: {len(ci_file_ids)} file(s) to migrate") + for fid in ci_file_ids: + if fid in file_id_map: + continue # already migrated (shared with VS) + downloaded = download_file_from_source(source_endpoint, fid, source_token) + if downloaded: + new_id = upload_file_to_target( + target_endpoint, downloaded["filename"], downloaded["content"], + purpose="assistants", target_token=target_token, + ) + if new_id: + file_id_map[fid] = new_id + + # โ”€โ”€ 2. file_search vector stores โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + vs_ids = tool_resources.get("file_search", {}).get("vector_store_ids", []) + if vs_ids: + print(f" ๐Ÿ” file_search: {len(vs_ids)} vector store(s) to migrate") + for vs_id in vs_ids: + # List files in the source VS + vs_file_ids = list_vector_store_files(source_endpoint, vs_id, source_token) + new_vs_file_ids = [] + for fid in vs_file_ids: + if fid in file_id_map: + new_vs_file_ids.append(file_id_map[fid]) + continue + downloaded = download_file_from_source(source_endpoint, fid, source_token) + if downloaded: + new_id = upload_file_to_target( + target_endpoint, downloaded["filename"], downloaded["content"], + purpose="assistants", target_token=target_token, + ) + if new_id: + file_id_map[fid] = new_id + new_vs_file_ids.append(new_id) + + # Create new VS on target with the uploaded files + if new_vs_file_ids: + new_vs_id = create_vector_store_on_target( + target_endpoint, new_vs_file_ids, + name=f"migrated-{vs_id}", + target_token=target_token, + ) + if new_vs_id: + vs_id_map[vs_id] = new_vs_id + + if file_id_map or vs_id_map: + print(f" ๐Ÿ“Š File migration summary: {len(file_id_map)} file(s), {len(vs_id_map)} vector store(s) remapped") + else: + print(f" โ„น๏ธ No files to migrate for this assistant") + + return {"file_id_map": file_id_map, "vs_id_map": vs_id_map} + + +def apply_file_id_remapping( + v2_agent_data: Dict[str, Any], + file_id_map: Dict[str, str], + vs_id_map: Dict[str, str], +) -> Dict[str, Any]: + """ + Rewrite file IDs and vector-store IDs in a v2 agent payload using + the maps produced by :func:`migrate_assistant_files`. + + Modifies *v2_agent_data* in place and returns it. + """ + definition = v2_agent_data.get("v2_agent_version", {}).get("definition", {}) + tools = definition.get("tools", []) + + for tool in tools: + ttype = tool.get("type") + + if ttype == "file_search" and "vector_store_ids" in tool: + tool["vector_store_ids"] = [ + vs_id_map.get(vid, vid) for vid in tool["vector_store_ids"] + ] + + if ttype == "code_interpreter" and "container" in tool: + container = tool["container"] + if "file_ids" in container: + container["file_ids"] = [ + file_id_map.get(fid, fid) for fid in container["file_ids"] + ] + + return v2_agent_data + + + """Get v1 assistant details from project endpoint using direct API calls (bypassing AIProjectClient SDK bug).""" # Since direct API calls work and AIProjectClient has issues, use direct REST API @@ -1158,11 +1558,12 @@ def get_assistant_from_project(project_endpoint: str, assistant_id: str, subscri # Remove trailing slash if present, then add the assistants path api_url = project_endpoint.rstrip('/') + f'/assistants/{assistant_id}' - # Add API version parameter - params = {"api-version": API_VERSION} + # Auto-detect the right API version for this endpoint + effective_api_version = _get_source_api_version(project_endpoint) + params = {"api-version": effective_api_version} print(f" ๐Ÿ“ž Making direct API call to: {api_url}") - print(f" ๐Ÿ”ง Using API version: {API_VERSION}") + print(f" ๐Ÿ”ง Using API version: {effective_api_version}") try: # Make the direct API request @@ -1240,11 +1641,12 @@ def list_assistants_from_project(project_endpoint: str, subscription_id: Optiona # Remove trailing slash if present, then add the assistants path api_url = project_endpoint.rstrip('/') + '/assistants' - # Add API version parameter - params = {"api-version": API_VERSION, "limit": "100"} + # Auto-detect the right API version for this endpoint + effective_api_version = _get_source_api_version(project_endpoint) + params = {"api-version": effective_api_version, "limit": "100"} print(f" ๐Ÿ“ž Making direct API call to: {api_url}") - print(f" ๐Ÿ”ง Using API version: {API_VERSION}") + print(f" ๐Ÿ”ง Using API version: {effective_api_version}") try: # Make the direct API request @@ -1338,8 +1740,8 @@ def create_agent_version_via_api(agent_name: str, agent_version_data: Dict[str, API response data """ # Build the v2 API endpoint URL based on mode (production vs local) - # Ensure agent name is lowercase for API compliance - agent_name = agent_name.lower() + # Ensure agent name satisfies the v2 API constraints before using it in the URL. + agent_name = sanitize_agent_name(agent_name) if PRODUCTION_ENDPOINT_OVERRIDE: # Direct endpoint override โ€” use as-is @@ -1396,7 +1798,7 @@ def create_agent_version_via_api(agent_name: str, agent_version_data: Dict[str, except requests.exceptions.HTTPError as e: print(f"โŒ Failed to create agent version via v2 API: {e}") - if hasattr(e, 'response') and e.response: + if hasattr(e, 'response') and e.response is not None: print(f"๐Ÿ” Response Status Code: {e.response.status_code}") try: error_response = e.response.json() @@ -1741,7 +2143,17 @@ def v1_assistant_to_v2_agent(v1_assistant: Dict[str, Any], agent_name: Optional[ # Derive agent name if not provided if not agent_name: - agent_name = v1_assistant.get("name") or f"agent_{v1_assistant.get('id', 'unknown')}" + v1_name = v1_assistant.get("name") + if v1_name: + agent_name = v1_name + else: + # Build from ID: strip common prefixes (asst_, agent_) and use hyphens + raw_id = v1_assistant.get('id', 'unknown') + # Remove leading 'asst_' prefix if present + clean_id = raw_id[5:] if raw_id.startswith('asst_') else raw_id + # Replace underscores with hyphens for v2 API compliance + clean_id = clean_id.replace('_', '-') + agent_name = f"agent-{clean_id}" # Determine the appropriate agent kind agent_kind = determine_agent_kind(v1_assistant) @@ -2227,7 +2639,7 @@ def save_v2_agent_to_cosmos(v2_agent_data: Dict[str, Any], connection_string: st print(f" Migration Doc: {migration_doc}") raise -def process_v1_assistants_to_v2_agents(args=None, assistant_id: Optional[str] = None, cosmos_connection_string: Optional[str] = None, use_api: bool = False, project_endpoint: Optional[str] = None, project_connection_string: Optional[str] = None, project_subscription: Optional[str] = None, project_resource_group: Optional[str] = None, project_name: Optional[str] = None, production_resource: Optional[str] = None, production_subscription: Optional[str] = None, production_tenant: Optional[str] = None, source_tenant: Optional[str] = None, only_with_tools: bool = False, only_without_tools: bool = False, migrate_connections: bool = False, production_endpoint: Optional[str] = None): +def process_v1_assistants_to_v2_agents(args=None, assistant_id: Optional[str] = None, cosmos_connection_string: Optional[str] = None, use_api: bool = False, project_endpoint: Optional[str] = None, project_connection_string: Optional[str] = None, project_subscription: Optional[str] = None, project_resource_group: Optional[str] = None, project_name: Optional[str] = None, production_resource: Optional[str] = None, production_subscription: Optional[str] = None, production_tenant: Optional[str] = None, source_tenant: Optional[str] = None, only_with_tools: bool = False, only_without_tools: bool = False, migrate_connections: bool = False, production_endpoint: Optional[str] = None, migrate_files: bool = True): """ Main processing function that reads v1 assistants from Cosmos DB, API, Project endpoint, or Project connection string, converts them to v2 agents, and saves via v2 API. @@ -2243,6 +2655,9 @@ def process_v1_assistants_to_v2_agents(args=None, assistant_id: Optional[str] = only_without_tools: If True, only migrate assistants that have no tools migrate_connections: If True, attempt to discover and recreate connections in target project production_endpoint: Optional full production URL (overrides production_resource URL construction) + migrate_files: If True, download files and vector stores from the source and re-upload + them via the target Foundry project endpoint so that file_search/code_interpreter references + remain valid for agents. """ # Handle package version management based on usage @@ -2688,6 +3103,39 @@ def ensure_tools_array(): # Convert v1 to v2 v2_agent = v1_assistant_to_v2_agent(v1_assistant) + # โ”€โ”€ File migration โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + # Download files / vector stores from the source endpoint + # and re-upload to the target's Foundry endpoint so that + # file_search and code_interpreter references resolve. + # NOTE: Files MUST be uploaded via the Foundry endpoint + # (*.services.ai.azure.com), NOT the OpenAI endpoint + # (*.openai.azure.com). The Foundry agent runtime only + # sees resources in the Foundry namespace. + file_id_map: Dict[str, str] = {} + vs_id_map: Dict[str, str] = {} + if migrate_files: + # Determine source endpoint (project_endpoint is set when reading from API / project) + source_ep = project_endpoint or (f"https://{HOST}/openai" if HOST else None) + target_foundry_ep = get_target_foundry_endpoint(production_resource, production_endpoint) + if source_ep and target_foundry_ep: + remap = migrate_assistant_files( + source_endpoint=source_ep, + target_endpoint=target_foundry_ep, + v1_assistant=v1_assistant, + source_token=TOKEN, + target_token=PRODUCTION_TOKEN or TOKEN, + ) + file_id_map = remap.get("file_id_map", {}) + vs_id_map = remap.get("vs_id_map", {}) + elif not source_ep: + print(" โš ๏ธ Cannot migrate files: no source endpoint available") + elif not target_foundry_ep: + print(" โš ๏ธ Cannot migrate files: unable to derive target Foundry endpoint") + + # Apply remapping to v2 agent payload + if file_id_map or vs_id_map: + v2_agent = apply_file_id_remapping(v2_agent, file_id_map, vs_id_map) + # Save to target container with proper project_id # You can customize this project_id as needed project_id = "e2e-tests-westus2-account@e2e-tests-westus2@AML" # Match existing data format @@ -2958,6 +3406,19 @@ def main(): help='Source tenant ID for reading v1 assistants. If not provided, uses SOURCE_TENANT environment variable or defaults to Microsoft tenant (72f988bf-86f1-41af-91ab-2d7cd011db47). Example: "72f988bf-86f1-41af-91ab-2d7cd011db47"' ) + # File migration is enabled by default; use --no-migrate-files to disable it. + parser.set_defaults(migrate_files=True) + parser.add_argument( + '--no-migrate-files', + dest='migrate_files', + action='store_false', + help='Skip file/vector-store migration. By default, files and vector stores are ' + 'downloaded from the source and re-uploaded via the target Foundry project ' + 'endpoint (*.services.ai.azure.com) so that file_search/code_interpreter ' + 'references remain valid. Pass this flag to copy agent definitions only ' + '(source file IDs will be broken on the target).' + ) + args = parser.parse_args() # Handle empty string as None for assistant_id @@ -3010,6 +3471,12 @@ def main(): if args.migrate_connections: print("๐Ÿ”— Connection migration: ENABLED") + if args.migrate_files: + target_foundry = get_target_foundry_endpoint(args.production_resource, args.production_endpoint) + print(f"๐Ÿ“‚ File migration: ENABLED (target Foundry endpoint: {target_foundry})") + else: + print("๐Ÿ“‚ File migration: DISABLED (--no-migrate-files)") + if assistant_id: print(f"๐ŸŽฏ Target Assistant ID: {assistant_id}") else: @@ -3048,7 +3515,8 @@ def main(): only_with_tools=args.only_with_tools, only_without_tools=args.only_without_tools, migrate_connections=args.migrate_connections, - production_endpoint=args.production_endpoint + production_endpoint=args.production_endpoint, + migrate_files=args.migrate_files, ) if __name__ == "__main__":