mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-07-08 16:35:08 +00:00
Compare commits
3 Commits
joaomdmour
...
rn/cor-373
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b30f39eaa | ||
|
|
6893b3c3d2 | ||
|
|
a77b9f85a2 |
@@ -168,6 +168,10 @@ def _serialize_executor_class(value: Any) -> str:
|
||||
return value.__name__ if isinstance(value, type) else str(value)
|
||||
|
||||
|
||||
# Required fields seeded with placeholders when an agent defers to a repository.
|
||||
_REPOSITORY_IDENTITY_FIELDS = ("role", "goal", "backstory")
|
||||
|
||||
|
||||
class Agent(BaseAgent):
|
||||
"""Represents an agent in a system.
|
||||
|
||||
@@ -200,6 +204,7 @@ class Agent(BaseAgent):
|
||||
_times_executed: int = PrivateAttr(default=0)
|
||||
_mcp_resolver: MCPToolResolver | None = PrivateAttr(default=None)
|
||||
_last_messages: list[LLMMessage] = PrivateAttr(default_factory=list)
|
||||
_from_repository_resolved: bool = PrivateAttr(default=False)
|
||||
max_execution_time: int | None = Field(
|
||||
default=None,
|
||||
description="Maximum execution time for an agent to execute a task",
|
||||
@@ -346,11 +351,46 @@ class Agent(BaseAgent):
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def validate_from_repository(cls, v: Any) -> dict[str, Any] | None | Any:
|
||||
"""Merge repository agent config with provided values before validation."""
|
||||
if v is not None and (from_repository := v.get("from_repository")):
|
||||
return load_agent_from_repository(from_repository) | v
|
||||
"""Defer repository resolution to first execution.
|
||||
|
||||
Fetching needs authenticated network access; doing it at construction
|
||||
breaks ``from_repository`` agents in deployments, where a crew loads
|
||||
before auth is wired. Seed required fields so the model validates now;
|
||||
the definition is fetched on first run (see ``_resolve_from_repository``).
|
||||
"""
|
||||
if isinstance(v, dict) and v.get("from_repository"):
|
||||
for field in _REPOSITORY_IDENTITY_FIELDS:
|
||||
v.setdefault(field, "")
|
||||
return v
|
||||
|
||||
def _resolve_from_repository(self) -> None:
|
||||
"""Fetch and apply the repository definition once, on first execution.
|
||||
|
||||
Values set explicitly at construction win; the repository fills the rest.
|
||||
"""
|
||||
if not self.from_repository or self._from_repository_resolved:
|
||||
return
|
||||
|
||||
explicit = set(self.model_fields_set)
|
||||
for field in _REPOSITORY_IDENTITY_FIELDS:
|
||||
if not getattr(self, field): # placeholder, not a user-supplied value
|
||||
explicit.discard(field)
|
||||
|
||||
fields = type(self).model_fields
|
||||
for key, value in load_agent_from_repository(self.from_repository).items():
|
||||
if key not in explicit and key in fields:
|
||||
# Validate as construction would (e.g. an llm string -> LLM),
|
||||
# without re-running the model's after-validators.
|
||||
self.__pydantic_validator__.validate_assignment(self, key, value)
|
||||
|
||||
self._from_repository_resolved = True
|
||||
|
||||
async def _aresolve_from_repository(self) -> None:
|
||||
"""Async variant: run the blocking repository fetch off the event loop."""
|
||||
if not self.from_repository or self._from_repository_resolved:
|
||||
return
|
||||
await asyncio.to_thread(self._resolve_from_repository)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def post_init_setup(self) -> Self:
|
||||
"""Initialize LLM, executor, code tools, and skills after model creation."""
|
||||
@@ -804,6 +844,7 @@ class Agent(BaseAgent):
|
||||
ValueError: If the max execution time is not a positive integer.
|
||||
RuntimeError: If the agent execution fails for other reasons.
|
||||
"""
|
||||
self._resolve_from_repository()
|
||||
task_prompt = self._prepare_task_execution(task, context)
|
||||
|
||||
knowledge_config = get_knowledge_config(self)
|
||||
@@ -940,6 +981,7 @@ class Agent(BaseAgent):
|
||||
ValueError: If the max execution time is not a positive integer.
|
||||
RuntimeError: If the agent execution fails for other reasons.
|
||||
"""
|
||||
await self._aresolve_from_repository()
|
||||
task_prompt = self._prepare_task_execution(task, context)
|
||||
|
||||
knowledge_config = get_knowledge_config(self)
|
||||
@@ -1611,6 +1653,7 @@ class Agent(BaseAgent):
|
||||
if is_inside_event_loop():
|
||||
return self.kickoff_async(messages, response_format, input_files)
|
||||
|
||||
self._resolve_from_repository()
|
||||
executor, inputs, agent_info, parsed_tools = self._prepare_kickoff(
|
||||
messages, response_format, input_files
|
||||
)
|
||||
@@ -1926,6 +1969,7 @@ class Agent(BaseAgent):
|
||||
input_files=input_files,
|
||||
)
|
||||
|
||||
await self._aresolve_from_repository()
|
||||
executor, inputs, agent_info, parsed_tools = self._prepare_kickoff(
|
||||
messages, response_format, input_files
|
||||
)
|
||||
|
||||
@@ -2207,6 +2207,9 @@ def test_agent_from_repository(mock_get_agent, mock_get_auth_token):
|
||||
mock_get_agent.return_value = mock_get_response
|
||||
|
||||
agent = Agent(from_repository="test_agent")
|
||||
assert mock_get_agent.called is False # deferred until execution
|
||||
|
||||
agent._resolve_from_repository()
|
||||
|
||||
assert agent.role == "test role"
|
||||
assert agent.goal == "test goal"
|
||||
@@ -2219,6 +2222,53 @@ def test_agent_from_repository(mock_get_agent, mock_get_auth_token):
|
||||
assert agent.tools[1].file_path == "test.txt"
|
||||
|
||||
|
||||
@patch("crewai.plus_api.PlusAPI.get_agent")
|
||||
def test_agent_from_repository_is_deferred_until_execution(
|
||||
mock_get_agent, mock_get_auth_token
|
||||
):
|
||||
mock_get_response = MagicMock()
|
||||
mock_get_response.status_code = 200
|
||||
mock_get_response.json.return_value = {
|
||||
"role": "test role",
|
||||
"goal": "test goal",
|
||||
"backstory": "test backstory",
|
||||
}
|
||||
mock_get_agent.return_value = mock_get_response
|
||||
|
||||
# Construction must not touch the network; resolution fetches once.
|
||||
agent = Agent(from_repository="test_agent")
|
||||
assert mock_get_agent.called is False
|
||||
assert agent.role == ""
|
||||
|
||||
agent._resolve_from_repository()
|
||||
agent._resolve_from_repository()
|
||||
assert agent.role == "test role"
|
||||
assert mock_get_agent.call_count == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("crewai.plus_api.PlusAPI.get_agent")
|
||||
async def test_agent_from_repository_resolves_inside_event_loop(
|
||||
mock_get_agent, mock_get_auth_token
|
||||
):
|
||||
mock_get_response = MagicMock()
|
||||
mock_get_response.status_code = 200
|
||||
mock_get_response.json.return_value = {
|
||||
"role": "test role",
|
||||
"goal": "test goal",
|
||||
"backstory": "test backstory",
|
||||
}
|
||||
mock_get_agent.return_value = mock_get_response
|
||||
|
||||
# The blocking fetch must run off the loop (load_agent_from_repository uses
|
||||
# asyncio.run), so async execution does not raise.
|
||||
agent = Agent(from_repository="test_agent")
|
||||
await agent._aresolve_from_repository()
|
||||
|
||||
assert agent.role == "test role"
|
||||
assert mock_get_agent.call_count == 1
|
||||
|
||||
|
||||
@patch("crewai.plus_api.PlusAPI.get_agent")
|
||||
def test_agent_from_repository_override_attributes(mock_get_agent, mock_get_auth_token):
|
||||
from crewai_tools import SerperDevTool
|
||||
@@ -2235,6 +2285,7 @@ def test_agent_from_repository_override_attributes(mock_get_agent, mock_get_auth
|
||||
}
|
||||
mock_get_agent.return_value = mock_get_response
|
||||
agent = Agent(from_repository="test_agent", role="Custom Role")
|
||||
agent._resolve_from_repository()
|
||||
|
||||
assert agent.role == "Custom Role"
|
||||
assert agent.goal == "test goal"
|
||||
@@ -2259,11 +2310,12 @@ def test_agent_from_repository_with_invalid_tools(mock_get_agent, mock_get_auth_
|
||||
],
|
||||
}
|
||||
mock_get_agent.return_value = mock_get_response
|
||||
agent = Agent(from_repository="test_agent")
|
||||
with pytest.raises(
|
||||
AgentRepositoryError,
|
||||
match="Tool DoesNotExist could not be loaded: module 'crewai_tools' has no attribute 'DoesNotExist'",
|
||||
):
|
||||
Agent(from_repository="test_agent")
|
||||
agent._resolve_from_repository()
|
||||
|
||||
|
||||
@patch("crewai.plus_api.PlusAPI.get_agent")
|
||||
@@ -2272,11 +2324,12 @@ def test_agent_from_repository_internal_error(mock_get_agent, mock_get_auth_toke
|
||||
mock_get_response.status_code = 500
|
||||
mock_get_response.text = "Internal server error"
|
||||
mock_get_agent.return_value = mock_get_response
|
||||
agent = Agent(from_repository="test_agent")
|
||||
with pytest.raises(
|
||||
AgentRepositoryError,
|
||||
match="Agent test_agent could not be loaded: Internal server error",
|
||||
):
|
||||
Agent(from_repository="test_agent")
|
||||
agent._resolve_from_repository()
|
||||
|
||||
|
||||
@patch("crewai.plus_api.PlusAPI.get_agent")
|
||||
@@ -2285,11 +2338,12 @@ def test_agent_from_repository_agent_not_found(mock_get_agent, mock_get_auth_tok
|
||||
mock_get_response.status_code = 404
|
||||
mock_get_response.text = "Agent not found"
|
||||
mock_get_agent.return_value = mock_get_response
|
||||
agent = Agent(from_repository="test_agent")
|
||||
with pytest.raises(
|
||||
AgentRepositoryError,
|
||||
match="Agent test_agent does not exist, make sure the name is correct or the agent is available on your organization",
|
||||
):
|
||||
Agent(from_repository="test_agent")
|
||||
agent._resolve_from_repository()
|
||||
|
||||
|
||||
@patch("crewai.plus_api.PlusAPI.get_agent")
|
||||
@@ -2314,6 +2368,7 @@ def test_agent_from_repository_displays_org_info(
|
||||
mock_get_agent.return_value = mock_get_response
|
||||
|
||||
agent = Agent(from_repository="test_agent")
|
||||
agent._resolve_from_repository()
|
||||
|
||||
mock_console.print.assert_any_call(
|
||||
"Fetching agent from organization: Test Organization (test-org-uuid)",
|
||||
@@ -2341,11 +2396,12 @@ def test_agent_from_repository_without_org_set(
|
||||
mock_get_response.text = "Unauthorized access"
|
||||
mock_get_agent.return_value = mock_get_response
|
||||
|
||||
agent = Agent(from_repository="test_agent")
|
||||
with pytest.raises(
|
||||
AgentRepositoryError,
|
||||
match="Agent test_agent could not be loaded: Unauthorized access",
|
||||
):
|
||||
Agent(from_repository="test_agent")
|
||||
agent._resolve_from_repository()
|
||||
|
||||
mock_console.print.assert_any_call(
|
||||
"No organization currently set. We recommend setting one before using: `crewai org switch <org_id>` command.",
|
||||
|
||||
Reference in New Issue
Block a user