diff --git a/.github/workflows/azure-dev.yml b/.github/workflows/azure-dev.yml index 0976ea17..a65ed6cb 100644 --- a/.github/workflows/azure-dev.yml +++ b/.github/workflows/azure-dev.yml @@ -15,69 +15,19 @@ permissions: jobs: template_validation_job: runs-on: ubuntu-latest - name: Template validation + name: template validation steps: - # Step 1: Checkout the code from your repository - - name: Checkout code - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - # Step 2: Set up Python - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: "3.9" - - # Step 3: Create and populate the virtual environment - - name: Create virtual environment and install dependencies - run: | - python -m venv .venv - source .venv/bin/activate - python -m pip install --upgrade pip - pip install azure-mgmt-resource azure-identity azure-core azure-mgmt-subscription azure-cli-core - # Install any other dependencies that might be needed - pip freeze > requirements-installed.txt - echo "Virtual environment created with these packages:" - cat requirements-installed.txt - - # Step 4: Create azd directory if it doesn't exist - - name: Create azd directory - run: | - mkdir -p ./.azd || true - touch ./.azd/.env || true - - # Step 5: Validate the Azure template - - name: Validate Azure Template - uses: microsoft/template-validation-action@v0.3.5 + - uses: microsoft/template-validation-action@Latest id: validation env: - AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} - AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} - AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - AZURE_ENV_NAME: ${{ secrets.AZURE_ENV_NAME }} - AZURE_LOCATION: ${{ secrets.AZURE_LOCATION }} + AZURE_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }} + AZURE_TENANT_ID: ${{ vars.AZURE_TENANT_ID }} + AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }} + AZURE_ENV_NAME: ${{ vars.AZURE_ENV_NAME }} + AZURE_LOCATION: ${{ vars.AZURE_LOCATION }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # Step 6: Debug output in case of failure - - name: Debug on failure - if: failure() - run: | - echo "Validation failed. Checking environment:" - ls -la - if [ -d ".venv" ]; then - echo ".venv directory exists" - ls -la .venv/bin/ - else - echo ".venv directory does not exist" - fi - if [ -d "tva_*" ]; then - echo "TVA directory exists:" - find . -name "tva_*" -type d - ls -la $(find . -name "tva_*" -type d) - else - echo "No TVA directory found" - fi - - # Step 7: Print the result of the validation - - name: Print result - if: success() + - name: print result run: cat ${{ steps.validation.outputs.resultFile }} diff --git a/.github/workflows/docker-build-and-push.yml b/.github/workflows/docker-build-and-push.yml index 383ef2fc..a6d2c59a 100644 --- a/.github/workflows/docker-build-and-push.yml +++ b/.github/workflows/docker-build-and-push.yml @@ -18,7 +18,7 @@ on: - dev - demo - hotfix - workflow_dispatch: + workflow_dispatch: jobs: build-and-push: @@ -32,14 +32,19 @@ jobs: uses: docker/setup-buildx-action@v1 - name: Log in to Azure Container Registry - if: ${{ (github.ref_name == 'main' || github.ref_name == 'dev' || github.ref_name == 'demo' || github.ref_name == 'hotfix') }} + if: ${{ github.ref_name == 'main' || github.ref_name == 'dev' || github.ref_name == 'demo' || github.ref_name == 'hotfix' }} uses: azure/docker-login@v2 with: login-server: ${{ secrets.ACR_LOGIN_SERVER }} username: ${{ secrets.ACR_USERNAME }} password: ${{ secrets.ACR_PASSWORD }} - - name: Set Docker image tag + - name: Get current date + id: date + run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT + + - name: Determine Tag Name Based on Branch + id: determine_tag run: | if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then echo "TAG=latest" >> $GITHUB_ENV @@ -52,24 +57,30 @@ jobs: else echo "TAG=pullrequest-ignore" >> $GITHUB_ENV fi - - - name: Build and push Docker images optionally + + - name: Set Historical Tag run: | - cd src/backend - docker build -t ${{ secrets.ACR_LOGIN_SERVER }}/macaebackend:${{ env.TAG }} -f Dockerfile . && \ - if [[ "${{ env.TAG }}" == "latest" || "${{ env.TAG }}" == "dev" || "${{ env.TAG }}" == "demo" || "${{ env.TAG }}" == "hotfix" ]]; then - docker push ${{ secrets.ACR_LOGIN_SERVER }}/macaebackend:${{ env.TAG }} && \ - echo "Backend image built and pushed successfully." - else - echo "Skipping Docker push for backend with tag: ${{ env.TAG }}" - fi - cd ../frontend - docker build -t ${{ secrets.ACR_LOGIN_SERVER }}/macaefrontend:${{ env.TAG }} -f Dockerfile . && \ - if [[ "${{ env.TAG }}" == "latest" || "${{ env.TAG }}" == "dev" || "${{ env.TAG }}" == "demo" || "${{ env.TAG }}" == "hotfix" ]]; then - docker push ${{ secrets.ACR_LOGIN_SERVER }}/macaefrontend:${{ env.TAG }} && \ - echo "Frontend image built and pushed successfully." - else - echo "Skipping Docker push for frontend with tag: ${{ env.TAG }}" - fi + DATE_TAG=$(date +'%Y-%m-%d') + RUN_ID=${{ github.run_number }} + # Create historical tag using TAG, DATE_TAG, and RUN_ID + echo "HISTORICAL_TAG=${{ env.TAG }}_${DATE_TAG}_${RUN_ID}" >> $GITHUB_ENV - + - name: Build and optionally push Backend Docker image + uses: docker/build-push-action@v6 + with: + context: ./src/backend + file: ./src/backend/Dockerfile + push: ${{ env.TAG != 'pullrequest-ignore' }} + tags: | + ${{ secrets.ACR_LOGIN_SERVER }}/macaebackend:${{ env.TAG }} + ${{ secrets.ACR_LOGIN_SERVER }}/macaebackend:${{ env.HISTORICAL_TAG }} + + - name: Build and optionally push Frontend Docker image + uses: docker/build-push-action@v6 + with: + context: ./src/frontend + file: ./src/frontend/Dockerfile + push: ${{ env.TAG != 'pullrequest-ignore' }} + tags: | + ${{ secrets.ACR_LOGIN_SERVER }}/macaefrontend:${{ env.TAG }} + ${{ secrets.ACR_LOGIN_SERVER }}/macaefrontend:${{ env.HISTORICAL_TAG }} \ No newline at end of file diff --git a/infra/deploy_ai_foundry.bicep b/infra/deploy_ai_foundry.bicep index ee9b3b37..328a37ea 100644 --- a/infra/deploy_ai_foundry.bicep +++ b/infra/deploy_ai_foundry.bicep @@ -6,7 +6,7 @@ param gptModelName string param gptModelVersion string param managedIdentityObjectId string param aiServicesEndpoint string -param aiServicesKey string +param aiServices object param aiServicesId string var storageName = '${solutionName}hubstorage' @@ -136,7 +136,7 @@ resource aiHub 'Microsoft.MachineLearningServices/workspaces@2023-08-01-preview' authType: 'ApiKey' isSharedToAll: true credentials: { - key: aiServicesKey + key: aiServices.Key.key1 } metadata: { ApiType: 'Azure' @@ -187,7 +187,7 @@ resource azureOpenAIApiKeyEntry 'Microsoft.KeyVault/vaults/secrets@2021-11-01-pr parent: keyVault name: 'AZURE-OPENAI-KEY' properties: { - value: aiServicesKey //aiServices_m.listKeys().key1 + value: aiServices.Key.key1 //aiServices_m.listKeys().key1 } } @@ -251,7 +251,7 @@ resource cogServiceKeyEntry 'Microsoft.KeyVault/vaults/secrets@2021-11-01-previe parent: keyVault name: 'COG-SERVICES-KEY' properties: { - value: aiServicesKey + value: aiServices.Key.key1 } } diff --git a/infra/main.bicep b/infra/main.bicep index bea37442..cdaf6ddd 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -1,6 +1,6 @@ targetScope = 'resourceGroup' -@description('Location for all resources.') -param location string +// @description('Location for all resources.') +// param location string @allowed([ 'australiaeast' @@ -28,15 +28,12 @@ param location string 'westus3' ]) @description('Location for all Ai services resources. This location can be different from the resource group location.') -param azureOpenAILocation string = 'eastus2' // The location used for all deployed resources. This location must be in the same region as the resource group. +param azureOpenAILocation string //= 'eastus2' // The location used for all deployed resources. This location must be in the same region as the resource group. @minLength(3) @maxLength(20) -@description('A unique prefix for all resources in this deployment. This should be 3-20 characters long:') -param environmentName string - -var uniqueId = toLower(uniqueString(subscription().id, environmentName, resourceGroup().location)) -var solutionPrefix = 'ma${padLeft(take(uniqueId, 12), 12, '0')}' +@description('Prefix for all resources created by this template. This prefix will be used to create unique names for all resources. The prefix must be unique within the resource group.') +param prefix string //= 'macae' @description('Tags to apply to all deployed resources') param tags object = {} @@ -61,8 +58,9 @@ param resourceSize { } param capacity int = 140 +var location = resourceGroup().location var modelVersion = '2024-08-06' -var aiServicesName = '${solutionPrefix}-aiservices' +var aiServicesName = '${prefix}-aiservices' var deploymentType = 'GlobalStandard' var gptModelVersion = 'gpt-4o' var appVersion = 'fnd01' @@ -73,7 +71,7 @@ var dockerRegistryUrl = 'https://${resgistryName}.azurecr.io' var backendDockerImageURL = '${resgistryName}.azurecr.io/macaebackend:${appVersion}' var frontendDockerImageURL = '${resgistryName}.azurecr.io/macaefrontend:${appVersion}' -var uniqueNameFormat = '${solutionPrefix}-{0}-${uniqueString(resourceGroup().id, solutionPrefix)}' +var uniqueNameFormat = '${prefix}-{0}-${uniqueString(resourceGroup().id, prefix)}' var aoaiApiVersion = '2025-01-01-preview' resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = { @@ -123,7 +121,7 @@ resource aiServices 'Microsoft.CognitiveServices/accounts@2024-04-01-preview' = apiProperties: { //statisticsEnabled: false } - //disableLocalAuth: true + disableLocalAuth: true } } @@ -149,7 +147,7 @@ resource aiServicesDeployments 'Microsoft.CognitiveServices/accounts/deployments module kvault 'deploy_keyvault.bicep' = { name: 'deploy_keyvault' params: { - solutionName: solutionPrefix + solutionName: prefix solutionLocation: location managedIdentityObjectId: managedIdentityModule.outputs.managedIdentityOutput.objectId } @@ -163,14 +161,14 @@ module kvault 'deploy_keyvault.bicep' = { module aifoundry 'deploy_ai_foundry.bicep' = { name: 'deploy_ai_foundry' params: { - solutionName: solutionPrefix + solutionName: prefix solutionLocation: azureOpenAILocation keyVaultName: kvault.outputs.keyvaultName gptModelName: gptModelVersion gptModelVersion: gptModelVersion managedIdentityObjectId: managedIdentityModule.outputs.managedIdentityOutput.objectId aiServicesEndpoint: aiServices.properties.endpoint - aiServicesKey: aiServices.listKeys().key1 + aiServices: aiServices aiServicesId: aiServices.id } scope: resourceGroup(resourceGroup().name) @@ -205,7 +203,7 @@ resource cosmos 'Microsoft.DocumentDB/databaseAccounts@2024-05-15' = { } ] capabilities: [{ name: 'EnableServerless' }] - //disableLocalAuth: true + disableLocalAuth: true } resource contributorRoleDefinition 'sqlRoleDefinitions' existing = { @@ -279,7 +277,7 @@ resource acaCosomsRoleAssignment 'Microsoft.DocumentDB/databaseAccounts/sqlRoleA @description('') resource containerApp 'Microsoft.App/containerApps@2024-03-01' = { - name: '${solutionPrefix}-backend' + name: '${prefix}-backend' location: location tags: tags identity: { @@ -448,7 +446,7 @@ resource frontendAppService 'Microsoft.Web/sites@2021-02-01' = { } resource aiHubProject 'Microsoft.MachineLearningServices/workspaces@2024-01-01-preview' existing = { - name: '${solutionPrefix}-aiproject' // aiProjectName must be calculated - available at main start. + name: '${prefix}-aiproject' // aiProjectName must be calculated - available at main start. } resource aiDeveloper 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { @@ -469,7 +467,7 @@ var cosmosAssignCli = 'az cosmosdb sql role assignment create --resource-group " module managedIdentityModule 'deploy_managed_identity.bicep' = { name: 'deploy_managed_identity' params: { - solutionName: solutionPrefix + solutionName: prefix //solutionLocation: location managedIdentityId: pullIdentity.id managedIdentityPropPrin: pullIdentity.properties.principalId diff --git a/infra/main.json b/infra/main.json index 6c40552d..ccecd875 100644 --- a/infra/main.json +++ b/infra/main.json @@ -5,20 +5,13 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.34.44.8038", - "templateHash": "7719893060553487435" + "version": "0.35.1.17967", + "templateHash": "18228555099764132241" } }, "parameters": { - "location": { - "type": "string", - "metadata": { - "description": "Location for all resources." - } - }, "azureOpenAILocation": { "type": "string", - "defaultValue": "eastus2", "allowedValues": [ "australiaeast", "brazilsouth", @@ -50,7 +43,6 @@ }, "prefix": { "type": "string", - "defaultValue": "macae", "minLength": 3, "maxLength": 20, "metadata": { @@ -107,6 +99,7 @@ } }, "variables": { + "location": "[resourceGroup().location]", "modelVersion": "2024-08-06", "aiServicesName": "[format('{0}-aiservices', parameters('prefix'))]", "deploymentType": "GlobalStandard", @@ -190,7 +183,7 @@ "type": "Microsoft.OperationalInsights/workspaces", "apiVersion": "2023-09-01", "name": "[format(variables('uniqueNameFormat'), 'logs')]", - "location": "[parameters('location')]", + "location": "[variables('location')]", "tags": "[parameters('tags')]", "properties": { "retentionInDays": 30, @@ -203,7 +196,7 @@ "type": "Microsoft.Insights/components", "apiVersion": "2020-02-02-preview", "name": "[format(variables('uniqueNameFormat'), 'appins')]", - "location": "[parameters('location')]", + "location": "[variables('location')]", "kind": "web", "properties": { "Application_Type": "web", @@ -217,7 +210,7 @@ "type": "Microsoft.CognitiveServices/accounts", "apiVersion": "2024-04-01-preview", "name": "[variables('aiServicesName')]", - "location": "[parameters('location')]", + "location": "[variables('location')]", "sku": { "name": "S0" }, @@ -277,7 +270,7 @@ "type": "Microsoft.DocumentDB/databaseAccounts", "apiVersion": "2024-05-15", "name": "[format(variables('uniqueNameFormat'), 'cosmos')]", - "location": "[parameters('location')]", + "location": "[variables('location')]", "tags": "[parameters('tags')]", "kind": "GlobalDocumentDB", "properties": { @@ -286,7 +279,7 @@ "locations": [ { "failoverPriority": 0, - "locationName": "[parameters('location')]" + "locationName": "[variables('location')]" } ], "capabilities": [ @@ -301,13 +294,13 @@ "type": "Microsoft.ManagedIdentity/userAssignedIdentities", "apiVersion": "2023-07-31-preview", "name": "[format(variables('uniqueNameFormat'), 'containerapp-pull')]", - "location": "[parameters('location')]" + "location": "[variables('location')]" }, "containerAppEnv": { "type": "Microsoft.App/managedEnvironments", "apiVersion": "2024-03-01", "name": "[format(variables('uniqueNameFormat'), 'containerapp')]", - "location": "[parameters('location')]", + "location": "[variables('location')]", "tags": "[parameters('tags')]", "properties": { "daprAIConnectionString": "[reference('appInsights').ConnectionString]", @@ -315,7 +308,7 @@ "destination": "log-analytics", "logAnalyticsConfiguration": { "customerId": "[reference('logAnalytics').customerId]", - "sharedKey": "[listKeys(resourceId('Microsoft.OperationalInsights/workspaces', format(variables('uniqueNameFormat'), 'logs')), '2023-09-01').primarySharedKey]" + "sharedKey": "[listKeys('logAnalytics', '2023-09-01').primarySharedKey]" } } }, @@ -342,7 +335,7 @@ "type": "Microsoft.App/containerApps", "apiVersion": "2024-03-01", "name": "[format('{0}-backend', parameters('prefix'))]", - "location": "[parameters('location')]", + "location": "[variables('location')]", "tags": "[parameters('tags')]", "identity": { "type": "SystemAssigned, UserAssigned", @@ -468,7 +461,7 @@ "type": "Microsoft.Web/serverfarms", "apiVersion": "2021-02-01", "name": "[format(variables('uniqueNameFormat'), 'frontend-plan')]", - "location": "[parameters('location')]", + "location": "[variables('location')]", "tags": "[parameters('tags')]", "sku": { "name": "P1v2", @@ -484,7 +477,7 @@ "type": "Microsoft.Web/sites", "apiVersion": "2021-02-01", "name": "[format(variables('uniqueNameFormat'), 'frontend')]", - "location": "[parameters('location')]", + "location": "[variables('location')]", "tags": "[parameters('tags')]", "kind": "app,linux,container", "properties": { @@ -568,7 +561,7 @@ "value": "[parameters('prefix')]" }, "solutionLocation": { - "value": "[parameters('location')]" + "value": "[variables('location')]" }, "managedIdentityObjectId": { "value": "[reference('managedIdentityModule').outputs.managedIdentityOutput.value.objectId]" @@ -580,8 +573,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.34.44.8038", - "templateHash": "10664495342911727649" + "version": "0.35.1.17967", + "templateHash": "5761607453167859573" } }, "parameters": { @@ -706,7 +699,7 @@ "value": "[reference('aiServices').endpoint]" }, "aiServicesKey": { - "value": "[listKeys(resourceId('Microsoft.CognitiveServices/accounts', variables('aiServicesName')), '2024-04-01-preview').key1]" + "value": "[listKeys('aiServices', '2024-04-01-preview').key1]" }, "aiServicesId": { "value": "[resourceId('Microsoft.CognitiveServices/accounts', variables('aiServicesName'))]" @@ -718,8 +711,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.34.44.8038", - "templateHash": "8087543237770345715" + "version": "0.35.1.17967", + "templateHash": "9490638595753234802" } }, "parameters": { @@ -1112,8 +1105,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.34.44.8038", - "templateHash": "11364190519186458619" + "version": "0.35.1.17967", + "templateHash": "12327197428621494853" } }, "parameters": { @@ -1199,7 +1192,7 @@ "value": "2.69.0" }, "location": { - "value": "[parameters('location')]" + "value": "[variables('location')]" }, "managedIdentities": { "value": { diff --git a/pytest.ini b/pytest.ini index 1693cefe..b0ea8b13 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,2 +1,3 @@ [pytest] -addopts = -p pytest_asyncio \ No newline at end of file +addopts = -p pytest_asyncio +pythonpath = src diff --git a/src/backend/tests/__init__.py b/src/backend/kernel_tools/__init__.py similarity index 100% rename from src/backend/tests/__init__.py rename to src/backend/kernel_tools/__init__.py diff --git a/src/backend/kernel_tools/marketing_tools.py b/src/backend/kernel_tools/marketing_tools.py index 5851e75c..90818f7e 100644 --- a/src/backend/kernel_tools/marketing_tools.py +++ b/src/backend/kernel_tools/marketing_tools.py @@ -1,14 +1,11 @@ """MarketingTools class provides various marketing functions for a marketing agent.""" - import inspect -from typing import Callable, List +import json +from typing import Callable, List, get_type_hints -from semantic_kernel.functions import kernel_function from models.messages_kernel import AgentType -import inspect -import json -from typing import Any, Dict, List, get_type_hints +from semantic_kernel.functions import kernel_function class MarketingTools: @@ -278,6 +275,7 @@ async def analyze_website_traffic(source: str) -> str: @staticmethod @kernel_function(description="Develop customer personas for a specific segment.") async def develop_customer_personas(segment_name: str) -> str: + """ Develop customer personas for a specific segment. """ return f"Customer personas developed for segment '{segment_name}'." # This function does NOT have the kernel_function annotation @@ -290,7 +288,6 @@ def generate_tools_json_doc(cls) -> str: Returns: str: JSON string containing the methods' information """ - tools_list = [] # Get all methods from the class that have the kernel_function annotation @@ -320,7 +317,7 @@ def generate_tools_json_doc(cls) -> str: type_hints = get_type_hints(method) # Process parameters - for param_name, param in sig.parameters.items(): + for param_name, param in sig.parameters.items(): # Skip first parameter 'cls' for class methods (though we're using staticmethod now) if param_name in ["cls", "self"]: continue @@ -345,7 +342,6 @@ def generate_tools_json_doc(cls) -> str: param_type = "string" # Create parameter description - param_desc = param_name.replace("_", " ") args_dict[param_name] = { "description": param_name, "title": param_name.replace("_", " ").title(), @@ -370,7 +366,8 @@ def generate_tools_json_doc(cls) -> str: @classmethod def get_all_kernel_functions(cls) -> dict[str, Callable]: """ - Returns a dictionary of all methods in this class that have the @kernel_function annotation. + Return a dictionary of all methods in this class that have the @kernel_function annotation. + This function itself is not annotated with @kernel_function. Returns: diff --git a/src/backend/tests/test_agent_integration.py b/src/backend/tests/test_agent_integration.py deleted file mode 100644 index 03e2f16e..00000000 --- a/src/backend/tests/test_agent_integration.py +++ /dev/null @@ -1,210 +0,0 @@ -"""Integration tests for the agent system. - -This test file verifies that the agent system correctly loads environment -variables and can use functions from the JSON tool files. -""" -import os -import sys -import unittest -import asyncio -import uuid -from dotenv import load_dotenv - -# Add the parent directory to the path so we can import our modules -sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -from config_kernel import Config -from kernel_agents.agent_factory import AgentFactory -from models.messages_kernel import AgentType -from utils_kernel import get_agents -from semantic_kernel.functions.kernel_arguments import KernelArguments - -# Load environment variables from .env file -load_dotenv() - - -class AgentIntegrationTest(unittest.TestCase): - """Integration tests for the agent system.""" - - def __init__(self, methodName='runTest'): - """Initialize the test case with required attributes.""" - super().__init__(methodName) - # Initialize these here to avoid the AttributeError - self.session_id = str(uuid.uuid4()) - self.user_id = "test-user" - self.required_env_vars = [ - "AZURE_OPENAI_DEPLOYMENT_NAME", - "AZURE_OPENAI_API_VERSION", - "AZURE_OPENAI_ENDPOINT" - ] - - def setUp(self): - """Set up the test environment.""" - # Ensure we have the required environment variables - for var in self.required_env_vars: - if not os.getenv(var): - self.fail(f"Required environment variable {var} not set") - - # Print test configuration - print(f"\nRunning tests with:") - print(f" - Session ID: {self.session_id}") - print(f" - OpenAI Deployment: {os.getenv('AZURE_OPENAI_DEPLOYMENT_NAME')}") - print(f" - OpenAI Endpoint: {os.getenv('AZURE_OPENAI_ENDPOINT')}") - - def tearDown(self): - """Clean up after tests.""" - # Clear the agent cache to ensure each test starts fresh - AgentFactory.clear_cache() - - def test_environment_variables(self): - """Test that environment variables are loaded correctly.""" - self.assertIsNotNone(Config.AZURE_OPENAI_DEPLOYMENT_NAME) - self.assertIsNotNone(Config.AZURE_OPENAI_API_VERSION) - self.assertIsNotNone(Config.AZURE_OPENAI_ENDPOINT) - - async def _test_create_kernel(self): - """Test creating a semantic kernel.""" - kernel = Config.CreateKernel() - self.assertIsNotNone(kernel) - return kernel - - async def _test_create_agent_factory(self): - """Test creating an agent using the agent factory.""" - # Create a generic agent - generic_agent = await AgentFactory.create_agent( - agent_type=AgentType.GENERIC, - session_id=self.session_id, - user_id=self.user_id - ) - - self.assertIsNotNone(generic_agent) - self.assertEqual(generic_agent._agent_name, "generic") - - # Test that the agent has tools loaded from the generic_tools.json file - self.assertTrue(hasattr(generic_agent, "_tools")) - - # Return the agent for further testing - return generic_agent - - async def _test_create_all_agents(self): - """Test creating all agents.""" - agents_raw = await AgentFactory.create_all_agents( - session_id=self.session_id, - user_id=self.user_id - ) - - # Check that all expected agent types are created - expected_types = [ - AgentType.HR, AgentType.MARKETING, AgentType.PRODUCT, - AgentType.PROCUREMENT, AgentType.TECH_SUPPORT, - AgentType.GENERIC, AgentType.HUMAN, AgentType.PLANNER, - AgentType.GROUP_CHAT_MANAGER - ] - - for agent_type in expected_types: - self.assertIn(agent_type, agents_raw) - self.assertIsNotNone(agents_raw[agent_type]) - - # Return the agents for further testing - return agents_raw - - async def _test_get_agents(self): - """Test the get_agents utility function.""" - agents = await get_agents(self.session_id, self.user_id) - - # Check that all expected agents are present - expected_agent_names = [ - "HrAgent", "ProductAgent", "MarketingAgent", - "ProcurementAgent", "TechSupportAgent", "GenericAgent", - "HumanAgent", "PlannerAgent", "GroupChatManager" - ] - - for agent_name in expected_agent_names: - self.assertIn(agent_name, agents) - self.assertIsNotNone(agents[agent_name]) - - # Return the agents for further testing - return agents - - async def _test_create_azure_ai_agent(self): - """Test creating an AzureAIAgent directly.""" - agent = await get_azure_ai_agent( - session_id=self.session_id, - agent_name="test-agent", - system_prompt="You are a test agent." - ) - - self.assertIsNotNone(agent) - return agent - - async def _test_agent_tool_invocation(self): - """Test that an agent can invoke tools from JSON configuration.""" - # Get a generic agent that should have the dummy_function loaded - agents = await get_agents(self.session_id, self.user_id) - generic_agent = agents["GenericAgent"] - - # Check that the agent has tools - self.assertTrue(hasattr(generic_agent, "_tools")) - - # Try to invoke a dummy function if it exists - try: - # Use the agent to invoke the dummy function - result = await generic_agent._agent.invoke_async("This is a test query that should use dummy_function") - - # If we got here, the function invocation worked - self.assertIsNotNone(result) - print(f"Tool invocation result: {result}") - except Exception as e: - self.fail(f"Tool invocation failed: {e}") - - return result - - async def run_all_tests(self): - """Run all tests in sequence.""" - # Call setUp explicitly to ensure environment is properly initialized - self.setUp() - - try: - print("Testing environment variables...") - self.test_environment_variables() - - print("Testing kernel creation...") - kernel = await self._test_create_kernel() - - print("Testing agent factory...") - generic_agent = await self._test_create_agent_factory() - - print("Testing creating all agents...") - all_agents_raw = await self._test_create_all_agents() - - print("Testing get_agents utility...") - agents = await self._test_get_agents() - - print("Testing Azure AI agent creation...") - azure_agent = await self._test_create_azure_ai_agent() - - print("Testing agent tool invocation...") - tool_result = await self._test_agent_tool_invocation() - - print("\nAll tests completed successfully!") - - except Exception as e: - print(f"Tests failed: {e}") - raise - finally: - # Call tearDown explicitly to ensure proper cleanup - self.tearDown() - -def run_tests(): - """Run the tests.""" - test = AgentIntegrationTest() - - # Create and run the event loop - loop = asyncio.get_event_loop() - try: - loop.run_until_complete(test.run_all_tests()) - finally: - loop.close() - -if __name__ == '__main__': - run_tests() \ No newline at end of file diff --git a/src/backend/tests/test_app.py b/src/backend/tests/test_app.py deleted file mode 100644 index 0e9f0d1e..00000000 --- a/src/backend/tests/test_app.py +++ /dev/null @@ -1,89 +0,0 @@ -import os -import sys -from unittest.mock import MagicMock, patch -import pytest -from fastapi.testclient import TestClient - -# Mock Azure dependencies to prevent import errors -sys.modules["azure.monitor"] = MagicMock() -sys.modules["azure.monitor.events.extension"] = MagicMock() -sys.modules["azure.monitor.opentelemetry"] = MagicMock() - -# Mock environment variables before importing app -os.environ["COSMOSDB_ENDPOINT"] = "https://mock-endpoint" -os.environ["COSMOSDB_KEY"] = "mock-key" -os.environ["COSMOSDB_DATABASE"] = "mock-database" -os.environ["COSMOSDB_CONTAINER"] = "mock-container" -os.environ[ - "APPLICATIONINSIGHTS_CONNECTION_STRING" -] = "InstrumentationKey=mock-instrumentation-key;IngestionEndpoint=https://mock-ingestion-endpoint" -os.environ["AZURE_OPENAI_DEPLOYMENT_NAME"] = "mock-deployment-name" -os.environ["AZURE_OPENAI_API_VERSION"] = "2023-01-01" -os.environ["AZURE_OPENAI_ENDPOINT"] = "https://mock-openai-endpoint" - -# Mock telemetry initialization to prevent errors -with patch("azure.monitor.opentelemetry.configure_azure_monitor", MagicMock()): - from src.backend.app import app - -# Initialize FastAPI test client -client = TestClient(app) - - -@pytest.fixture(autouse=True) -def mock_dependencies(monkeypatch): - """Mock dependencies to simplify tests.""" - monkeypatch.setattr( - "src.backend.auth.auth_utils.get_authenticated_user_details", - lambda headers: {"user_principal_id": "mock-user-id"}, - ) - monkeypatch.setattr( - "src.backend.utils.retrieve_all_agent_tools", - lambda: [{"agent": "test_agent", "function": "test_function"}], - ) - - -def test_input_task_invalid_json(): - """Test the case where the input JSON is invalid.""" - invalid_json = "Invalid JSON data" - - headers = {"Authorization": "Bearer mock-token"} - response = client.post("/input_task", data=invalid_json, headers=headers) - - # Assert response for invalid JSON - assert response.status_code == 422 - assert "detail" in response.json() - - -def test_input_task_missing_description(): - """Test the case where the input task description is missing.""" - input_task = { - "session_id": None, - "user_id": "mock-user-id", - } - - headers = {"Authorization": "Bearer mock-token"} - response = client.post("/input_task", json=input_task, headers=headers) - - # Assert response for missing description - assert response.status_code == 422 - assert "detail" in response.json() - - -def test_basic_endpoint(): - """Test a basic endpoint to ensure the app runs.""" - response = client.get("/") - assert response.status_code == 404 # The root endpoint is not defined - - -def test_input_task_empty_description(): - """Tests if /input_task handles an empty description.""" - empty_task = {"session_id": None, "user_id": "mock-user-id", "description": ""} - headers = {"Authorization": "Bearer mock-token"} - response = client.post("/input_task", json=empty_task, headers=headers) - - assert response.status_code == 422 - assert "detail" in response.json() # Assert error message for missing description - - -if __name__ == "__main__": - pytest.main() diff --git a/src/backend/tests/test_config.py b/src/backend/tests/test_config.py deleted file mode 100644 index 3c4b0efe..00000000 --- a/src/backend/tests/test_config.py +++ /dev/null @@ -1,62 +0,0 @@ -# tests/test_config.py -from unittest.mock import patch -import os - -# Mock environment variables globally -MOCK_ENV_VARS = { - "COSMOSDB_ENDPOINT": "https://mock-cosmosdb.documents.azure.com:443/", - "COSMOSDB_DATABASE": "mock_database", - "COSMOSDB_CONTAINER": "mock_container", - "AZURE_OPENAI_DEPLOYMENT_NAME": "mock-deployment", - "AZURE_OPENAI_API_VERSION": "2024-05-01-preview", - "AZURE_OPENAI_ENDPOINT": "https://mock-openai-endpoint.azure.com/", - "AZURE_OPENAI_API_KEY": "mock-api-key", - "AZURE_TENANT_ID": "mock-tenant-id", - "AZURE_CLIENT_ID": "mock-client-id", - "AZURE_CLIENT_SECRET": "mock-client-secret", -} - -with patch.dict(os.environ, MOCK_ENV_VARS): - from src.backend.config import ( - Config, - GetRequiredConfig, - GetOptionalConfig, - GetBoolConfig, - ) - - -@patch.dict(os.environ, MOCK_ENV_VARS) -def test_get_required_config(): - """Test GetRequiredConfig.""" - assert GetRequiredConfig("COSMOSDB_ENDPOINT") == MOCK_ENV_VARS["COSMOSDB_ENDPOINT"] - - -@patch.dict(os.environ, MOCK_ENV_VARS) -def test_get_optional_config(): - """Test GetOptionalConfig.""" - assert GetOptionalConfig("NON_EXISTENT_VAR", "default_value") == "default_value" - assert ( - GetOptionalConfig("COSMOSDB_DATABASE", "default_db") - == MOCK_ENV_VARS["COSMOSDB_DATABASE"] - ) - - -@patch.dict(os.environ, MOCK_ENV_VARS) -def test_get_bool_config(): - """Test GetBoolConfig.""" - with patch.dict("os.environ", {"FEATURE_ENABLED": "true"}): - assert GetBoolConfig("FEATURE_ENABLED") is True - with patch.dict("os.environ", {"FEATURE_ENABLED": "false"}): - assert GetBoolConfig("FEATURE_ENABLED") is False - with patch.dict("os.environ", {"FEATURE_ENABLED": "1"}): - assert GetBoolConfig("FEATURE_ENABLED") is True - with patch.dict("os.environ", {"FEATURE_ENABLED": "0"}): - assert GetBoolConfig("FEATURE_ENABLED") is False - - -@patch("config.DefaultAzureCredential") -def test_get_azure_credentials_with_env_vars(mock_default_cred): - """Test Config.GetAzureCredentials with explicit credentials.""" - with patch.dict(os.environ, MOCK_ENV_VARS): - creds = Config.GetAzureCredentials() - assert creds is not None diff --git a/src/backend/tests/agents/__init__.py b/src/tests/__init__.py similarity index 100% rename from src/backend/tests/agents/__init__.py rename to src/tests/__init__.py diff --git a/src/backend/tests/auth/__init__.py b/src/tests/backend/__init__.py similarity index 100% rename from src/backend/tests/auth/__init__.py rename to src/tests/backend/__init__.py diff --git a/src/backend/tests/context/__init__.py b/src/tests/backend/auth/__init__.py similarity index 100% rename from src/backend/tests/context/__init__.py rename to src/tests/backend/auth/__init__.py diff --git a/src/backend/tests/auth/test_auth_utils.py b/src/tests/backend/auth/test_auth_utils.py similarity index 100% rename from src/backend/tests/auth/test_auth_utils.py rename to src/tests/backend/auth/test_auth_utils.py diff --git a/src/backend/tests/auth/test_sample_user.py b/src/tests/backend/auth/test_sample_user.py similarity index 100% rename from src/backend/tests/auth/test_sample_user.py rename to src/tests/backend/auth/test_sample_user.py diff --git a/src/backend/tests/handlers/__init__.py b/src/tests/backend/context/__init__.py similarity index 100% rename from src/backend/tests/handlers/__init__.py rename to src/tests/backend/context/__init__.py diff --git a/src/backend/tests/context/test_cosmos_memory.py b/src/tests/backend/context/test_cosmos_memory.py similarity index 100% rename from src/backend/tests/context/test_cosmos_memory.py rename to src/tests/backend/context/test_cosmos_memory.py diff --git a/src/backend/tests/middleware/__init__.py b/src/tests/backend/handlers/__init__.py similarity index 100% rename from src/backend/tests/middleware/__init__.py rename to src/tests/backend/handlers/__init__.py diff --git a/src/tests/backend/handlers/test_runtime_interrupt_kernel.py b/src/tests/backend/handlers/test_runtime_interrupt_kernel.py new file mode 100644 index 00000000..db14cd07 --- /dev/null +++ b/src/tests/backend/handlers/test_runtime_interrupt_kernel.py @@ -0,0 +1,178 @@ +# src/tests/backend/handlers/test_runtime_interrupt_kernel.py + +import sys +import os +import types +import pytest +import asyncio + +# ─── Stub out semantic_kernel so the module import works ───────────────────────── +sk = types.ModuleType("semantic_kernel") +ka = types.ModuleType("semantic_kernel.kernel_arguments") +kp = types.ModuleType("semantic_kernel.kernel_pydantic") + +# Provide classes so subclassing and instantiation work +class StubKernelBaseModel: + def __init__(self, **data): + for k, v in data.items(): setattr(self, k, v) + +class StubKernelArguments: + pass + +class StubKernel: + def __init__(self): + self.functions = {} + self.variables = {} + def add_function(self, func, plugin_name, function_name): + self.functions[(plugin_name, function_name)] = func + def set_variable(self, name, value): + self.variables[name] = value + def get_variable(self, name, default=None): + return self.variables.get(name, default) + +# Assign stubs to semantic_kernel modules +sk.Kernel = StubKernel +ka.KernelArguments = StubKernelArguments +kp.KernelBaseModel = StubKernelBaseModel + +# Install into sys.modules before import +sys.modules["semantic_kernel"] = sk +sys.modules["semantic_kernel.kernel_arguments"] = ka +sys.modules["semantic_kernel.kernel_pydantic"] = kp +# ──────────────────────────────────────────────────────────────────────────────── + +# Ensure /src is on sys.path +THIS_DIR = os.path.dirname(__file__) +SRC_DIR = os.path.abspath(os.path.join(THIS_DIR, "..", "..", "..")) +if SRC_DIR not in sys.path: + sys.path.insert(0, SRC_DIR) + +# Now import the module under test +from backend.handlers.runtime_interrupt_kernel import ( + GetHumanInputMessage, + MessageBody, + GroupChatMessage, + NeedsUserInputHandler, + AssistantResponseHandler, + register_handlers, + get_handlers, +) + +# ─── Tests ─────────────────────────────────────────────────────────────────── + +def test_models_and_str(): + # GetHumanInputMessage and MessageBody + gi = GetHumanInputMessage(content="hi") + assert gi.content == "hi" + mb = MessageBody(content="body") + assert mb.content == "body" + + # GroupChatMessage with content attr + class B1: + def __init__(self, content): + self.content = content + g1 = GroupChatMessage(body=B1("c1"), source="S1", session_id="SID", target="T1") + assert str(g1) == "GroupChatMessage(source=S1, content=c1)" + + # GroupChatMessage without content attr + class B2: + def __str__(self): return "bodystr" + g2 = GroupChatMessage(body=B2(), source="S2", session_id="SID2", target="") + assert "bodystr" in str(g2) + +@pytest.mark.asyncio +async def test_needs_user_handler_all_branches(): + h = NeedsUserInputHandler() + # initial + assert not h.needs_human_input + assert h.question_content is None + assert h.get_messages() == [] + + # human input message + human = GetHumanInputMessage(content="ask") + ret = await h.on_message(human, sender_type="T", sender_key="K") + assert ret is human + assert h.needs_human_input + assert h.question_content == "ask" + msgs = h.get_messages() + assert msgs == [{"agent": {"type": "T", "key": "K"}, "content": "ask"}] + + # group chat message + class B: + content = "grp" + grp = GroupChatMessage(body=B(), source="A", session_id="SID3", target="") + ret2 = await h.on_message(grp, sender_type="A", sender_key="B") + assert ret2 is grp + # human_input remains + assert h.needs_human_input + msgs2 = h.get_messages() + assert msgs2 == [{"agent": {"type": "A", "key": "B"}, "content": "grp"}] + + # dict message branch + d = {"content": "xyz"} + ret3 = await h.on_message(d, sender_type="X", sender_key="Y") + assert isinstance(h.question_for_human, GetHumanInputMessage) + assert h.question_content == "xyz" + msgs3 = h.get_messages() + assert msgs3 == [{"agent": {"type": "X", "key": "Y"}, "content": "xyz"}] + +@pytest.mark.asyncio +async def test_needs_user_handler_unrelated(): + h = NeedsUserInputHandler() + class C: pass + obj = C() + ret = await h.on_message(obj, sender_type="t", sender_key="k") + assert ret is obj + assert not h.needs_human_input + assert h.get_messages() == [] + +@pytest.mark.asyncio +async def test_assistant_response_handler_various(): + h = AssistantResponseHandler() + # no response yet + assert not h.has_response + + # writer branch with content attr + class Body: + content = "r1" + msg = type("M", (), {"body": Body()})() + out = await h.on_message(msg, sender_type="writer") + assert out is msg + assert h.has_response and h.get_response() == "r1" + + # editor branch with no content attr + class Body2: + def __str__(self): return "s2" + msg2 = type("M2", (), {"body": Body2()})() + await h.on_message(msg2, sender_type="editor") + assert h.get_response() == "s2" + + # dict/value branch + await h.on_message({"value": "v2"}, sender_type="any") + assert h.get_response() == "v2" + + # no-match + prev = h.assistant_response + await h.on_message(123, sender_type="writer") + assert h.assistant_response == prev + + +def test_register_and_get_handlers_flow(): + k = StubKernel() + u1, a1 = register_handlers(k, "sess") + assert ("user_input_handler_sess", "on_message") in k.functions + assert ("assistant_handler_sess", "on_message") in k.functions + assert k.get_variable("input_handler_sess") is u1 + assert k.get_variable("response_handler_sess") is a1 + + # get existing + u2, a2 = get_handlers(k, "sess") + assert u2 is u1 and a2 is a1 + + # new pair when missing + k2 = StubKernel() + k2.set_variable("input_handler_new", None) + k2.set_variable("response_handler_new", None) + u3, a3 = get_handlers(k2, "new") + assert isinstance(u3, NeedsUserInputHandler) + assert isinstance(a3, AssistantResponseHandler) diff --git a/src/backend/tests/models/__init__.py b/src/tests/backend/kernel_agents/__init__.py similarity index 100% rename from src/backend/tests/models/__init__.py rename to src/tests/backend/kernel_agents/__init__.py diff --git a/src/tests/backend/kernel_agents/test_agent_utils.py b/src/tests/backend/kernel_agents/test_agent_utils.py new file mode 100644 index 00000000..e69de29b diff --git a/src/tests/backend/kernel_tools/__init__.py b/src/tests/backend/kernel_tools/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/tests/backend/kernel_tools/test_marketing_tools.py b/src/tests/backend/kernel_tools/test_marketing_tools.py new file mode 100644 index 00000000..18a1ef9f --- /dev/null +++ b/src/tests/backend/kernel_tools/test_marketing_tools.py @@ -0,0 +1,464 @@ +"""Test cases for marketing tools.""" +import json +import sys +import types + +import pytest + + +mock_models = types.ModuleType("models") +mock_messages_kernel = types.ModuleType("messages_kernel") + + +class MockAgentType: + """Mock class to simulate AgentType enum used in messages_kernel.""" + + MARKETING = type("EnumValue", (), {"value": "marketing-agent"}) + + +mock_messages_kernel.AgentType = MockAgentType +mock_models.messages_kernel = mock_messages_kernel + +sys.modules["models"] = mock_models +sys.modules["models.messages_kernel"] = mock_messages_kernel +from src.backend.kernel_tools.marketing_tools import MarketingTools + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "campaign_name, target_audience, budget, expected", + [ + ( + "Summer Sale", "Young Adults", 5000.0, + "Marketing campaign 'Summer Sale' created targeting 'Young Adults' with a budget of $5000.00." + ) + ] +) +async def test_create_marketing_campaign(campaign_name, target_audience, budget, expected): + """Test creation of a marketing campaign.""" + result = await MarketingTools.create_marketing_campaign(campaign_name, target_audience, budget) + assert result == expected + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "industry, expected", + [ + ("Retail", "Market trends analyzed for the 'Retail' industry.") + ] +) +async def test_analyze_market_trends(industry, expected): + """Test analysis of market trends for a given industry.""" + result = await MarketingTools.analyze_market_trends(industry) + assert result == expected + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "campaign_name, platforms, expected", + [ + ( + "Holiday Push", ["Facebook", "Instagram"], + "Social media posts for campaign 'Holiday Push' generated for platforms: Facebook, Instagram." + ) + ] +) +async def test_generate_social_posts(campaign_name, platforms, expected): + """Test generation of social media posts.""" + result = await MarketingTools.generate_social_posts(campaign_name, platforms) + assert result == expected + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "event_name, date, location, expected", + [ + ( + "Product Launch", "2025-08-15", "New York", + "Marketing event 'Product Launch' scheduled on 2025-08-15 at New York." + ) + ] +) +async def test_schedule_marketing_event(event_name, date, location, expected): + """Test scheduling of a marketing event.""" + result = await MarketingTools.schedule_marketing_event(event_name, date, location) + assert result == expected + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "campaign_name, material_type, expected", + [ + ( + "Back to School", "flyer", + "Flyer for campaign 'Back to School' designed." + ) + ] +) +async def test_design_promotional_material(campaign_name, material_type, expected): + """Test design of promotional material.""" + result = await MarketingTools.design_promotional_material(campaign_name, material_type) + assert result == expected + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "page_name, expected", + [ + ( + "homepage", "Website content on page 'homepage' updated." + ) + ] +) +async def test_update_website_content(page_name, expected): + """Test update of website content.""" + result = await MarketingTools.update_website_content(page_name) + assert result == expected + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "campaign_name, email_list_size, expected", + [ + ( + "Newsletter Blast", 2500, + "Email marketing managed for campaign 'Newsletter Blast' targeting 2500 recipients." + ) + ] +) +async def test_manage_email_marketing(campaign_name, email_list_size, expected): + """Test managing an email marketing campaign.""" + result = await MarketingTools.manage_email_marketing(campaign_name, email_list_size) + assert result == expected + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "key_info, expected_substring", + [ + ( + "Product XYZ release", "generate a press release based on this content Product XYZ release" + ) + ] +) +async def test_generate_press_release(key_info, expected_substring): + """Test generation of a press release.""" + result = await MarketingTools.generate_press_release(key_info) + assert expected_substring in result + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "platform, account_name, expected", + [ + ( + "Twitter", "BrandHandle", + "Social media account 'BrandHandle' on platform 'Twitter' managed." + ) + ] +) +async def test_manage_social_media_account(platform, account_name, expected): + """Test management of a social media account.""" + result = await MarketingTools.manage_social_media_account(platform, account_name) + assert result == expected + + +@pytest.mark.asyncio +async def test_create_marketing_campaign_empty_name(): + """Test creation of a marketing campaign with an empty name.""" + result = await MarketingTools.create_marketing_campaign("", "Adults", 1000.0) + assert "campaign" in result.lower() + + +def test_generate_tools_json_doc_contains_expected_keys(): + """Test that the generated JSON document contains expected keys.""" + tools_json = MarketingTools.generate_tools_json_doc() + tools_list = json.loads(tools_json) + assert isinstance(tools_list, list) + assert all("agent" in tool for tool in tools_list) + assert all("function" in tool for tool in tools_list) + assert all("description" in tool for tool in tools_list) + assert all("arguments" in tool for tool in tools_list) + # Optional: check presence of a known function + assert any(tool["function"] == "create_marketing_campaign" for tool in tools_list) + + +def test_get_all_kernel_functions_returns_expected_functions(): + """Test that get_all_kernel_functions returns expected functions.""" + kernel_funcs = MarketingTools.get_all_kernel_functions() + assert isinstance(kernel_funcs, dict) + assert "create_marketing_campaign" in kernel_funcs + assert callable(kernel_funcs["create_marketing_campaign"]) + + +@pytest.mark.asyncio +async def test_plan_advertising_budget(): + """Test planning of an advertising budget.""" + result = await MarketingTools.plan_advertising_budget("Winter Sale", 10000.0) + assert result == "Advertising budget planned for campaign 'Winter Sale' with a total budget of $10000.00." + + +@pytest.mark.asyncio +async def test_conduct_customer_survey(): + """Test conducting a customer survey.""" + result = await MarketingTools.conduct_customer_survey("Product Feedback", "Adults") + assert result == "Customer survey on 'Product Feedback' conducted targeting 'Adults'." + + +@pytest.mark.asyncio +async def test_perform_competitor_analysis(): + """Test competitor analysis.""" + result = await MarketingTools.perform_competitor_analysis("Competitor A") + assert result == "Competitor analysis performed on 'Competitor A'." + + +@pytest.mark.asyncio +async def test_track_campaign_performance(): + """Test tracking of campaign performance.""" + result = await MarketingTools.track_campaign_performance("Spring Promo") + assert result == "Performance of campaign 'Spring Promo' tracked." + + +@pytest.mark.asyncio +async def test_coordinate_with_sales_team(): + """Test coordination with the sales team.""" + result = await MarketingTools.coordinate_with_sales_team("Black Friday") + assert result == "Campaign 'Black Friday' coordinated with the sales team." + + +@pytest.mark.asyncio +async def test_develop_brand_strategy(): + """Test development of a brand strategy.""" + result = await MarketingTools.develop_brand_strategy("BrandX") + assert result == "Brand strategy developed for 'BrandX'." + + +@pytest.mark.asyncio +async def test_create_content_calendar(): + """Test creation of a content calendar.""" + result = await MarketingTools.create_content_calendar("August") + assert result == "Content calendar for 'August' created." + + +@pytest.mark.asyncio +async def test_plan_product_launch(): + """Test planning of a product launch.""" + result = await MarketingTools.plan_product_launch("GadgetPro", "2025-12-01") + assert result == "Product launch for 'GadgetPro' planned on 2025-12-01." + + +@pytest.mark.asyncio +async def test_conduct_market_research(): + """Test conducting market research.""" + result = await MarketingTools.conduct_market_research("Smartphones") + assert result == "Market research conducted on 'Smartphones'." + + +@pytest.mark.asyncio +async def test_handle_customer_feedback(): + """Test handling of customer feedback.""" + result = await MarketingTools.handle_customer_feedback("Great service") + assert result == "Customer feedback handled: Great service." + + +@pytest.mark.asyncio +async def test_generate_marketing_report(): + """Test generation of a marketing report.""" + result = await MarketingTools.generate_marketing_report("Holiday Campaign") + assert result == "Marketing report generated for campaign 'Holiday Campaign'." + + +@pytest.mark.asyncio +async def test_create_video_ad(): + """Test creation of a video advertisement.""" + result = await MarketingTools.create_video_ad("New Product", "YouTube") + assert result == "Video advertisement 'New Product' created for platform 'YouTube'." + + +@pytest.mark.asyncio +async def test_conduct_focus_group(): + """Test conducting a focus group study.""" + result = await MarketingTools.conduct_focus_group("Brand Awareness", 15) + assert result == "Focus group study on 'Brand Awareness' conducted with 15 participants." + + +@pytest.mark.asyncio +async def test_update_brand_guidelines(): + """Test update of brand guidelines.""" + result = await MarketingTools.update_brand_guidelines("BrandY", "New colors and fonts") + assert result == "Brand guidelines for 'BrandY' updated." + + +@pytest.mark.asyncio +async def test_handle_influencer_collaboration(): + """Test handling of influencer collaboration.""" + result = await MarketingTools.handle_influencer_collaboration("InfluencerX", "Summer Blast") + assert result == "Collaboration with influencer 'InfluencerX' for campaign 'Summer Blast' handled." + + +@pytest.mark.asyncio +async def test_analyze_customer_behavior(): + """Test analysis of customer behavior in a specific segment.""" + result = await MarketingTools.analyze_customer_behavior("Teenagers") + assert result == "Customer behavior in segment 'Teenagers' analyzed." + + +@pytest.mark.asyncio +async def test_manage_loyalty_program(): + """Test management of a loyalty program.""" + result = await MarketingTools.manage_loyalty_program("RewardsPlus", 1200) + assert result == "Loyalty program 'RewardsPlus' managed with 1200 members." + + +@pytest.mark.asyncio +async def test_develop_content_strategy(): + """Test development of a content strategy.""" + result = await MarketingTools.develop_content_strategy("Video Focus") + assert result == "Content strategy 'Video Focus' developed." + + +@pytest.mark.asyncio +async def test_create_infographic(): + """Test creation of an infographic.""" + result = await MarketingTools.create_infographic("Market Growth 2025") + assert result == "Infographic 'Market Growth 2025' created." + + +@pytest.mark.asyncio +async def test_schedule_webinar(): + """Test scheduling of a webinar.""" + result = await MarketingTools.schedule_webinar("Q3 Update", "2025-07-10", "Zoom") + assert result == "Webinar 'Q3 Update' scheduled on 2025-07-10 via Zoom." + + +@pytest.mark.asyncio +async def test_manage_online_reputation(): + """Test management of online reputation.""" + result = await MarketingTools.manage_online_reputation("BrandZ") + assert result == "Online reputation for 'BrandZ' managed." + + +@pytest.mark.asyncio +async def test_run_email_ab_testing(): + """Test running A/B testing for email campaigns.""" + result = await MarketingTools.run_email_ab_testing("Email Campaign 1") + assert result == "A/B testing for email campaign 'Email Campaign 1' run." + + +@pytest.mark.asyncio +async def test_create_podcast_episode(): + """Test creation of a podcast episode.""" + result = await MarketingTools.create_podcast_episode("Tech Talk", "AI Trends") + assert result == "Podcast episode 'AI Trends' for series 'Tech Talk' created." + + +@pytest.mark.asyncio +async def test_manage_affiliate_program(): + """Test management of an affiliate program.""" + result = await MarketingTools.manage_affiliate_program("AffiliatePro", 50) + assert result == "Affiliate program 'AffiliatePro' managed with 50 affiliates." + + +@pytest.mark.asyncio +async def test_generate_lead_magnets(): + """Test generation of lead magnets.""" + result = await MarketingTools.generate_lead_magnets("Free Guide") + assert result == "Lead magnet 'Free Guide' generated." + + +@pytest.mark.asyncio +async def test_organize_trade_show(): + """Test organization of a trade show.""" + result = await MarketingTools.organize_trade_show("B12", "Global Expo") + assert result == "Trade show 'Global Expo' organized at booth number 'B12'." + + +@pytest.mark.asyncio +async def test_manage_retention_program(): + """Test management of a customer retention program.""" + result = await MarketingTools.manage_retention_program("RetentionX") + assert result == "Customer retention program 'RetentionX' managed." + + +@pytest.mark.asyncio +async def test_run_ppc_campaign(): + """Test running a pay-per-click campaign.""" + result = await MarketingTools.run_ppc_campaign("PPC Spring", 15000.0) + assert result == "PPC campaign 'PPC Spring' run with a budget of $15000.00." + + +@pytest.mark.asyncio +async def test_create_case_study(): + """Test creation of a case study.""" + result = await MarketingTools.create_case_study("Success Story", "Client A") + assert result == "Case study 'Success Story' for client 'Client A' created." + + +@pytest.mark.asyncio +async def test_generate_lead_nurturing_emails(): + """Test generation of lead nurturing emails.""" + result = await MarketingTools.generate_lead_nurturing_emails("Welcome Sequence", 5) + assert result == "Lead nurturing email sequence 'Welcome Sequence' generated with 5 steps." + + +@pytest.mark.asyncio +async def test_manage_crisis_communication(): + """Test management of crisis communication.""" + result = await MarketingTools.manage_crisis_communication("Product Recall") + assert result == "Crisis communication managed for situation 'Product Recall'." + + +@pytest.mark.asyncio +async def test_create_interactive_content(): + """Test creation of interactive content.""" + result = await MarketingTools.create_interactive_content("Interactive Quiz") + assert result == "Interactive content 'Interactive Quiz' created." + + +@pytest.mark.asyncio +async def test_handle_media_relations(): + """Test handling of media relations.""" + result = await MarketingTools.handle_media_relations("Tech Daily") + assert result == "Media relations handled with 'Tech Daily'." + + +@pytest.mark.asyncio +async def test_create_testimonial_video(): + """Test creation of a testimonial video.""" + result = await MarketingTools.create_testimonial_video("Client B") + assert result == "Testimonial video created for client 'Client B'." + + +@pytest.mark.asyncio +async def test_manage_event_sponsorship(): + """Test management of event sponsorship.""" + result = await MarketingTools.manage_event_sponsorship("Tech Conference", "SponsorCorp") + assert result == "Event sponsorship for 'Tech Conference' managed with sponsor 'SponsorCorp'." + + +@pytest.mark.asyncio +async def test_optimize_conversion_funnel(): + """Test optimization of a conversion funnel stage.""" + result = await MarketingTools.optimize_conversion_funnel("Checkout") + assert result == "Conversion funnel stage 'Checkout' optimized." + + +@pytest.mark.asyncio +async def test_run_influencer_campaign(): + """Test running an influencer marketing campaign.""" + result = await MarketingTools.run_influencer_campaign("Winter Campaign", ["Influencer1", "Influencer2"]) + assert result == "Influencer marketing campaign 'Winter Campaign' run with influencers: Influencer1, Influencer2." + + +@pytest.mark.asyncio +async def test_analyze_website_traffic(): + """Test analysis of website traffic from a specific source.""" + result = await MarketingTools.analyze_website_traffic("Google Ads") + assert result == "Website traffic analyzed from source 'Google Ads'." + + +@pytest.mark.asyncio +async def test_develop_customer_personas(): + """Test development of customer personas for a market segment.""" + result = await MarketingTools.develop_customer_personas("Millennials") + assert result == "Customer personas developed for segment 'Millennials'." diff --git a/src/tests/backend/middleware/__init__.py b/src/tests/backend/middleware/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/backend/tests/middleware/test_health_check.py b/src/tests/backend/middleware/test_health_check.py similarity index 100% rename from src/backend/tests/middleware/test_health_check.py rename to src/tests/backend/middleware/test_health_check.py diff --git a/src/tests/backend/models/__init__.py b/src/tests/backend/models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/backend/tests/models/test_messages.py b/src/tests/backend/models/test_messages.py similarity index 100% rename from src/backend/tests/models/test_messages.py rename to src/tests/backend/models/test_messages.py diff --git a/src/tests/backend/models/test_messages_kernel.py b/src/tests/backend/models/test_messages_kernel.py new file mode 100644 index 00000000..7968c720 --- /dev/null +++ b/src/tests/backend/models/test_messages_kernel.py @@ -0,0 +1,80 @@ +import pytest +from datetime import datetime +from src.backend.models.messages_kernel import ( + GetHumanInputMessage, GroupChatMessage, DataType, AgentType, + StepStatus, PlanStatus, HumanFeedbackStatus, MessageRole, + ChatMessage, StoredMessage, AgentMessage, Session, + Plan, Step, ThreadIdAgent, AzureIdAgent, PlanWithSteps +) + +def test_get_human_input_message(): + msg = GetHumanInputMessage(content="Need your input") + assert msg.content == "Need your input" + +def test_group_chat_message_str(): + msg = GroupChatMessage(body={"content": "Hello"}, source="tester", session_id="abc123") + assert "GroupChatMessage" in str(msg) + assert "tester" in str(msg) + assert "Hello" in str(msg) + +def test_chat_message_to_semantic_kernel_dict(): + chat_msg = ChatMessage(role=MessageRole.user, content="Test message") + sk_dict = chat_msg.to_semantic_kernel_dict() + assert sk_dict["role"] == "user" + assert sk_dict["content"] == "Test message" + assert isinstance(sk_dict["metadata"], dict) + +def test_stored_message_to_chat_message(): + stored = StoredMessage( + session_id="s1", user_id="u1", role=MessageRole.assistant, content="reply", + plan_id="p1", step_id="step1", source="source" + ) + chat = stored.to_chat_message() + assert chat.role == MessageRole.assistant + assert chat.content == "reply" + assert chat.metadata["plan_id"] == "p1" + +def test_agent_message_fields(): + agent_msg = AgentMessage( + session_id="s", user_id="u", plan_id="p", content="hi", source="system" + ) + assert agent_msg.data_type == "agent_message" + assert agent_msg.content == "hi" + +def test_session_defaults(): + session = Session(user_id="u", current_status="active") + assert session.data_type == "session" + assert session.current_status == "active" + +def test_plan_status_and_source(): + plan = Plan(session_id="s", user_id="u", initial_goal="goal") + assert plan.overall_status == PlanStatus.in_progress + assert plan.source == AgentType.PLANNER + +def test_step_defaults(): + step = Step(plan_id="p", session_id="s", user_id="u", action="act", agent=AgentType.HUMAN) + assert step.status == StepStatus.planned + assert step.human_approval_status == HumanFeedbackStatus.requested + +def test_thread_id_agent(): + thread = ThreadIdAgent(session_id="s", user_id="u", thread_id="t1") + assert thread.data_type == "thread" + assert thread.thread_id == "t1" + +def test_azure_id_agent(): + azure = AzureIdAgent(session_id="s", user_id="u", action="a", agent=AgentType.HR, agent_id="a1") + assert azure.agent == AgentType.HR + assert azure.agent_id == "a1" + +def test_plan_with_steps_update_counts(): + steps = [ + Step(plan_id="p", session_id="s", user_id="u", action="a1", agent=AgentType.HR, status=StepStatus.planned), + Step(plan_id="p", session_id="s", user_id="u", action="a2", agent=AgentType.HR, status=StepStatus.completed), + Step(plan_id="p", session_id="s", user_id="u", action="a3", agent=AgentType.HR, status=StepStatus.failed), + ] + plan_with_steps = PlanWithSteps(session_id="s", user_id="u", initial_goal="goal", steps=steps) + plan_with_steps.update_step_counts() + assert plan_with_steps.total_steps == 3 + assert plan_with_steps.planned == 1 + assert plan_with_steps.completed == 1 + assert plan_with_steps.failed == 1 diff --git a/src/backend/tests/test_group_chat_manager_integration.py b/src/tests/backend/test_group_chat_manager_integration.py similarity index 100% rename from src/backend/tests/test_group_chat_manager_integration.py rename to src/tests/backend/test_group_chat_manager_integration.py diff --git a/src/backend/tests/test_hr_agent_integration.py b/src/tests/backend/test_hr_agent_integration.py similarity index 100% rename from src/backend/tests/test_hr_agent_integration.py rename to src/tests/backend/test_hr_agent_integration.py diff --git a/src/backend/tests/test_human_agent_integration.py b/src/tests/backend/test_human_agent_integration.py similarity index 100% rename from src/backend/tests/test_human_agent_integration.py rename to src/tests/backend/test_human_agent_integration.py diff --git a/src/backend/tests/test_multiple_agents_integration.py b/src/tests/backend/test_multiple_agents_integration.py similarity index 100% rename from src/backend/tests/test_multiple_agents_integration.py rename to src/tests/backend/test_multiple_agents_integration.py diff --git a/src/backend/tests/test_otlp_tracing.py b/src/tests/backend/test_otlp_tracing.py similarity index 100% rename from src/backend/tests/test_otlp_tracing.py rename to src/tests/backend/test_otlp_tracing.py diff --git a/src/backend/tests/test_planner_agent_integration.py b/src/tests/backend/test_planner_agent_integration.py similarity index 100% rename from src/backend/tests/test_planner_agent_integration.py rename to src/tests/backend/test_planner_agent_integration.py