diff --git a/docs/CustomizingAzdParameters.md b/docs/CustomizingAzdParameters.md index 1efd8acc..ec8f5d74 100644 --- a/docs/CustomizingAzdParameters.md +++ b/docs/CustomizingAzdParameters.md @@ -18,7 +18,7 @@ By default this template will use the environment name as the prefix to prevent | `AZURE_ENV_MODEL_CAPACITY` | int | `150` | Sets the GPT model capacity. | | `AZURE_ENV_IMAGETAG` | string | `latest` | Docker image tag used for container deployments. | | `AZURE_ENV_ENABLE_TELEMETRY` | bool | `true` | Enables telemetry for monitoring and diagnostics. | -| `AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID` | string | `` | Set this if you want to reuse an existing Log Analytics Workspace instead of creating a new one. | +| `AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID` | string | Guide to get your [Existing Workspace ID](/docs/re-use-log-analytics.md) | Set this if you want to reuse an existing Log Analytics Workspace instead of creating a new one. | --- ## How to Set a Parameter diff --git a/infra/modules/role.bicep b/infra/modules/role.bicep index f700f092..ba07c0ae 100644 --- a/infra/modules/role.bicep +++ b/infra/modules/role.bicep @@ -29,6 +29,7 @@ resource aiUserAccessFoundry 'Microsoft.Authorization/roleAssignments@2022-04-01 properties: { roleDefinitionId: aiUser.id principalId: principalId + principalType: 'ServicePrincipal' } } @@ -38,6 +39,7 @@ resource aiDeveloperAccessFoundry 'Microsoft.Authorization/roleAssignments@2022- properties: { roleDefinitionId: aiDeveloper.id principalId: principalId + principalType: 'ServicePrincipal' } } @@ -47,5 +49,6 @@ resource cognitiveServiceOpenAIUserAccessFoundry 'Microsoft.Authorization/roleAs properties: { roleDefinitionId: cognitiveServiceOpenAIUser.id principalId: principalId + principalType: 'ServicePrincipal' } } diff --git a/infra/old/deploy_ai_foundry.bicep b/infra/old/deploy_ai_foundry.bicep index 11b40bf0..9f29af12 100644 --- a/infra/old/deploy_ai_foundry.bicep +++ b/infra/old/deploy_ai_foundry.bicep @@ -169,6 +169,7 @@ resource aiDevelopertoAIProject 'Microsoft.Authorization/roleAssignments@2022-04 properties: { roleDefinitionId: aiDeveloper.id principalId: aiHubProject.identity.principalId + principalType: 'ServicePrincipal' } } diff --git a/infra/old/main.bicep b/infra/old/main.bicep index 661973ff..c84added 100644 --- a/infra/old/main.bicep +++ b/infra/old/main.bicep @@ -680,6 +680,7 @@ module aiFoundryStorageAccount 'br/public:avm/res/storage/storage-account:0.18.2 { principalId: userAssignedIdentity.outputs.principalId roleDefinitionIdOrName: 'Storage Blob Data Contributor' + principalType: 'ServicePrincipal' } ] } @@ -760,6 +761,7 @@ module aiFoundryAiProject 'br/public:avm/res/machine-learning-services/workspace principalId: containerApp.outputs.?systemAssignedMIPrincipalId! // Assigning the role with the role name instead of the role ID freezes the deployment at this point roleDefinitionIdOrName: '64702f94-c441-49e6-a78b-ef80e0188fee' //'Azure AI Developer' + principalType: 'ServicePrincipal' } ] } diff --git a/infra/scripts/quota_check_params.sh b/infra/scripts/quota_check_params.sh index 6182e449..f1a15f93 100644 --- a/infra/scripts/quota_check_params.sh +++ b/infra/scripts/quota_check_params.sh @@ -164,11 +164,7 @@ for REGION in "${REGIONS[@]}"; do FOUND=false INSUFFICIENT_QUOTA=false - if [ "$MODEL_NAME" = "text-embedding-ada-002" ]; then - MODEL_TYPES=("openai.standard.$MODEL_NAME") - else - MODEL_TYPES=("openai.standard.$MODEL_NAME" "openai.globalstandard.$MODEL_NAME") - fi + MODEL_TYPES=("openai.standard.$MODEL_NAME" "openai.globalstandard.$MODEL_NAME") for MODEL_TYPE in "${MODEL_TYPES[@]}"; do FOUND=false diff --git a/infra/scripts/validate_model_quota.ps1 b/infra/scripts/validate_model_quota.ps1 index fc217b99..7afe3773 100644 --- a/infra/scripts/validate_model_quota.ps1 +++ b/infra/scripts/validate_model_quota.ps1 @@ -1,7 +1,7 @@ param ( [string]$Location, [string]$Model, - [string]$DeploymentType = "Standard", + [string]$DeploymentType = "GlobalStandard", [int]$Capacity ) diff --git a/infra/scripts/validate_model_quota.sh b/infra/scripts/validate_model_quota.sh index ae56ae0f..5cf71f96 100644 --- a/infra/scripts/validate_model_quota.sh +++ b/infra/scripts/validate_model_quota.sh @@ -2,7 +2,7 @@ LOCATION="" MODEL="" -DEPLOYMENT_TYPE="Standard" +DEPLOYMENT_TYPE="GlobalStandard" CAPACITY=0 ALL_REGIONS=('australiaeast' 'eastus2' 'francecentral' 'japaneast' 'norwayeast' 'swedencentral' 'uksouth' 'westus') diff --git a/src/backend/app_kernel.py b/src/backend/app_kernel.py index e0e81abd..d3a65d81 100644 --- a/src/backend/app_kernel.py +++ b/src/backend/app_kernel.py @@ -1,5 +1,6 @@ # app_kernel.py import asyncio +import json import logging import os import uuid @@ -17,7 +18,7 @@ from event_utils import track_event_if_configured # FastAPI imports -from fastapi import FastAPI, HTTPException, Query, Request +from fastapi import FastAPI, HTTPException, Query, Request, UploadFile, File from fastapi.middleware.cors import CORSMiddleware from kernel_agents.agent_factory import AgentFactory @@ -31,8 +32,10 @@ InputTask, PlanWithSteps, Step, - UserLanguage + TeamConfiguration, + UserLanguage, ) +from services.json_service import JsonService # Updated import for KernelArguments from utils_kernel import initialize_runtime_and_context, rai_success @@ -98,13 +101,13 @@ def format_dates_in_messages(messages, target_locale="en-US"): """ # Define target format patterns per locale locale_date_formats = { - "en-IN": "%d %b %Y", # 30 Jul 2025 - "en-US": "%b %d, %Y", # Jul 30, 2025 + "en-IN": "%d %b %Y", # 30 Jul 2025 + "en-US": "%b %d, %Y", # Jul 30, 2025 } output_format = locale_date_formats.get(target_locale, "%d %b %Y") # Match both "Jul 30, 2025, 12:00:00 AM" and "30 Jul 2025" - date_pattern = r'(\d{1,2} [A-Za-z]{3,9} \d{4}|[A-Za-z]{3,9} \d{1,2}, \d{4}(, \d{1,2}:\d{2}:\d{2} ?[APap][Mm])?)' + date_pattern = r"(\d{1,2} [A-Za-z]{3,9} \d{4}|[A-Za-z]{3,9} \d{1,2}, \d{4}(, \d{1,2}:\d{2}:\d{2} ?[APap][Mm])?)" def convert_date(match): date_str = match.group(0) @@ -118,11 +121,15 @@ def convert_date(match): if isinstance(messages, list): formatted_messages = [] for message in messages: - if hasattr(message, 'content') and message.content: + if hasattr(message, "content") and message.content: # Create a copy of the message with formatted content - formatted_message = message.model_copy() if hasattr(message, 'model_copy') else message - if hasattr(formatted_message, 'content'): - formatted_message.content = re.sub(date_pattern, convert_date, formatted_message.content) + formatted_message = ( + message.model_copy() if hasattr(message, "model_copy") else message + ) + if hasattr(formatted_message, "content"): + formatted_message.content = re.sub( + date_pattern, convert_date, formatted_message.content + ) formatted_messages.append(formatted_message) else: formatted_messages.append(message) @@ -134,10 +141,7 @@ def convert_date(match): @app.post("/api/user_browser_language") -async def user_browser_language_endpoint( - user_language: UserLanguage, - request: Request -): +async def user_browser_language_endpoint(user_language: UserLanguage, request: Request): """ Receive the user's browser language. @@ -267,9 +271,13 @@ async def input_task_endpoint(input_task: InputTask, request: Request): # Extract clean error message for rate limit errors error_msg = str(e) if "Rate limit is exceeded" in error_msg: - match = re.search(r"Rate limit is exceeded\. Try again in (\d+) seconds?\.", error_msg) + match = re.search( + r"Rate limit is exceeded\. Try again in (\d+) seconds?\.", error_msg + ) if match: - error_msg = f"Rate limit is exceeded. Try again in {match.group(1)} seconds." + error_msg = ( + f"Rate limit is exceeded. Try again in {match.group(1)} seconds." + ) track_event_if_configured( "InputTaskError", @@ -279,7 +287,9 @@ async def input_task_endpoint(input_task: InputTask, request: Request): "error": str(e), }, ) - raise HTTPException(status_code=400, detail=f"Error creating plan: {error_msg}") from e + raise HTTPException( + status_code=400, detail=f"Error creating plan: {error_msg}" + ) from e @app.post("/api/human_feedback") @@ -734,7 +744,9 @@ async def get_plans( plan_with_steps.update_step_counts() # Format dates in messages according to locale - formatted_messages = format_dates_in_messages(messages, config.get_user_local_browser_language()) + formatted_messages = format_dates_in_messages( + messages, config.get_user_local_browser_language() + ) return [plan_with_steps, formatted_messages] @@ -1080,6 +1092,351 @@ async def get_agent_tools(): return [] +@app.post("/api/upload_team_config") +async def upload_team_config_endpoint(request: Request, file: UploadFile = File(...)): + """ + Upload and save a team configuration JSON file. + + --- + tags: + - Team Configuration + parameters: + - name: user_principal_id + in: header + type: string + required: true + description: User ID extracted from the authentication header + - name: file + in: formData + type: file + required: true + description: JSON file containing team configuration + responses: + 200: + description: Team configuration uploaded successfully + schema: + type: object + properties: + status: + type: string + config_id: + type: string + team_id: + type: string + name: + type: string + 400: + description: Invalid request or file format + 401: + description: Missing or invalid user information + 500: + description: Internal server error + """ + # Validate user authentication + authenticated_user = get_authenticated_user_details(request_headers=request.headers) + user_id = authenticated_user["user_principal_id"] + if not user_id: + raise HTTPException( + status_code=401, detail="Missing or invalid user information" + ) + + # Validate file is provided and is JSON + if not file: + raise HTTPException(status_code=400, detail="No file provided") + + if not file.filename.endswith(".json"): + raise HTTPException(status_code=400, detail="File must be a JSON file") + + try: + # Read and parse JSON content + content = await file.read() + try: + json_data = json.loads(content.decode("utf-8")) + except json.JSONDecodeError as e: + raise HTTPException( + status_code=400, detail=f"Invalid JSON format: {str(e)}" + ) + + # Initialize memory store and service + kernel, memory_store = await initialize_runtime_and_context("", user_id) + json_service = JsonService(memory_store) + + # Validate and parse the team configuration + try: + team_config = await json_service.validate_and_parse_team_config( + json_data, user_id + ) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + + # Save the configuration + try: + config_id = await json_service.save_team_configuration(team_config) + except ValueError as e: + raise HTTPException( + status_code=500, detail=f"Failed to save configuration: {str(e)}" + ) + + # Track the event + track_event_if_configured( + "Team configuration uploaded", + { + "status": "success", + "config_id": config_id, + "team_id": team_config.team_id, + "user_id": user_id, + "agents_count": len(team_config.agents), + "tasks_count": len(team_config.starting_tasks), + }, + ) + + return { + "status": "success", + "config_id": config_id, + "team_id": team_config.team_id, + "name": team_config.name, + "message": "Team configuration uploaded and saved successfully", + } + + except HTTPException: + # Re-raise HTTP exceptions + raise + except Exception as e: + # Log and return generic error for unexpected exceptions + logging.error(f"Unexpected error uploading team configuration: {str(e)}") + raise HTTPException(status_code=500, detail="Internal server error occurred") + + +@app.get("/api/team_configs") +async def get_team_configs_endpoint(request: Request): + """ + Retrieve all team configurations for the current user. + + --- + tags: + - Team Configuration + parameters: + - name: user_principal_id + in: header + type: string + required: true + description: User ID extracted from the authentication header + responses: + 200: + description: List of team configurations for the user + schema: + type: array + items: + type: object + properties: + id: + type: string + team_id: + type: string + name: + type: string + status: + type: string + created: + type: string + created_by: + type: string + description: + type: string + logo: + type: string + plan: + type: string + agents: + type: array + starting_tasks: + type: array + 401: + description: Missing or invalid user information + """ + # Validate user authentication + authenticated_user = get_authenticated_user_details(request_headers=request.headers) + user_id = authenticated_user["user_principal_id"] + if not user_id: + raise HTTPException( + status_code=401, detail="Missing or invalid user information" + ) + + try: + # Initialize memory store and service + kernel, memory_store = await initialize_runtime_and_context("", user_id) + json_service = JsonService(memory_store) + + # Retrieve all team configurations + team_configs = await json_service.get_all_team_configurations(user_id) + + # Convert to dictionaries for response + configs_dict = [config.model_dump() for config in team_configs] + + return configs_dict + + except Exception as e: + logging.error(f"Error retrieving team configurations: {str(e)}") + raise HTTPException(status_code=500, detail="Internal server error occurred") + + +@app.get("/api/team_configs/{config_id}") +async def get_team_config_by_id_endpoint(config_id: str, request: Request): + """ + Retrieve a specific team configuration by ID. + + --- + tags: + - Team Configuration + parameters: + - name: config_id + in: path + type: string + required: true + description: The ID of the team configuration to retrieve + - name: user_principal_id + in: header + type: string + required: true + description: User ID extracted from the authentication header + responses: + 200: + description: Team configuration details + schema: + type: object + properties: + id: + type: string + team_id: + type: string + name: + type: string + status: + type: string + created: + type: string + created_by: + type: string + description: + type: string + logo: + type: string + plan: + type: string + agents: + type: array + starting_tasks: + type: array + 401: + description: Missing or invalid user information + 404: + description: Team configuration not found + """ + # Validate user authentication + authenticated_user = get_authenticated_user_details(request_headers=request.headers) + user_id = authenticated_user["user_principal_id"] + if not user_id: + raise HTTPException( + status_code=401, detail="Missing or invalid user information" + ) + + try: + # Initialize memory store and service + kernel, memory_store = await initialize_runtime_and_context("", user_id) + json_service = JsonService(memory_store) + + # Retrieve the specific team configuration + team_config = await json_service.get_team_configuration(config_id, user_id) + + if team_config is None: + raise HTTPException(status_code=404, detail="Team configuration not found") + + # Convert to dictionary for response + return team_config.model_dump() + + except HTTPException: + # Re-raise HTTP exceptions + raise + except Exception as e: + logging.error(f"Error retrieving team configuration: {str(e)}") + raise HTTPException(status_code=500, detail="Internal server error occurred") + + +@app.delete("/api/team_configs/{config_id}") +async def delete_team_config_endpoint(config_id: str, request: Request): + """ + Delete a team configuration by ID. + + --- + tags: + - Team Configuration + parameters: + - name: config_id + in: path + type: string + required: true + description: The ID of the team configuration to delete + - name: user_principal_id + in: header + type: string + required: true + description: User ID extracted from the authentication header + responses: + 200: + description: Team configuration deleted successfully + schema: + type: object + properties: + status: + type: string + message: + type: string + config_id: + type: string + 401: + description: Missing or invalid user information + 404: + description: Team configuration not found + """ + # Validate user authentication + authenticated_user = get_authenticated_user_details(request_headers=request.headers) + user_id = authenticated_user["user_principal_id"] + if not user_id: + raise HTTPException( + status_code=401, detail="Missing or invalid user information" + ) + + try: + # Initialize memory store and service + kernel, memory_store = await initialize_runtime_and_context("", user_id) + json_service = JsonService(memory_store) + + # Delete the team configuration + deleted = await json_service.delete_team_configuration(config_id, user_id) + + if not deleted: + raise HTTPException(status_code=404, detail="Team configuration not found") + + # Track the event + track_event_if_configured( + "Team configuration deleted", + {"status": "success", "config_id": config_id, "user_id": user_id}, + ) + + return { + "status": "success", + "message": "Team configuration deleted successfully", + "config_id": config_id, + } + + except HTTPException: + # Re-raise HTTP exceptions + raise + except Exception as e: + logging.error(f"Error deleting team configuration: {str(e)}") + raise HTTPException(status_code=500, detail="Internal server error occurred") + + # Run the app if __name__ == "__main__": import uvicorn diff --git a/src/backend/models/messages_kernel.py b/src/backend/models/messages_kernel.py index 533af6aa..bc8f4366 100644 --- a/src/backend/models/messages_kernel.py +++ b/src/backend/models/messages_kernel.py @@ -93,7 +93,9 @@ class BaseDataModel(KernelBaseModel): """Base data model with common fields.""" id: str = Field(default_factory=lambda: str(uuid.uuid4())) - timestamp: Optional[datetime] = Field(default_factory=lambda: datetime.now(timezone.utc)) + timestamp: Optional[datetime] = Field( + default_factory=lambda: datetime.now(timezone.utc) + ) # Basic message class for Semantic Kernel compatibility @@ -214,6 +216,46 @@ class AzureIdAgent(BaseDataModel): agent_id: str +class TeamAgent(KernelBaseModel): + """Represents an agent within a team.""" + + input_key: str + type: str + name: str + system_message: str = "" + description: str = "" + icon: str + index_name: str = "" + + +class StartingTask(KernelBaseModel): + """Represents a starting task for a team.""" + + id: str + name: str + prompt: str + created: str + creator: str + logo: str + + +class TeamConfiguration(BaseDataModel): + """Represents a team configuration stored in the database.""" + + data_type: Literal["team_config"] = Field("team_config", Literal=True) + team_id: str + name: str + status: str + created: str + created_by: str + agents: List[TeamAgent] = Field(default_factory=list) + description: str = "" + logo: str = "" + plan: str = "" + starting_tasks: List[StartingTask] = Field(default_factory=list) + user_id: str # Who uploaded this configuration + + class PlanWithSteps(Plan): """Plan model that includes the associated steps.""" diff --git a/src/backend/services/__init__.py b/src/backend/services/__init__.py new file mode 100644 index 00000000..a70b3029 --- /dev/null +++ b/src/backend/services/__init__.py @@ -0,0 +1 @@ +# Services package diff --git a/src/backend/services/json_service.py b/src/backend/services/json_service.py new file mode 100644 index 00000000..0bd53f61 --- /dev/null +++ b/src/backend/services/json_service.py @@ -0,0 +1,271 @@ +import logging +from typing import Dict, Any, List, Optional + +from ..models.messages_kernel import TeamConfiguration, TeamAgent, StartingTask + + +class JsonService: + """Service for handling JSON team configuration operations.""" + + def __init__(self, memory_store): + """Initialize with memory store.""" + self.memory_store = memory_store + self.logger = logging.getLogger(__name__) + + async def validate_and_parse_team_config( + self, json_data: Dict[str, Any], user_id: str + ) -> TeamConfiguration: + """ + Validate and parse team configuration JSON. + + Args: + json_data: Raw JSON data + user_id: User ID who uploaded the configuration + + Returns: + TeamConfiguration object + + Raises: + ValueError: If JSON structure is invalid + """ + try: + # Validate required top-level fields + required_fields = [ + "id", + "team_id", + "name", + "status", + "created", + "created_by", + ] + for field in required_fields: + if field not in json_data: + raise ValueError(f"Missing required field: {field}") + + # Validate agents array exists and is not empty + if "agents" not in json_data or not isinstance(json_data["agents"], list): + raise ValueError( + "Missing or invalid 'agents' field - must be a non-empty array" + ) + + if len(json_data["agents"]) == 0: + raise ValueError("Agents array cannot be empty") + + # Validate starting_tasks array exists and is not empty + if "starting_tasks" not in json_data or not isinstance( + json_data["starting_tasks"], list + ): + raise ValueError( + "Missing or invalid 'starting_tasks' field - must be a non-empty array" + ) + + if len(json_data["starting_tasks"]) == 0: + raise ValueError("Starting tasks array cannot be empty") + + # Parse agents + agents = [] + for agent_data in json_data["agents"]: + agent = self._validate_and_parse_agent(agent_data) + agents.append(agent) + + # Parse starting tasks + starting_tasks = [] + for task_data in json_data["starting_tasks"]: + task = self._validate_and_parse_task(task_data) + starting_tasks.append(task) + + # Create team configuration + team_config = TeamConfiguration( + team_id=json_data["team_id"], + name=json_data["name"], + status=json_data["status"], + created=json_data["created"], + created_by=json_data["created_by"], + agents=agents, + description=json_data.get("description", ""), + logo=json_data.get("logo", ""), + plan=json_data.get("plan", ""), + starting_tasks=starting_tasks, + user_id=user_id, + ) + + self.logger.info( + "Successfully validated team configuration: %s", team_config.team_id + ) + return team_config + + except Exception as e: + self.logger.error("Error validating team configuration: %s", str(e)) + raise ValueError(f"Invalid team configuration: {str(e)}") from e + + def _validate_and_parse_agent(self, agent_data: Dict[str, Any]) -> TeamAgent: + """Validate and parse a single agent.""" + required_fields = ["input_key", "type", "name", "icon"] + for field in required_fields: + if field not in agent_data: + raise ValueError(f"Agent missing required field: {field}") + + return TeamAgent( + input_key=agent_data["input_key"], + type=agent_data["type"], + name=agent_data["name"], + system_message=agent_data.get("system_message", ""), + description=agent_data.get("description", ""), + icon=agent_data["icon"], + index_name=agent_data.get("index_name", ""), + ) + + def _validate_and_parse_task(self, task_data: Dict[str, Any]) -> StartingTask: + """Validate and parse a single starting task.""" + required_fields = ["id", "name", "prompt", "created", "creator", "logo"] + for field in required_fields: + if field not in task_data: + raise ValueError(f"Starting task missing required field: {field}") + + return StartingTask( + id=task_data["id"], + name=task_data["name"], + prompt=task_data["prompt"], + created=task_data["created"], + creator=task_data["creator"], + logo=task_data["logo"], + ) + + async def save_team_configuration(self, team_config: TeamConfiguration) -> str: + """ + Save team configuration to the database. + + Args: + team_config: TeamConfiguration object to save + + Returns: + The unique ID of the saved configuration + """ + try: + # Convert to dictionary for storage + config_dict = team_config.model_dump() + + # Save to memory store + await self.memory_store.upsert_async( + f"team_config_{team_config.user_id}", config_dict + ) + + self.logger.info( + "Successfully saved team configuration with ID: %s", team_config.id + ) + return team_config.id + + except Exception as e: + self.logger.error("Error saving team configuration: %s", str(e)) + raise ValueError(f"Failed to save team configuration: {str(e)}") from e + + async def get_team_configuration( + self, config_id: str, user_id: str + ) -> Optional[TeamConfiguration]: + """ + Retrieve a team configuration by ID. + + Args: + config_id: Configuration ID to retrieve + user_id: User ID for access control + + Returns: + TeamConfiguration object or None if not found + """ + try: + # Query from memory store + configs = await self.memory_store.query_items( + f"team_config_{user_id}", limit=1000 + ) + + for config_dict in configs: + if config_dict.get("id") == config_id: + return TeamConfiguration.model_validate(config_dict) + + return None + + except (KeyError, TypeError, ValueError) as e: + self.logger.error("Error retrieving team configuration: %s", str(e)) + return None + + async def get_all_team_configurations( + self, user_id: str + ) -> List[TeamConfiguration]: + """ + Retrieve all team configurations for a user. + + Args: + user_id: User ID to retrieve configurations for + + Returns: + List of TeamConfiguration objects + """ + try: + # Query from memory store + configs = await self.memory_store.query_items( + f"team_config_{user_id}", limit=1000 + ) + + team_configs = [] + for config_dict in configs: + try: + team_config = TeamConfiguration.model_validate(config_dict) + team_configs.append(team_config) + except (ValueError, TypeError) as e: + self.logger.warning( + "Failed to parse team configuration: %s", str(e) + ) + continue + + return team_configs + + except (KeyError, TypeError, ValueError) as e: + self.logger.error("Error retrieving team configurations: %s", str(e)) + return [] + + async def delete_team_configuration(self, config_id: str, user_id: str) -> bool: + """ + Delete a team configuration by ID. + + Args: + config_id: Configuration ID to delete + user_id: User ID for access control + + Returns: + True if deleted successfully, False if not found + """ + try: + # Get all configurations to find the one to delete + configs = await self.memory_store.query_items( + f"team_config_{user_id}", limit=1000 + ) + + # Find the configuration to delete + config_to_delete = None + remaining_configs = [] + + for config_dict in configs: + if config_dict.get("id") == config_id: + config_to_delete = config_dict + else: + remaining_configs.append(config_dict) + + if config_to_delete is None: + self.logger.warning( + "Team configuration not found for deletion: %s", config_id + ) + return False + + # Clear the collection + await self.memory_store.delete_collection_async(f"team_config_{user_id}") + + # Re-add remaining configurations + for config in remaining_configs: + await self.memory_store.upsert_async(f"team_config_{user_id}", config) + + self.logger.info("Successfully deleted team configuration: %s", config_id) + return True + + except (KeyError, TypeError, ValueError) as e: + self.logger.error("Error deleting team configuration: %s", str(e)) + return False