diff --git a/infra/main.bicep b/infra/main.bicep index eb42f13..f90ba0a 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -32,7 +32,7 @@ var solutionLocation = empty(location) ? resourceGroup().location : location azd: { type: 'location' usageName: [ - 'OpenAI.GlobalStandard.GPT5.1, 500' + 'OpenAI.GlobalStandard.gpt-5.1, 500' ] } }) @@ -69,11 +69,11 @@ param aiDeploymentType string = 'GlobalStandard' @minLength(1) @description('Optional. Name of the AI model to deploy. Recommend using GPT5.1. Defaults to GPT5.1.') -param aiModelName string = 'GPT5.1' +param aiModelName string = 'gpt-5.1' @minLength(1) @description('Optional. Version of AI model. Review available version numbers per model before setting. Defaults to 2025-04-16.') -param aiModelVersion string = '2025-04-16' +param aiModelVersion string = '2025-11-13' @description('Optional. AI model deployment token capacity. Lower this if initial provisioning fails due to capacity. Defaults to 50K tokens per minute to improve regional success rate.') param aiModelCapacity int = 500 @@ -899,7 +899,7 @@ module appConfiguration 'br/public:avm/res/app-configuration/configuration-store } { name: 'AZURE_OPENAI_API_VERSION' - value: '2025-01-01-preview' + value: '2025-03-01-preview' } { name: 'AZURE_OPENAI_CHAT_DEPLOYMENT_NAME' @@ -1065,6 +1065,7 @@ module containerAppsEnvironment 'br/public:avm/res/app/managed-environment:0.11. var backendContainerPort = 80 var backendContainerAppName = take('ca-backend-api-${solutionSuffix}', 32) +var processorContainerAppName = take('ca-processor-${solutionSuffix}', 32) module containerAppBackend 'br/public:avm/res/app/container-app:0.18.1' = { name: take('avm.res.app.container-app.${backendContainerAppName}', 64) #disable-next-line no-unnecessary-dependson @@ -1092,6 +1093,11 @@ module containerAppBackend 'br/public:avm/res/app/container-app:0.18.1' = { name: 'AZURE_CLIENT_ID' value: appIdentity.outputs.clientId } + { + name: 'PROCESSOR_CONTROL_URL' + // Internal ingress FQDN format: https://.internal. + value: 'https://${processorContainerAppName}.internal.${containerAppsEnvironment.outputs.defaultDomain}' + } ], enableMonitoring ? [ @@ -1211,7 +1217,6 @@ module containerAppFrontend 'br/public:avm/res/app/container-app:0.18.1' = { } } -var processorContainerAppName = take('ca-processor-${solutionSuffix}', 32) module containerAppProcessor 'br/public:avm/res/app/container-app:0.18.1' = { name: take('avm.res.app.container-app.${processorContainerAppName}', 64) #disable-next-line no-unnecessary-dependson @@ -1247,6 +1252,14 @@ module containerAppProcessor 'br/public:avm/res/app/container-app:0.18.1' = { name: 'STORAGE_ACCOUNT_NAME' // TODO - verify name and if needed value: storageAccount.outputs.name } + { + name: 'CONTROL_API_ENABLED' + value: '1' + } + { + name: 'CONTROL_API_PORT' + value: '8080' + } ], enableMonitoring ? [ @@ -1264,9 +1277,10 @@ module containerAppProcessor 'br/public:avm/res/app/container-app:0.18.1' = { } } ] - ingressTransport: null - disableIngress: true + // Internal ingress required for container-to-container communication + ingressTargetPort: 8080 ingressExternal: false + ingressAllowInsecure: true // Allow HTTP without SSL redirect for internal calls scaleSettings: { maxReplicas: enableScalability ? 3 : 1 minReplicas: 1 diff --git a/infra/main_custom.bicep b/infra/main_custom.bicep index 4597478..8b9f9cd 100644 --- a/infra/main_custom.bicep +++ b/infra/main_custom.bicep @@ -1036,6 +1036,7 @@ module containerAppsEnvironment 'br/public:avm/res/app/managed-environment:0.11. var backendContainerPort = 80 var backendContainerAppName = take('ca-backend-api-${solutionSuffix}', 32) +var processorContainerAppName = take('ca-processor-${solutionSuffix}', 32) module containerAppBackend 'br/public:avm/res/app/container-app:0.18.1' = { name: take('avm.res.app.container-app.${backendContainerAppName}', 64) #disable-next-line no-unnecessary-dependson @@ -1070,6 +1071,11 @@ module containerAppBackend 'br/public:avm/res/app/container-app:0.18.1' = { name: 'AZURE_CLIENT_ID' value: appIdentity.outputs.clientId } + { + name: 'PROCESSOR_CONTROL_URL' + // Internal ingress FQDN format: https://.internal. + value: 'https://${processorContainerAppName}.internal.${containerAppsEnvironment.outputs.defaultDomain}' + } ], enableMonitoring ? [ @@ -1186,7 +1192,6 @@ module containerAppFrontend 'br/public:avm/res/app/container-app:0.18.1' = { } } -var processorContainerAppName = take('ca-processor-${solutionSuffix}', 32) module containerAppProcessor 'br/public:avm/res/app/container-app:0.18.1' = { name: take('avm.res.app.container-app.${processorContainerAppName}', 64) #disable-next-line no-unnecessary-dependson @@ -1229,6 +1234,14 @@ module containerAppProcessor 'br/public:avm/res/app/container-app:0.18.1' = { name: 'STORAGE_ACCOUNT_NAME' // TODO - verify name and if needed value: storageAccount.outputs.name } + { + name: 'CONTROL_API_ENABLED' + value: '1' + } + { + name: 'CONTROL_API_PORT' + value: '8080' + } ], enableMonitoring ? [ @@ -1246,9 +1259,10 @@ module containerAppProcessor 'br/public:avm/res/app/container-app:0.18.1' = { } } ] - ingressTransport: null - disableIngress: true + // Internal ingress required for container-to-container communication + ingressTargetPort: 8080 ingressExternal: false + ingressAllowInsecure: true // Allow HTTP without SSL redirect for internal calls scaleSettings: { maxReplicas: enableScalability ? 3 : 1 minReplicas: 1 diff --git a/src/backend-api/src/app/libs/application/application_configuration.py b/src/backend-api/src/app/libs/application/application_configuration.py index bb8d9b8..295e37f 100644 --- a/src/backend-api/src/app/libs/application/application_configuration.py +++ b/src/backend-api/src/app/libs/application/application_configuration.py @@ -69,6 +69,16 @@ class Configuration(_configuration_base, KernelBaseSettings): default=None, env="APPLICATIONINSIGHTS_CONNECTION_STRING" ) + # Processor Control API configuration + # In Azure Container Apps, apps call each other by name: http:// + # The actual URL is set via PROCESSOR_CONTROL_URL env var from Bicep + processor_control_url: str | None = Field( + default="http://localhost:8080", env="PROCESSOR_CONTROL_URL" + ) + processor_control_token: str | None = Field( + default=None, env="PROCESSOR_CONTROL_TOKEN" + ) + class _envConfiguration(_configuration_base): """ diff --git a/src/backend-api/src/app/routers/router_process.py b/src/backend-api/src/app/routers/router_process.py index 27d378c..9851a26 100644 --- a/src/backend-api/src/app/routers/router_process.py +++ b/src/backend-api/src/app/routers/router_process.py @@ -1,9 +1,10 @@ import io import zipfile from enum import Enum -from typing import List +from typing import List, Optional from uuid import uuid4 +import httpx from fastapi import APIRouter, File, Form, HTTPException, Request, Response, UploadFile from fastapi.responses import JSONResponse, StreamingResponse from libs.base.typed_fastapi import TypedFastAPI @@ -37,6 +38,8 @@ class process_router_paths(str, Enum): START_PROCESSING = "/start-processing" DELETE_FILE = "/delete-file/{file_name}" DELETE_PROCESS = "/delete-process/{process_id}" + CANCEL_PROCESS = "/cancel/{process_id}" + CANCEL_STATUS = "/cancel/{process_id}/status" STATUS = "/status/{process_id}/" RENDER_STATUS = "/status/{process_id}/render/" PROCESS_AGENT_ACTIVITIES = "/status/{process_id}/activities" @@ -578,3 +581,188 @@ async def get_file_content( raise HTTPException( status_code=500, detail=f"Error retrieving file content: {str(e)}" ) + + +@router.post(process_router_paths.CANCEL_PROCESS, status_code=202) +async def cancel_process( + process_id: str, + request: Request, + reason: Optional[str] = None, +): + """ + Request cancellation of a running process. + This endpoint forwards the kill request to the Processor's Control API. + The processor will observe this request and terminate the running process. + """ + app: TypedFastAPI = request.app + logger_service: ILoggerService = app.app_context.get_service(ILoggerService) + + try: + logger_service.log_info(f"Cancel process request for process_id: {process_id}") + + # Get authenticated user + authenticated_user = get_authenticated_user(request) + user_id = authenticated_user.user_principal_id + + if not user_id: + raise HTTPException(status_code=401, detail="User not authenticated") + + # Get processor control URL from configuration + config = app.app_context.configuration + processor_url = config.processor_control_url or "http://processor:8080" + processor_token = config.processor_control_token or "" + + # Prepare headers for processor control API + headers = {} + if processor_token: + headers["Authorization"] = f"Bearer {processor_token}" + + # Build the full URL for the kill endpoint + kill_url = f"{processor_url}/processes/{process_id}/kill" + logger_service.log_info(f"Calling processor kill API at: {kill_url}") + + # Forward kill request to Processor Control API + # Note: verify=False is needed for internal ACA communication (self-signed certs) + async with httpx.AsyncClient(timeout=30.0, verify=False) as client: + response = await client.post( + kill_url, + json={"reason": reason or f"User {user_id} cancelled from UI"}, + headers=headers, + ) + logger_service.log_info(f"Processor kill API response: {response.status_code}") + + if response.status_code == 401: + logger_service.log_error("Unauthorized access to processor control API") + raise HTTPException( + status_code=502, + detail="Failed to authenticate with processor control API", + ) + + if response.status_code >= 400: + logger_service.log_error( + f"Processor control API error: {response.status_code} - {response.text}" + ) + raise HTTPException( + status_code=502, + detail=f"Processor control API error: {response.text}", + ) + + result = response.json() + + logger_service.log_info( + f"Cancel request sent for process {process_id}, state: {result.get('kill_state', 'unknown')}" + ) + + return { + "message": "Cancellation request submitted", + "process_id": process_id, + "kill_requested": result.get("kill_requested", True), + "kill_state": result.get("kill_state", "pending"), + "kill_requested_at": result.get("kill_requested_at", ""), + } + + except httpx.TimeoutException: + logger_service.log_error(f"Timeout connecting to processor control API") + raise HTTPException( + status_code=504, + detail="Timeout connecting to processor control API", + ) + except httpx.ConnectError: + logger_service.log_error(f"Failed to connect to processor control API") + raise HTTPException( + status_code=503, + detail="Processor control API is unavailable", + ) + except HTTPException: + raise + except Exception as e: + logger_service.log_error(f"Error in cancel_process: {str(e)}") + raise HTTPException( + status_code=500, detail=f"Error cancelling process: {str(e)}" + ) + + +@router.get(process_router_paths.CANCEL_STATUS, status_code=200) +async def get_cancel_status( + process_id: str, + request: Request, +): + """ + Get the cancellation status of a process. + Returns the current kill state from the Processor's Control API. + """ + app: TypedFastAPI = request.app + logger_service: ILoggerService = app.app_context.get_service(ILoggerService) + + try: + logger_service.log_info(f"Get cancel status for process_id: {process_id}") + + # Get authenticated user + authenticated_user = get_authenticated_user(request) + user_id = authenticated_user.user_principal_id + + if not user_id: + raise HTTPException(status_code=401, detail="User not authenticated") + + # Get processor control URL from configuration + config = app.app_context.configuration + processor_url = config.processor_control_url or "http://processor:8080" + processor_token = config.processor_control_token or "" + + # Prepare headers for processor control API + headers = {} + if processor_token: + headers["Authorization"] = f"Bearer {processor_token}" + + # Build the full URL for the control status endpoint + control_url = f"{processor_url}/processes/{process_id}/control" + logger_service.log_info(f"Calling processor control API at: {control_url}") + + # Get control status from Processor Control API + # Note: verify=False is needed for internal ACA communication (self-signed certs) + async with httpx.AsyncClient(timeout=30.0, verify=False) as client: + response = await client.get( + control_url, + headers=headers, + ) + logger_service.log_info(f"Processor control API response: {response.status_code}") + + if response.status_code == 401: + logger_service.log_error("Unauthorized access to processor control API") + raise HTTPException( + status_code=502, + detail="Failed to authenticate with processor control API", + ) + + if response.status_code >= 400: + logger_service.log_error( + f"Processor control API error: {response.status_code} - {response.text}" + ) + raise HTTPException( + status_code=502, + detail=f"Processor control API error: {response.text}", + ) + + result = response.json() + + return result + + except httpx.TimeoutException: + logger_service.log_error(f"Timeout connecting to processor control API") + raise HTTPException( + status_code=504, + detail="Timeout connecting to processor control API", + ) + except httpx.ConnectError: + logger_service.log_error(f"Failed to connect to processor control API") + raise HTTPException( + status_code=503, + detail="Processor control API is unavailable", + ) + except HTTPException: + raise + except Exception as e: + logger_service.log_error(f"Error in get_cancel_status: {str(e)}") + raise HTTPException( + status_code=500, detail=f"Error getting cancel status: {str(e)}" + ) diff --git a/src/frontend/src/commonComponents/ProgressModal/progressModal.tsx b/src/frontend/src/commonComponents/ProgressModal/progressModal.tsx index 3abaa6b..5541acd 100644 --- a/src/frontend/src/commonComponents/ProgressModal/progressModal.tsx +++ b/src/frontend/src/commonComponents/ProgressModal/progressModal.tsx @@ -66,10 +66,15 @@ const ProgressModal: React.FC = ({ setOpen(false); }; - const handleCancel = () => { - // Trigger onCancel (navigate to landing page) and close modal + const handleCancel = async () => { + console.log('=== ProgressModal handleCancel called ==='); + // Trigger onCancel (calls cancel API and navigates to landing page) and close modal if (onCancel) { - onCancel(); + console.log('Calling onCancel callback...'); + await onCancel(); + console.log('onCancel callback completed'); + } else { + console.warn('No onCancel callback provided'); } setOpen(false); }; diff --git a/src/frontend/src/pages/processPage.tsx b/src/frontend/src/pages/processPage.tsx index 6869182..a2ae05a 100644 --- a/src/frontend/src/pages/processPage.tsx +++ b/src/frontend/src/pages/processPage.tsx @@ -118,6 +118,8 @@ const ProcessPage: React.FC = () => { const [lastUpdateTime, setLastUpdateTime] = useState(""); const [processingCompleted, setProcessingCompleted] = useState(false); const stepsContainerRef = useRef(null); + // Track the last seen phase to prevent duplicate phase messages + const [lastSeenPhase, setLastSeenPhase] = useState(""); // Progress modal state const [showProgressModal, setShowProgressModal] = useState(false); @@ -126,13 +128,30 @@ const ProcessPage: React.FC = () => { // Error state management const [migrationError, setMigrationError] = useState(false); + // Helper function to clean phase name - removes "PHASE X - " prefix + const cleanPhaseName = (phase: string): string => { + if (!phase) return ""; + // Remove "PHASE X - " prefix (e.g., "PHASE 3 - SOURCE PLATFORM REVIEW" -> "SOURCE PLATFORM REVIEW") + return phase.replace(/^PHASE\s*\d+\s*-\s*/i, '').trim(); + }; + // Helper function to generate phase message from API data - const getPhaseMessage = (apiResponse: any) => { + // Returns message with agent activity but without phase number prefixes + const getPhaseMessage = (apiResponse: any): string => { if (!apiResponse) return ""; - const { phase, active_agent_count, total_agents, health_status, agents } = apiResponse; + const { phase, active_agent_count, total_agents, agents } = apiResponse; + + // Clean the phase name to remove "PHASE X - " prefix + const cleanedPhase = cleanPhaseName(phase); - const phaseMessages = { + // Handle unknown/start phases with friendly initialization message + const initializationPhases = ['unknown', 'start', '', undefined, null]; + if (initializationPhases.includes(phase?.toLowerCase()) || initializationPhases.includes(cleanedPhase?.toLowerCase())) { + return 'Initialization phase in progress'; + } + + const phaseMessages: Record = { 'Analysis': 'Analyzing workloads and dependencies, existing container images and configurations', 'Design': 'Designing target environment mappings to align with Azure AKS', 'YAML': 'Converting container specifications and orchestration configs to Azure format', @@ -140,12 +159,12 @@ const ProcessPage: React.FC = () => { }; // Extract active agent information from agents array - const activeAgents = agents?.filter(agent => + const activeAgents = agents?.filter((agent: string) => agent.includes('speaking') || agent.includes('thinking') ) || []; - const speakingAgent = activeAgents.find(agent => agent.includes('speaking')); - const thinkingAgent = activeAgents.find(agent => agent.includes('thinking')); + const speakingAgent = activeAgents.find((agent: string) => agent.includes('speaking')); + const thinkingAgent = activeAgents.find((agent: string) => agent.includes('thinking')); let agentActivity = ""; if (speakingAgent) { @@ -156,11 +175,12 @@ const ProcessPage: React.FC = () => { agentActivity = ` - ${agentName} is thinking`; } - const baseMessage = phaseMessages[phase] || `${phase} phase in progress`; + // Use predefined message if available, otherwise use cleaned phase name + const baseMessage = phaseMessages[phase] || phaseMessages[cleanedPhase] || `${cleanedPhase} phase in progress`; const agentInfo = active_agent_count && total_agents ? ` (${active_agent_count}/${total_agents} agents active)` : ''; - const healthIcon = health_status?.includes('🟢') ? ' 🟢' : ''; - return `${phase} phase: ${baseMessage}${agentActivity}${agentInfo}`; + // Return message without phase number prefix + return `${baseMessage}${agentActivity}${agentInfo}`; }; // Polling function to check batch status @@ -185,33 +205,34 @@ const ProcessPage: React.FC = () => { setShowProgressModal(true); } - // Check if last_update_time has changed - only update if there's new activity - if (response.last_update_time && response.last_update_time !== lastUpdateTime) { - console.log('New activity detected! Last update time changed from', lastUpdateTime, 'to', response.last_update_time); - - // Update the stored last update time + // Update the stored last update time + if (response.last_update_time) { setLastUpdateTime(response.last_update_time); + } - // Update current phase and generate step message - if (response.phase) { - const newPhaseMessage = getPhaseMessage(response); - - // Add the new message to steps ONLY if it's different from the last message - setCurrentPhase(response.phase); - setPhaseSteps(prev => { - // Check if the new message is different from the last message - const lastMessage = prev[prev.length - 1]; - if (lastMessage !== newPhaseMessage) { - console.log('Adding new unique message:', newPhaseMessage); - return [...prev, newPhaseMessage]; - } else { - console.log('Skipping duplicate message even though timestamp changed:', newPhaseMessage); - return prev; - } - }); - } - } else if (response.last_update_time) { - console.log('No new activity - last update time unchanged:', response.last_update_time); + // Update current phase - only add a new message when the phase actually changes + // This prevents duplicate messages from agent activity changes within the same phase + if (response.phase && response.phase !== lastSeenPhase) { + console.log('Phase transition detected:', lastSeenPhase, '->', response.phase); + + const newPhaseMessage = getPhaseMessage(response); + setCurrentPhase(response.phase); + setLastSeenPhase(response.phase); + + // Add the phase message only on phase transition + setPhaseSteps(prev => { + // Double-check to avoid any duplicate messages + if (!prev.includes(newPhaseMessage)) { + console.log('Adding new phase message:', newPhaseMessage); + return [...prev, newPhaseMessage]; + } + console.log('Skipping duplicate phase message:', newPhaseMessage); + return prev; + }); + } else if (response.phase) { + // Phase unchanged, just update current phase state for display + setCurrentPhase(response.phase); + console.log('Same phase, no new message added:', response.phase); } // Check for completion and navigate to batch-view @@ -292,13 +313,77 @@ const ProcessPage: React.FC = () => { */ // Handle modal cancellation - const handleCancelProcessing = () => { - // TODO: Add API call to cancel processing if needed - console.log('User cancelled processing'); - setShowProgressModal(false); - setProcessingState('IDLE'); - // Optionally navigate back to landing or previous page - navigate('/'); + const handleCancelProcessing = async () => { + console.log('=== handleCancelProcessing called ==='); + console.log('batchId:', batchId); + + // Call API to cancel the process + if (batchId) { + try { + console.log('Calling apiService.cancelProcess with batchId:', batchId); + const result = await apiService.cancelProcess(batchId, 'User cancelled from UI'); + console.log('Cancel request result:', result); + + // Poll for cancel status every 3 seconds until kill_state is "executed" + const pollCancelStatus = async () => { + const maxAttempts = 60; // Max 3 minutes of polling (60 * 3 seconds) + let attempts = 0; + + const poll = async (): Promise => { + attempts++; + console.log(`Polling cancel status, attempt ${attempts}...`); + + try { + const status = await apiService.getCancelStatus(batchId); + console.log('Cancel status response:', status); + + if (status.kill_state === 'executed') { + console.log('Process cancellation completed successfully'); + return; + } + + if (status.kill_state === 'failed') { + console.error('Process cancellation failed'); + return; + } + + // Continue polling if not completed and under max attempts + if (attempts < maxAttempts) { + await new Promise(resolve => setTimeout(resolve, 3000)); // Wait 3 seconds + return poll(); + } else { + console.warn('Max polling attempts reached for cancel status'); + } + } catch (error) { + console.error('Error polling cancel status:', error); + // Don't throw, just stop polling on error + } + }; + + await poll(); + }; + + // Start polling in the background (don't await to avoid blocking navigation) + pollCancelStatus(); + + // Navigate to home page immediately after kill is requested + setShowProgressModal(false); + setProcessingState('IDLE'); + navigate('/'); + + } catch (error) { + console.error('Failed to cancel process:', error); + // Still navigate to home page even if API call fails + setShowProgressModal(false); + setProcessingState('IDLE'); + navigate('/'); + } + } else { + console.warn('No batchId available, skipping cancel API call'); + setShowProgressModal(false); + setProcessingState('IDLE'); + navigate('/'); + } }; // Effect to show modal automatically when processing is detected diff --git a/src/frontend/src/services/ApiService.tsx b/src/frontend/src/services/ApiService.tsx index d4d3657..b72e925 100644 --- a/src/frontend/src/services/ApiService.tsx +++ b/src/frontend/src/services/ApiService.tsx @@ -121,6 +121,11 @@ export const apiService = { upload: (url, formData) => fetchWithAuth(url, 'POST', formData), downloadBlob: (url) => fetchBlobWithAuth(url), // For blob downloads login: (url, body) => fetchWithoutAuth(url, 'POST', body), // For login without auth + // Process cancellation methods + cancelProcess: (processId: string, reason?: string) => + fetchWithAuth(`/process/cancel/${processId}`, 'POST', reason ? { reason } : null), + getCancelStatus: (processId: string) => + fetchWithAuth(`/process/cancel/${processId}/status`, 'GET'), }; export default apiService; diff --git a/src/processor/src/libs/agent_framework/agent_framework_helper.py b/src/processor/src/libs/agent_framework/agent_framework_helper.py index a620be8..38cbf02 100644 --- a/src/processor/src/libs/agent_framework/agent_framework_helper.py +++ b/src/processor/src/libs/agent_framework/agent_framework_helper.py @@ -150,7 +150,8 @@ def create_client( env_file_path: str | None = None, env_file_encoding: str | None = None, instruction_role: str | None = None, - ) -> "AzureOpenAIChatClient": ... + ) -> "AzureOpenAIChatClient": + ... @overload @staticmethod @@ -173,7 +174,8 @@ def create_client( async_client: object | None = None, env_file_path: str | None = None, env_file_encoding: str | None = None, - ) -> "AzureOpenAIAssistantsClient": ... + ) -> "AzureOpenAIAssistantsClient": + ... @overload @staticmethod @@ -194,7 +196,8 @@ def create_client( env_file_path: str | None = None, env_file_encoding: str | None = None, instruction_role: str | None = None, - ) -> "AzureOpenAIResponsesClient": ... + ) -> "AzureOpenAIResponsesClient": + ... @overload @staticmethod @@ -216,7 +219,8 @@ def create_client( env_file_encoding: str | None = None, instruction_role: str | None = None, retry_config: RateLimitRetryConfig | None = None, - ) -> AzureOpenAIResponseClientWithRetry: ... + ) -> AzureOpenAIResponseClientWithRetry: + ... @overload @staticmethod @@ -232,7 +236,8 @@ def create_client( async_credential: object | None = None, env_file_path: str | None = None, env_file_encoding: str | None = None, - ) -> "AzureAIAgentClient": ... + ) -> "AzureAIAgentClient": + ... @staticmethod def create_client( diff --git a/src/processor/src/libs/base/orchestrator_base.py b/src/processor/src/libs/base/orchestrator_base.py index 5b6aa8f..aab8668 100644 --- a/src/processor/src/libs/base/orchestrator_base.py +++ b/src/processor/src/libs/base/orchestrator_base.py @@ -94,7 +94,6 @@ async def prepare_agent_infos(self) -> list[AgentInfo]: """Prepare agent information list for workflow""" pass - async def create_agents( self, agent_infos: list[AgentInfo], process_id: str ) -> list[ChatAgent]: diff --git a/src/processor/src/main.py b/src/processor/src/main.py index 32f2b8f..e62076c 100644 --- a/src/processor/src/main.py +++ b/src/processor/src/main.py @@ -40,23 +40,20 @@ def initialize(self): def register_services(self): # Additional initialization logic can be added here - ############################################################################ - ## Initialize AgentFrameworkHelper and add it to the application context ## - ############################################################################ + # Initialize AgentFrameworkHelper and add it to the application context self.application_context.add_singleton( AgentFrameworkHelper, AgentFrameworkHelper() ) - ## Initialize AgentFrameworkHelper with LLM settings from application context + # Initialize AgentFrameworkHelper with LLM settings from application context self.application_context.get_service(AgentFrameworkHelper).initialize( self.application_context.llm_settings ) - ################################################################################## - ## Initialize middlewares - All Middlewares below are registered as a singleton ## - ################################################################################## - ### InputObserverMiddleware(Agent Level) - ### LoggingFunctionMiddleware(Agent Level) - ### DebuggingMiddleware(Run Level) + # Initialize middlewares - All Middlewares below are registered as a singleton + # ------------------------------------------------------------------------- + # InputObserverMiddleware(Agent Level) + # LoggingFunctionMiddleware(Agent Level) + # DebuggingMiddleware(Run Level) ( # Register DebuggingMiddleware as a singleton self.application_context.add_singleton( diff --git a/src/processor/src/main_service.py b/src/processor/src/main_service.py index 4340273..1b61be0 100644 --- a/src/processor/src/main_service.py +++ b/src/processor/src/main_service.py @@ -120,23 +120,23 @@ def register_services(self): """ # Additional initialization logic can be added here - ############################################################################ - ## Initialize AgentFrameworkHelper and add it to the application context ## - ############################################################################ + # ------------------------------------------------------------------------- + # Initialize AgentFrameworkHelper and add it to the application context + # ------------------------------------------------------------------------- self.application_context.add_singleton( AgentFrameworkHelper, AgentFrameworkHelper() ) - ## Initialize AgentFrameworkHelper with LLM settings from application context + # Initialize AgentFrameworkHelper with LLM settings from application context self.application_context.get_service(AgentFrameworkHelper).initialize( self.application_context.llm_settings ) - ################################################################################## - ## Initialize middlewares - All Middlewares below are registered as a singleton ## - ################################################################################## - ### InputObserverMiddleware(Agent Level) - ### LoggingFunctionMiddleware(Agent Level) - ### DebuggingMiddleware(Run Level) + # ------------------------------------------------------------------------- + # Initialize middlewares - All Middlewares below are registered as a singleton + # ------------------------------------------------------------------------- + # InputObserverMiddleware(Agent Level) + # LoggingFunctionMiddleware(Agent Level) + # DebuggingMiddleware(Run Level) ( # Register DebuggingMiddleware as a singleton self.application_context.add_singleton( @@ -347,12 +347,22 @@ async def start_service(self): if self.control_api is None: try: self.control_api = await self._build_control_api() + if self.control_api: + logger.info("Control API built successfully") + else: + logger.info("Control API is disabled") except Exception as e: logger.warning(f"Failed to build control API: {e}") self.control_api = None if self.control_api: await self.control_api.start() + logger.info( + "Control API is now listening - endpoints: /health, " + "/processes/{id}/control, /processes/{id}/kill" + ) + else: + logger.warning("Control API not started - kill requests will not work") # Start the service (this will run until stopped) await self.queue_service.start_service() diff --git a/src/processor/src/tests/unit/steps/convert/test_conversion_report_quality_gates.py b/src/processor/src/tests/unit/steps/convert/test_conversion_report_quality_gates.py index 9b266c4..3ba38f7 100644 --- a/src/processor/src/tests/unit/steps/convert/test_conversion_report_quality_gates.py +++ b/src/processor/src/tests/unit/steps/convert/test_conversion_report_quality_gates.py @@ -52,4 +52,3 @@ def test_parse_signoffs_and_open_blockers_no_open_all_pass(): "Azure Architect", "Chief Architect", } -