Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions aixplain/exceptions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,22 @@
NetworkError,
ServiceError,
InternalError,
AlreadyDeployedError,
)

__all__ = [
"AixplainBaseException",
"AuthenticationError",
"ValidationError",
"ResourceError",
"BillingError",
"SupplierError",
"NetworkError",
"ServiceError",
"InternalError",
"AlreadyDeployedError",
]


def get_error_from_status_code(status_code: int, error_details: str = None) -> AixplainBaseException:
"""
Expand Down
14 changes: 14 additions & 0 deletions aixplain/exceptions/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,3 +275,17 @@ def __init__(self, message: str, **kwargs):
error_code=ErrorCode.AX_INT_ERROR,
**kwargs,
)


class AlreadyDeployedError(AixplainBaseException):
"""Raised when an asset is already deployed."""

def __init__(self, message: str, **kwargs):
super().__init__(
message=message,
category=ErrorCategory.INTERNAL,
severity=ErrorSeverity.ERROR,
retry_recommended=kwargs.pop("retry_recommended", False),
error_code=ErrorCode.AX_INT_ERROR,
**kwargs,
)
32 changes: 21 additions & 11 deletions aixplain/modules/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@
Description:
Mixins for common functionality across different asset types
"""

from abc import ABC
from typing import TypeVar, Generic
from aixplain.enums import AssetStatus
from aixplain.exceptions import AlreadyDeployedError

T = TypeVar("T")

Expand Down Expand Up @@ -50,7 +52,7 @@ def _validate_deployment_readiness(self) -> None:
"""
asset_type = self.__class__.__name__
if self.status == AssetStatus.ONBOARDED:
raise ValueError(f"{asset_type} is already deployed.")
raise AlreadyDeployedError(f"{asset_type} is already deployed.")

if self.status != AssetStatus.DRAFT:
raise ValueError(f"{asset_type} must be in DRAFT status to be deployed.")
Expand All @@ -67,19 +69,27 @@ def deploy(self) -> None:
self._validate_deployment_readiness()
previous_status = self.status
try:
# Deploy tools if present
if hasattr(self, "tools"):
[tool.deploy() for tool in self.tools]
for tool in self.tools:
try:
tool.deploy()
except AlreadyDeployedError:
pass
except Exception as e:
raise Exception(f"Error deploying tool {tool.name}: {e}") from e

# Deploy agents if present (for TeamAgent)
if hasattr(self, "agents"):
undeployed_agents = [agent for agent in self.agents if agent.status != AssetStatus.ONBOARDED]
if undeployed_agents:
names = ", ".join(str(agent) for agent in undeployed_agents)
if names:
raise ValueError(
f"Agents not deployed: {names}. "
"Deploy them with `<agent>.deploy()` before running this command."
)


for agent in self.agents:
try:
agent.deploy()
except AlreadyDeployedError:
pass
except Exception as e:
raise Exception(f"Error deploying agent {agent.name}: {e}") from e

self.status = AssetStatus.ONBOARDED
self.update()
except Exception as e:
Expand Down
144 changes: 144 additions & 0 deletions tests/functional/team_agent/team_agent_functional_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,76 @@ def test_draft_team_agent_update(run_input_map, TeamAgentFactory):
assert team_agent.status == AssetStatus.DRAFT


@pytest.mark.parametrize("TeamAgentFactory", [TeamAgentFactory, v2.TeamAgent])
def test_nested_deployment_chain(delete_agents_and_team_agents, TeamAgentFactory):
"""Test that deploying a team agent properly deploys all nested components (tools -> agents -> team)"""
assert delete_agents_and_team_agents

# Create first agent with translation tool (in DRAFT state)
translation_function = Function.TRANSLATION
function_params = translation_function.get_parameters()
function_params.targetlanguage = "es"
function_params.sourcelanguage = "en"
translation_tool = AgentFactory.create_model_tool(
function=translation_function,
description="Translation tool from English to Spanish",
supplier="microsoft",
)

translation_agent = AgentFactory.create(
name="Translation Agent",
description="Agent for translation",
instructions="Translate text from English to Spanish",
llm_id="6646261c6eb563165658bbb1",
tools=[translation_tool],
)
assert translation_agent.status == AssetStatus.DRAFT
# Create second agent with text generation tool (in DRAFT state)
text_gen_tool = AgentFactory.create_model_tool(
function=Function.TEXT_GENERATION,
description="Text generation tool",
supplier="openai",
)

text_gen_agent = AgentFactory.create(
name="Text Generation Agent",
description="Agent for text generation",
instructions="Generate creative text based on input",
llm_id="6646261c6eb563165658bbb1",
tools=[text_gen_tool],
)
assert text_gen_agent.status == AssetStatus.DRAFT

# Create team agent with both agents (in DRAFT state)
team_agent = TeamAgentFactory.create(
name="Multi-Function Team",
description="Team that can translate and generate text",
agents=[translation_agent, text_gen_agent],
llm_id="6646261c6eb563165658bbb1",
)
assert team_agent.status == AssetStatus.DRAFT
for agent in team_agent.agents:
assert agent.status == AssetStatus.DRAFT

# Deploy team agent - this should trigger deployment of all nested components
team_agent.deploy()

# Verify team agent is deployed
team_agent = TeamAgentFactory.get(team_agent.id)
assert team_agent.status == AssetStatus.ONBOARDED

# Verify all agents are deployed
for agent in team_agent.agents:
agent_obj = AgentFactory.get(agent.id)
assert agent_obj.status == AssetStatus.ONBOARDED
# Verify all tools are deployed
for tool in agent_obj.tools:
assert tool.status == AssetStatus.ONBOARDED

# Clean up
team_agent.delete()


@pytest.mark.parametrize("TeamAgentFactory", [TeamAgentFactory, v2.TeamAgent])
def test_fail_non_existent_llm(run_input_map, TeamAgentFactory):
from tests.test_deletion_utils import safe_delete_all_agents_and_team_agents
Expand Down Expand Up @@ -511,3 +581,77 @@ def test_team_agent_with_slack_connector():
team_agent.delete()
agent.delete()
connection.delete()


@pytest.mark.parametrize("TeamAgentFactory", [TeamAgentFactory, v2.TeamAgent])
def test_multiple_teams_with_shared_deployed_agent(delete_agents_and_team_agents, TeamAgentFactory):
"""Test that multiple team agents can share the same deployed agent without name conflicts"""
assert delete_agents_and_team_agents

# Create and deploy a shared agent first
translation_tool = AgentFactory.create_model_tool(
function=Function.TRANSLATION,
description="Translation tool from English to Spanish",
supplier="microsoft",
)

shared_agent = AgentFactory.create(
name="Shared Translation Agent",
description="Agent for translation shared between teams",
instructions="Translate text from English to Spanish",
llm_id="6646261c6eb563165658bbb1",
tools=[translation_tool],
)

# Deploy the shared agent first
shared_agent.deploy()
shared_agent = AgentFactory.get(shared_agent.id)
assert shared_agent.status == AssetStatus.ONBOARDED

# Create first team agent with the shared agent
team_agent_1 = TeamAgentFactory.create(
name="Team Agent 1",
description="First team using shared agent",
agents=[shared_agent],
llm_id="6646261c6eb563165658bbb1",
)
assert team_agent_1.status == AssetStatus.DRAFT

# Deploy first team agent - should succeed without trying to redeploy the shared agent
team_agent_1.deploy()
team_agent_1 = TeamAgentFactory.get(team_agent_1.id)
assert team_agent_1.status == AssetStatus.ONBOARDED

# Create second team agent with the same shared agent
team_agent_2 = TeamAgentFactory.create(
name="Team Agent 2",
description="Second team using shared agent",
agents=[shared_agent],
llm_id="6646261c6eb563165658bbb1",
)
assert team_agent_2.status == AssetStatus.DRAFT

# Deploy second team agent - should succeed without trying to redeploy the shared agent
# This should NOT throw a name_already_exists error
team_agent_2.deploy()
team_agent_2 = TeamAgentFactory.get(team_agent_2.id)
assert team_agent_2.status == AssetStatus.ONBOARDED

# Verify both team agents are deployed and functional
response_1 = team_agent_1.run(data="Hello world")
assert response_1 is not None
assert response_1["completed"] is True
assert response_1["status"].lower() == "success"

response_2 = team_agent_2.run(data="Hello world")
assert response_2 is not None
assert response_2["completed"] is True
assert response_2["status"].lower() == "success"

# Verify the shared agent is still deployed and accessible
shared_agent_refreshed = AgentFactory.get(shared_agent.id)
assert shared_agent_refreshed.status == AssetStatus.ONBOARDED

# Clean up
team_agent_1.delete()
team_agent_2.delete()
72 changes: 50 additions & 22 deletions tests/unit/team_agent/team_agent_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ def test_create_team_agent(mock_model_factory_get):
"function": {"id": "text-generation"},
"supplier": "openai",
"version": {"id": "1.0"},
"status": "onboarded",
"status": "draft",
"pricing": {"currency": "USD", "value": 0.0},
}
mock.get(url, headers=headers, json=model_ref_response)
Expand All @@ -163,7 +163,7 @@ def test_create_team_agent(mock_model_factory_get):
"instructions": "Test Agent Instructions",
"teamId": "123",
"version": "1.0",
"status": "onboarded",
"status": "draft",
"llmId": "6646261c6eb563165658bbb1",
"pricing": {"currency": "USD", "value": 0.0},
"assets": [
Expand Down Expand Up @@ -215,8 +215,6 @@ def test_create_team_agent(mock_model_factory_get):
llm_id="6646261c6eb563165658bbb1",
description="TEST Multi agent",
use_mentalist=True,
# TODO: inspectors=[Inspector(name="Test Inspector", model_id="6646261c6eb563165658bbb1", model_params={"prompt": "Test Prompt"}, policy=InspectorPolicy.ADAPTIVE)],
# TODO: inspector_targets=[InspectorTarget.STEPS, InspectorTarget.OUTPUT],
)
assert team_agent.id is not None
assert team_agent.name == team_ref_response["name"]
Expand All @@ -227,27 +225,24 @@ def test_create_team_agent(mock_model_factory_get):
assert len(team_agent.agents) == 1
assert team_agent.agents[0].id == team_ref_response["agents"][0]["assetId"]

# Mock deployment responses
# Mock agent deployment
url = urljoin(config.BACKEND_URL, f"sdk/agents/{agent.id}")
deployed_agent_response = ref_response.copy()
deployed_agent_response["status"] = "onboarded"
mock.put(url, headers=headers, json=deployed_agent_response)
mock.get(url, headers=headers, json=deployed_agent_response)

# Mock team agent deployment
url = urljoin(config.BACKEND_URL, f"sdk/agent-communities/{team_agent.id}")
team_ref_response = {
"id": "team_agent_123",
"name": "TEST Multi agent(-)",
"status": "onboarded",
"teamId": 645,
"description": "TEST Multi agent",
"llmId": "6646261c6eb563165658bbb1",
"assets": [],
"agents": [{"assetId": "123", "type": "AGENT", "number": 0, "label": "AGENT"}],
"links": [],
"plannerId": "6646261c6eb563165658bbb1",
"inspectorId": "6646261c6eb563165658bbb1",
"supervisorId": "6646261c6eb563165658bbb1",
"createdAt": "2024-10-28T19:30:25.344Z",
"updatedAt": "2024-10-28T19:30:25.344Z",
}
mock.put(url, headers=headers, json=team_ref_response)
deployed_team_response = team_ref_response.copy()
deployed_team_response["status"] = "onboarded"
mock.put(url, headers=headers, json=deployed_team_response)

# Deploy and verify
team_agent.deploy()
assert team_agent.status.value == "onboarded"
assert team_agent.status == AssetStatus.ONBOARDED
assert team_agent.agents[0].status == AssetStatus.ONBOARDED


def test_fail_inspector_without_mentalist():
Expand Down Expand Up @@ -389,6 +384,39 @@ def test_deploy_team_agent():
assert team_agent.status == AssetStatus.ONBOARDED
team_agent.update.assert_called_once()

def test_deploy_team_agent_with_nested_agents():
"""Test that deploying a team agent properly deploys its nested agents."""
# Create mock agents
mock_agent1 = Mock()
mock_agent1.id = "agent-1"
mock_agent1.name = "Test Agent 1"
mock_agent1.status = AssetStatus.DRAFT
mock_agent1.deploy = Mock()

mock_agent2 = Mock()
mock_agent2.id = "agent-2"
mock_agent2.name = "Test Agent 2"
mock_agent2.status = AssetStatus.DRAFT
mock_agent2.deploy = Mock()

# Create the team agent
team_agent = TeamAgent(
id="team-agent-id", name="Test Team Agent", agents=[mock_agent1, mock_agent2], status=AssetStatus.DRAFT
)

# Mock the update method
team_agent.update = Mock()

# Deploy the team agent
team_agent.deploy()

# Verify that each agent's deploy method was called
mock_agent1.deploy.assert_called_once()
mock_agent2.deploy.assert_called_once()

# Verify that status was updated and update was called
assert team_agent.status == AssetStatus.ONBOARDED
team_agent.update.assert_called_once()

def test_team_agent_serialization_completeness():
"""Test that TeamAgent to_dict includes all necessary fields."""
Expand Down