Skip to content

Commit 3ebc0e6

Browse files
Merge pull request #175 from microsoft/dev
feat: Enhancements to SQL Migration: Agent Management, Retry Logic, and SFI Changes
2 parents 5778a0a + 3d7d571 commit 3ebc0e6

File tree

14 files changed

+783
-299
lines changed

14 files changed

+783
-299
lines changed

infra/main.bicep

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,10 @@ module containerAppBackend 'br/public:avm/res/app/container-app:0.17.0' = {
530530
name: 'AZURE_CLIENT_ID'
531531
value: appIdentity.outputs.clientId // NOTE: This is the client ID of the managed identity, not the Entra application, and is needed for the App Service to access the Cosmos DB account.
532532
}
533+
{
534+
name: 'APP_ENV'
535+
value: 'prod'
536+
}
533537
],
534538
enableMonitoring
535539
? [
@@ -604,6 +608,10 @@ module containerAppFrontend 'br/public:avm/res/app/container-app:0.17.0' = {
604608
name: 'API_URL'
605609
value: 'https://${containerAppBackend.outputs.fqdn}'
606610
}
611+
{
612+
name: 'APP_ENV'
613+
value: 'prod'
614+
}
607615
]
608616
image: 'cmsacontainerreg.azurecr.io/cmsafrontend:${imageVersion}'
609617
name: 'cmsafrontend'

infra/main.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"_generator": {
77
"name": "bicep",
88
"version": "0.36.177.2456",
9-
"templateHash": "9967677186411813072"
9+
"templateHash": "12716753818171697569"
1010
},
1111
"name": "Modernize Your Code Solution Accelerator",
1212
"description": "CSA CTO Gold Standard Solution Accelerator for Modernize Your Code. \r\n"
@@ -41832,7 +41832,7 @@
4183241832
{
4183341833
"name": "cmsabackend",
4183441834
"image": "[format('cmsacontainerreg.azurecr.io/cmsabackend:{0}', parameters('imageVersion'))]",
41835-
"env": "[concat(createArray(createObject('name', 'COSMOSDB_ENDPOINT', 'value', reference('cosmosDb').outputs.endpoint.value), createObject('name', 'COSMOSDB_DATABASE', 'value', reference('cosmosDb').outputs.databaseName.value), createObject('name', 'COSMOSDB_BATCH_CONTAINER', 'value', reference('cosmosDb').outputs.containerNames.value.batch), createObject('name', 'COSMOSDB_FILE_CONTAINER', 'value', reference('cosmosDb').outputs.containerNames.value.file), createObject('name', 'COSMOSDB_LOG_CONTAINER', 'value', reference('cosmosDb').outputs.containerNames.value.log), createObject('name', 'AZURE_BLOB_ACCOUNT_NAME', 'value', reference('storageAccount').outputs.name.value), createObject('name', 'AZURE_BLOB_CONTAINER_NAME', 'value', variables('appStorageContainerName')), createObject('name', 'AZURE_OPENAI_ENDPOINT', 'value', format('https://{0}.openai.azure.com/', reference('aiServices').outputs.name.value)), createObject('name', 'MIGRATOR_AGENT_MODEL_DEPLOY', 'value', variables('modelDeployment').name), createObject('name', 'PICKER_AGENT_MODEL_DEPLOY', 'value', variables('modelDeployment').name), createObject('name', 'FIXER_AGENT_MODEL_DEPLOY', 'value', variables('modelDeployment').name), createObject('name', 'SEMANTIC_VERIFIER_AGENT_MODEL_DEPLOY', 'value', variables('modelDeployment').name), createObject('name', 'SYNTAX_CHECKER_AGENT_MODEL_DEPLOY', 'value', variables('modelDeployment').name), createObject('name', 'SELECTION_MODEL_DEPLOY', 'value', variables('modelDeployment').name), createObject('name', 'TERMINATION_MODEL_DEPLOY', 'value', variables('modelDeployment').name), createObject('name', 'AZURE_AI_AGENT_MODEL_DEPLOYMENT_NAME', 'value', variables('modelDeployment').name), createObject('name', 'AI_PROJECT_ENDPOINT', 'value', reference('aiServices').outputs.aiProjectInfo.value.apiEndpoint), createObject('name', 'AZURE_AI_AGENT_PROJECT_CONNECTION_STRING', 'value', reference('aiServices').outputs.aiProjectInfo.value.apiEndpoint), createObject('name', 'AZURE_AI_AGENT_PROJECT_NAME', 'value', reference('aiServices').outputs.aiProjectInfo.value.name), createObject('name', 'AZURE_AI_AGENT_RESOURCE_GROUP_NAME', 'value', resourceGroup().name), createObject('name', 'AZURE_AI_AGENT_SUBSCRIPTION_ID', 'value', subscription().subscriptionId), createObject('name', 'AZURE_AI_AGENT_ENDPOINT', 'value', reference('aiServices').outputs.aiProjectInfo.value.apiEndpoint), createObject('name', 'AZURE_CLIENT_ID', 'value', reference('appIdentity').outputs.clientId.value)), if(parameters('enableMonitoring'), createArray(createObject('name', 'APPLICATIONINSIGHTS_INSTRUMENTATION_KEY', 'value', reference('applicationInsights').outputs.instrumentationKey.value), createObject('name', 'APPLICATIONINSIGHTS_CONNECTION_STRING', 'value', reference('applicationInsights').outputs.connectionString.value)), createArray()))]",
41835+
"env": "[concat(createArray(createObject('name', 'COSMOSDB_ENDPOINT', 'value', reference('cosmosDb').outputs.endpoint.value), createObject('name', 'COSMOSDB_DATABASE', 'value', reference('cosmosDb').outputs.databaseName.value), createObject('name', 'COSMOSDB_BATCH_CONTAINER', 'value', reference('cosmosDb').outputs.containerNames.value.batch), createObject('name', 'COSMOSDB_FILE_CONTAINER', 'value', reference('cosmosDb').outputs.containerNames.value.file), createObject('name', 'COSMOSDB_LOG_CONTAINER', 'value', reference('cosmosDb').outputs.containerNames.value.log), createObject('name', 'AZURE_BLOB_ACCOUNT_NAME', 'value', reference('storageAccount').outputs.name.value), createObject('name', 'AZURE_BLOB_CONTAINER_NAME', 'value', variables('appStorageContainerName')), createObject('name', 'AZURE_OPENAI_ENDPOINT', 'value', format('https://{0}.openai.azure.com/', reference('aiServices').outputs.name.value)), createObject('name', 'MIGRATOR_AGENT_MODEL_DEPLOY', 'value', variables('modelDeployment').name), createObject('name', 'PICKER_AGENT_MODEL_DEPLOY', 'value', variables('modelDeployment').name), createObject('name', 'FIXER_AGENT_MODEL_DEPLOY', 'value', variables('modelDeployment').name), createObject('name', 'SEMANTIC_VERIFIER_AGENT_MODEL_DEPLOY', 'value', variables('modelDeployment').name), createObject('name', 'SYNTAX_CHECKER_AGENT_MODEL_DEPLOY', 'value', variables('modelDeployment').name), createObject('name', 'SELECTION_MODEL_DEPLOY', 'value', variables('modelDeployment').name), createObject('name', 'TERMINATION_MODEL_DEPLOY', 'value', variables('modelDeployment').name), createObject('name', 'AZURE_AI_AGENT_MODEL_DEPLOYMENT_NAME', 'value', variables('modelDeployment').name), createObject('name', 'AI_PROJECT_ENDPOINT', 'value', reference('aiServices').outputs.aiProjectInfo.value.apiEndpoint), createObject('name', 'AZURE_AI_AGENT_PROJECT_CONNECTION_STRING', 'value', reference('aiServices').outputs.aiProjectInfo.value.apiEndpoint), createObject('name', 'AZURE_AI_AGENT_PROJECT_NAME', 'value', reference('aiServices').outputs.aiProjectInfo.value.name), createObject('name', 'AZURE_AI_AGENT_RESOURCE_GROUP_NAME', 'value', resourceGroup().name), createObject('name', 'AZURE_AI_AGENT_SUBSCRIPTION_ID', 'value', subscription().subscriptionId), createObject('name', 'AZURE_AI_AGENT_ENDPOINT', 'value', reference('aiServices').outputs.aiProjectInfo.value.apiEndpoint), createObject('name', 'AZURE_CLIENT_ID', 'value', reference('appIdentity').outputs.clientId.value), createObject('name', 'APP_ENV', 'value', 'prod')), if(parameters('enableMonitoring'), createArray(createObject('name', 'APPLICATIONINSIGHTS_INSTRUMENTATION_KEY', 'value', reference('applicationInsights').outputs.instrumentationKey.value), createObject('name', 'APPLICATIONINSIGHTS_CONNECTION_STRING', 'value', reference('applicationInsights').outputs.connectionString.value)), createArray()))]",
4183641836
"resources": {
4183741837
"cpu": 1,
4183841838
"memory": "2.0Gi"
@@ -43334,6 +43334,10 @@
4333443334
{
4333543335
"name": "API_URL",
4333643336
"value": "[format('https://{0}', reference('containerAppBackend').outputs.fqdn.value)]"
43337+
},
43338+
{
43339+
"name": "APP_ENV",
43340+
"value": "prod"
4333743341
}
4333843342
],
4333943343
"image": "[format('cmsacontainerreg.azurecr.io/cmsafrontend:{0}', parameters('imageVersion'))]",

src/backend/.env.sample

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,6 @@ AZURE_AI_AGENT_PROJECT_CONNECTION_STRING = ""
2323
AZURE_AI_AGENT_SUBSCRIPTION_ID = ""
2424
AZURE_AI_AGENT_RESOURCE_GROUP_NAME = ""
2525
AZURE_AI_AGENT_PROJECT_NAME = ""
26-
AZURE_AI_AGENT_MODEL_DEPLOYMENT_NAME = ""
26+
AZURE_AI_AGENT_MODEL_DEPLOYMENT_NAME = ""
27+
28+
APP_ENV = "dev"

src/backend/app.py

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,24 @@
11
"""Create and configure the FastAPI application."""
2+
from contextlib import asynccontextmanager
3+
24
from api.api_routes import router as backend_router
35

6+
from common.config.config import app_config
47
from common.logger.app_logger import AppLogger
58

69
from dotenv import load_dotenv
710

811
from fastapi import FastAPI
912
from fastapi.middleware.cors import CORSMiddleware
1013

14+
from helper.azure_credential_utils import get_azure_credential
15+
16+
from semantic_kernel.agents.azure_ai.azure_ai_agent import AzureAIAgent # pylint: disable=E0611
17+
18+
from sql_agents.agent_manager import clear_sql_agents, set_sql_agents
19+
from sql_agents.agents.agent_config import AgentBaseConfig
20+
from sql_agents.helpers.agents_manager import SqlAgents
21+
1122
import uvicorn
1223
# from agent_services.agents_routes import router as agents_router
1324

@@ -17,10 +28,67 @@
1728
# Configure logging
1829
logger = AppLogger("app")
1930

31+
# Global variables for agents
32+
sql_agents: SqlAgents = None
33+
azure_client = None
34+
35+
36+
@asynccontextmanager
37+
async def lifespan(app: FastAPI):
38+
"""Manage application lifespan - startup and shutdown."""
39+
global sql_agents, azure_client
40+
41+
# Startup
42+
try:
43+
logger.logger.info("Initializing SQL agents...")
44+
45+
# Create Azure credentials and client
46+
creds = get_azure_credential(app_config.azure_client_id)
47+
azure_client = AzureAIAgent.create_client(
48+
credential=creds,
49+
endpoint=app_config.ai_project_endpoint
50+
)
51+
52+
# Setup agent configuration with default conversion settings
53+
agent_config = AgentBaseConfig(
54+
project_client=azure_client,
55+
sql_from="informix", # Default source dialect
56+
sql_to="tsql" # Default target dialect
57+
)
58+
59+
# Create SQL agents
60+
sql_agents = await SqlAgents.create(agent_config)
61+
62+
# Set the global agents instance
63+
set_sql_agents(sql_agents)
64+
logger.logger.info("SQL agents initialized successfully.")
65+
66+
except Exception as exc:
67+
logger.logger.error("Failed to initialize SQL agents: %s", exc)
68+
# Don't raise the exception to allow the app to start even if agents fail
69+
70+
yield # Application runs here
71+
72+
# Shutdown
73+
try:
74+
if sql_agents:
75+
logger.logger.info("Application shutting down - cleaning up SQL agents...")
76+
await sql_agents.delete_agents()
77+
logger.logger.info("SQL agents cleaned up successfully.")
78+
79+
# Clear the global agents instance
80+
await clear_sql_agents()
81+
82+
if azure_client:
83+
await azure_client.close()
84+
85+
except Exception as exc:
86+
logger.logger.error("Error during agent cleanup: %s", exc)
87+
2088

2189
def create_app() -> FastAPI:
2290
"""Create and return the FastAPI application instance."""
23-
app = FastAPI(title="Code Gen Accelerator", version="1.0.0")
91+
app = FastAPI(title="Code Gen Accelerator", version="1.0.0", lifespan=lifespan)
2492

2593
# Configure CORS
2694
app.add_middleware(

src/backend/common/config/config.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414

1515
import os
1616

17-
from azure.identity.aio import ClientSecretCredential, DefaultAzureCredential
17+
from azure.identity.aio import ClientSecretCredential
18+
19+
from helper.azure_credential_utils import get_azure_credential
1820

1921

2022
class Config:
@@ -49,7 +51,7 @@ def __init__(self):
4951
"SYNTAX_CHECKER_AGENT_MODEL_DEPLOY"
5052
)
5153

52-
self.__azure_credentials = DefaultAzureCredential()
54+
self.__azure_credentials = get_azure_credential(self.azure_client_id)
5355

5456
def get_azure_credentials(self):
5557
"""Retrieve Azure credentials, either from environment variables or managed identity."""

src/backend/common/services/batch_service.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,14 @@ async def get_file_report(self, file_id: str) -> Optional[Dict]:
6161
storage = await BlobStorageFactory.get_storage()
6262
if file_record.translated_path not in ["", None]:
6363
translated_content = await storage.get_file(file_record.translated_path)
64+
else:
65+
# If translated_path is empty, try to get translated content from logs
66+
# Look for the final success log with the translated result
67+
if logs:
68+
for log in logs:
69+
if (log.get("log_type") == "success" and log.get("agent_type") == "agents" and log.get("last_candidate")):
70+
translated_content = log["last_candidate"]
71+
break
6472
except IOError as e:
6573
self.logger.error(f"Error downloading file content: {str(e)}")
6674

@@ -79,6 +87,14 @@ async def get_file_translated(self, file: dict):
7987
storage = await BlobStorageFactory.get_storage()
8088
if file["translated_path"] not in ["", None]:
8189
translated_content = await storage.get_file(file["translated_path"])
90+
else:
91+
# If translated_path is empty, try to get translated content from logs
92+
# Look for the final success log with the translated result
93+
if "logs" in file and file["logs"]:
94+
for log in file["logs"]:
95+
if (log.get("log_type") == "success" and log.get("agent_type") == "agents" and log.get("last_candidate")):
96+
translated_content = log["last_candidate"]
97+
break
8298
except IOError as e:
8399
self.logger.error(f"Error downloading file content: {str(e)}")
84100

@@ -126,19 +142,22 @@ async def get_batch_summary(self, batch_id: str, user_id: str) -> Optional[Dict]
126142
try:
127143
logs = await self.database.get_file_logs(file["file_id"])
128144
file["logs"] = logs
129-
if file["translated_path"]:
130-
try:
131-
translated_content = await self.get_file_translated(file)
132-
file["translated_content"] = translated_content
133-
except Exception as e:
134-
self.logger.error(
135-
f"Error retrieving translated content for file {file['file_id']}: {str(e)}"
136-
)
145+
# Try to get translated content for all files, but prioritize completed files
146+
try:
147+
translated_content = await self.get_file_translated(file)
148+
file["translated_content"] = translated_content
149+
except Exception as e:
150+
self.logger.error(
151+
f"Error retrieving translated content for file {file['file_id']}: {str(e)}"
152+
)
153+
# Ensure translated_content field exists even if empty
154+
file["translated_content"] = ""
137155
except Exception as e:
138156
self.logger.error(
139157
f"Error retrieving logs for file {file['file_id']}: {str(e)}"
140158
)
141159
file["logs"] = [] # Set empty logs on error
160+
file["translated_content"] = "" # Ensure field exists
142161

143162
return {
144163
"files": files,

src/backend/common/storage/blob_azure.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
from typing import Any, BinaryIO, Dict, Optional
22

3-
from azure.identity import DefaultAzureCredential
43
from azure.storage.blob import BlobServiceClient
54

5+
from common.config.config import app_config
66
from common.logger.app_logger import AppLogger
77
from common.storage.blob_base import BlobStorageBase
88

9+
from helper.azure_credential_utils import get_azure_credential
10+
911

1012
class AzureBlobStorage(BlobStorageBase):
1113
def __init__(self, account_name: str, container_name: Optional[str] = None):
@@ -15,7 +17,7 @@ def __init__(self, account_name: str, container_name: Optional[str] = None):
1517
self.container_name = container_name
1618
self.service_client = None
1719
self.container_client = None
18-
credential = DefaultAzureCredential() # Using Entra Authentication
20+
credential = get_azure_credential(app_config.azure_client_id) # Using Entra Authentication
1921
self.service_client = BlobServiceClient(
2022
account_url=f"https://{self.account_name}.blob.core.windows.net/",
2123
credential=credential,
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import os
2+
3+
from azure.identity import DefaultAzureCredential, ManagedIdentityCredential
4+
from azure.identity.aio import DefaultAzureCredential as AioDefaultAzureCredential, ManagedIdentityCredential as AioManagedIdentityCredential
5+
6+
7+
async def get_azure_credential_async(client_id=None):
8+
"""
9+
Returns an Azure credential asynchronously based on the application environment.
10+
If the environment is 'dev', it uses AioDefaultAzureCredential.
11+
Otherwise, it uses AioManagedIdentityCredential.
12+
Args:
13+
client_id (str, optional): The client ID for the Managed Identity Credential.
14+
Returns:
15+
Credential object: Either AioDefaultAzureCredential or AioManagedIdentityCredential.
16+
"""
17+
if os.getenv("APP_ENV", "prod").lower() == 'dev':
18+
return AioDefaultAzureCredential() # CodeQL [SM05139] Okay use of DefaultAzureCredential as it is only used in development
19+
else:
20+
return AioManagedIdentityCredential(client_id=client_id)
21+
22+
23+
def get_azure_credential(client_id=None):
24+
"""
25+
Returns an Azure credential based on the application environment.
26+
If the environment is 'dev', it uses DefaultAzureCredential.
27+
Otherwise, it uses ManagedIdentityCredential.
28+
Args:
29+
client_id (str, optional): The client ID for the Managed Identity Credential.
30+
Returns:
31+
Credential object: Either DefaultAzureCredential or ManagedIdentityCredential.
32+
"""
33+
if os.getenv("APP_ENV", "prod").lower() == 'dev':
34+
return DefaultAzureCredential() # CodeQL [SM05139] Okay use of DefaultAzureCredential as it is only used in development
35+
else:
36+
return ManagedIdentityCredential(client_id=client_id)
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"""
2+
Global agent manager for SQL agents.
3+
This module manages the global SQL agents instance to avoid circular imports.
4+
"""
5+
6+
import logging
7+
from typing import Optional
8+
9+
from sql_agents.helpers.agents_manager import SqlAgents
10+
11+
logger = logging.getLogger(__name__)
12+
13+
# Global variable to store the SQL agents instance
14+
_sql_agents: Optional[SqlAgents] = None
15+
16+
17+
def set_sql_agents(agents: SqlAgents) -> None:
18+
"""Set the global SQL agents instance."""
19+
global _sql_agents
20+
_sql_agents = agents
21+
logger.info("Global SQL agents instance has been set")
22+
23+
24+
def get_sql_agents() -> Optional[SqlAgents]:
25+
"""Get the global SQL agents instance."""
26+
return _sql_agents
27+
28+
29+
async def update_agent_config(convert_from: str, convert_to: str) -> None:
30+
"""Update the global agent configuration for different SQL conversion types."""
31+
if _sql_agents and _sql_agents.agent_config:
32+
_sql_agents.agent_config.sql_from = convert_from
33+
_sql_agents.agent_config.sql_to = convert_to
34+
logger.info(f"Updated agent configuration: {convert_from} -> {convert_to}")
35+
else:
36+
logger.warning("SQL agents not initialized, cannot update configuration")
37+
38+
39+
async def clear_sql_agents() -> None:
40+
"""Clear the global SQL agents instance."""
41+
global _sql_agents
42+
await _sql_agents.delete_agents()
43+
_sql_agents = None
44+
logger.info("Global SQL agents instance has been cleared")

0 commit comments

Comments
 (0)