mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-07-03 06:08:15 +00:00
Allow repository-backed flow agents
Flow declarations can now reference full repository-backed Agent config
without duplicating it inline. The repository response is merged the same
way as `Agent(from_repository=...)`, so values like `role`, `goal`,
`backstory`, `tools`, `planning_config`, deprecated `reasoning`, `memory`,
and other Agent fields can come from the repository. Local YAML fields
still override repository values.
Standalone agent action:
```yaml
do:
call: agent
with:
from_repository: support_specialist
input: "${state.question}"
```
Inline crew agent:
```yaml
do:
call: crew
with:
name: inline_research
agents:
researcher:
from_repository: researcher
tasks:
- description: Research {topic}
expected_output: Findings about {topic}
agent: researcher
```
This commit is contained in:
@@ -258,9 +258,10 @@ Fields:
|
||||
#### Crew Agent Definition (`methods.<name>.do[call=crew].with.agents.<name>`)
|
||||
|
||||
Fields:
|
||||
- `role` (required): string. Crew agent role. Crew inputs are interpolated with `{name}` placeholders such as `{topic}`; this is not CEL. Example: `Research analyst`
|
||||
- `goal` (required): string. Crew agent goal. Crew inputs are interpolated with `{name}` placeholders such as `{topic}`; this is not CEL. Example: `Research {topic}`
|
||||
- `backstory` (required): string. Crew agent backstory. Crew inputs are interpolated with `{name}` placeholders such as `{topic}`; this is not CEL. Example: `Expert at concise technical research.`
|
||||
- `role` (optional): string | null; default `null`. Crew agent role. Crew inputs are interpolated with `{name}` placeholders such as `{topic}`; this is not CEL. Example: `Research analyst`
|
||||
- `goal` (optional): string | null; default `null`. Crew agent goal. Crew inputs are interpolated with `{name}` placeholders such as `{topic}`; this is not CEL. Example: `Research {topic}`
|
||||
- `backstory` (optional): string | null; default `null`. Crew agent backstory. Crew inputs are interpolated with `{name}` placeholders such as `{topic}`; this is not CEL. Example: `Expert at concise technical research.`
|
||||
- `from_repository` (optional): string | null; default `null`. Agent repository name to load. Repository values supply missing agent configuration; explicitly provided local fields override the repository values. Example: `researcher`
|
||||
- `settings` (optional): map of string to any. Additional agent settings passed to the loader. Example: `{"llm": "openai/gpt-4o-mini"}`
|
||||
- `llm` (optional): string or inline LLM config; default `null`. Language model that runs this crew agent. Use an object when setting LLM options such as `max_tokens`. Example: `{"max_tokens": 4096, "model": "openai/gpt-4o-mini"}`
|
||||
- `planning_config` (optional): object | null; default `null`. Agent planning configuration. Set `max_attempts` to limit planning refinement attempts before task execution. Example: `{"max_attempts": 3}`
|
||||
@@ -292,15 +293,16 @@ Shape:
|
||||
- `call: agent`
|
||||
|
||||
Fields:
|
||||
- `call` (required): must be `agent`. Action discriminator. Use agent to run an individual inline Agent definition outside of a crew. Example: `agent`
|
||||
- `call` (required): must be `agent`. Action discriminator. Use agent to run an individual inline or repository-backed Agent definition outside of a crew. Example: `agent`
|
||||
- `with` (required): any. Individual Agent definition to load and execute outside of a crew for this action. Put the agent input in `with.input`; agent actions do not support action-level `inputs`. Example: `{"backstory": "Precise and concise.", "goal": "Answer user questions", "input": "${state.question}", "role": "Analyst", "settings": {"llm": "openai/gpt-4o-mini"}}`
|
||||
|
||||
#### Agent Definition (`methods.<name>.do[call=agent].with`)
|
||||
|
||||
Fields:
|
||||
- `role` (required): string. Individual agent role used by a Flow agent action outside of a crew. Example: `Support specialist`
|
||||
- `goal` (required): string. Individual agent goal for the Flow agent action outside of a crew. Example: `Draft a concise customer reply`
|
||||
- `backstory` (required): string. Individual agent backstory used to shape behavior outside of a crew. Example: `Expert at resolving SaaS support questions.`
|
||||
- `role` (optional): string | null; default `null`. Individual agent role used by a Flow agent action outside of a crew. Example: `Support specialist`
|
||||
- `goal` (optional): string | null; default `null`. Individual agent goal for the Flow agent action outside of a crew. Example: `Draft a concise customer reply`
|
||||
- `backstory` (optional): string | null; default `null`. Individual agent backstory used to shape behavior outside of a crew. Example: `Expert at resolving SaaS support questions.`
|
||||
- `from_repository` (optional): string | null; default `null`. Agent repository name to load. Repository values supply missing agent configuration; explicitly provided local fields override the repository values. Example: `support_specialist`
|
||||
- `settings` (optional): map of string to any. Additional agent settings passed to the loader. Example: `{"llm": "openai/gpt-4o-mini"}`
|
||||
- `llm` (optional): string or inline LLM config; default `null`. Language model that runs this agent. Use an object when setting LLM options such as `max_tokens`. Example: `{"max_tokens": 4096, "model": "openai/gpt-4o-mini"}`
|
||||
- `planning_config` (optional): object | null; default `null`. Agent planning configuration. Set `max_attempts` to limit planning refinement attempts before task execution. Example: `{"max_attempts": 3}`
|
||||
|
||||
@@ -472,8 +472,8 @@ class FlowAgentActionDefinition(BaseModel):
|
||||
|
||||
call: Literal["agent"] = Field(
|
||||
description=(
|
||||
"Action discriminator. Use agent to run an individual inline Agent "
|
||||
"definition outside of a crew."
|
||||
"Action discriminator. Use agent to run an individual inline or "
|
||||
"repository-backed Agent definition outside of a crew."
|
||||
),
|
||||
examples=["agent"],
|
||||
)
|
||||
@@ -481,7 +481,8 @@ class FlowAgentActionDefinition(BaseModel):
|
||||
alias="with",
|
||||
description=(
|
||||
"Individual Agent definition to load and execute outside of a crew "
|
||||
"for this action."
|
||||
"for this action. Set from_repository to load agent configuration "
|
||||
"from the agent repository."
|
||||
),
|
||||
examples=[
|
||||
{
|
||||
|
||||
@@ -62,25 +62,28 @@ class LLMDefinition(BaseModel):
|
||||
|
||||
|
||||
class CrewAgentDefinition(BaseModel):
|
||||
"""Inline agent definition used by a crew definition."""
|
||||
"""Agent definition used by a crew definition."""
|
||||
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
role: str = Field(
|
||||
role: str | None = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"Crew agent role. Crew inputs are interpolated with `{name}` "
|
||||
"placeholders such as `{topic}`; this is not CEL."
|
||||
),
|
||||
examples=["Research analyst"],
|
||||
)
|
||||
goal: str = Field(
|
||||
goal: str | None = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"Crew agent goal. Crew inputs are interpolated with `{name}` "
|
||||
"placeholders such as `{topic}`; this is not CEL."
|
||||
),
|
||||
examples=["Research {topic}"],
|
||||
)
|
||||
backstory: str = Field(
|
||||
backstory: str | None = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"Crew agent backstory. Crew inputs are interpolated with `{name}` "
|
||||
"placeholders such as `{topic}`; this is not CEL."
|
||||
@@ -92,6 +95,15 @@ class CrewAgentDefinition(BaseModel):
|
||||
description="Optional built-in type or Python reference used to load the agent.",
|
||||
examples=["agent", {"python": "my_project.agents.ResearchAgent"}],
|
||||
)
|
||||
from_repository: str | None = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"Agent repository name to load. Repository values supply missing "
|
||||
"agent configuration; explicitly provided local fields override the "
|
||||
"repository values."
|
||||
),
|
||||
examples=["researcher"],
|
||||
)
|
||||
settings: dict[str, Any] = Field(
|
||||
default_factory=dict,
|
||||
description="Additional agent settings passed to the loader.",
|
||||
@@ -179,19 +191,40 @@ class CrewAgentDefinition(BaseModel):
|
||||
raise ValueError("agent.settings must be a mapping")
|
||||
return value or {}
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _validate_agent_source(self) -> CrewAgentDefinition:
|
||||
if self.from_repository:
|
||||
return self
|
||||
|
||||
missing = [
|
||||
field
|
||||
for field in ("role", "goal", "backstory")
|
||||
if getattr(self, field) is None
|
||||
]
|
||||
if missing:
|
||||
missing_fields = ", ".join(f"'{field}'" for field in missing)
|
||||
raise ValueError(
|
||||
f"agent definition requires {missing_fields} unless "
|
||||
"from_repository is set"
|
||||
)
|
||||
return self
|
||||
|
||||
|
||||
class AgentDefinition(CrewAgentDefinition):
|
||||
"""Inline individual agent definition used outside of a crew."""
|
||||
"""Individual agent definition used outside of a crew."""
|
||||
|
||||
role: str = Field(
|
||||
role: str | None = Field(
|
||||
default=None,
|
||||
description="Individual agent role used by a Flow agent action outside of a crew.",
|
||||
examples=["Support specialist"],
|
||||
)
|
||||
goal: str = Field(
|
||||
goal: str | None = Field(
|
||||
default=None,
|
||||
description="Individual agent goal for the Flow agent action outside of a crew.",
|
||||
examples=["Draft a concise customer reply"],
|
||||
)
|
||||
backstory: str = Field(
|
||||
backstory: str | None = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"Individual agent backstory used to shape behavior outside of a crew."
|
||||
),
|
||||
@@ -202,6 +235,15 @@ class AgentDefinition(CrewAgentDefinition):
|
||||
description="Optional built-in type or Python reference used to load the agent.",
|
||||
examples=["agent", {"python": "my_project.agents.SupportAgent"}],
|
||||
)
|
||||
from_repository: str | None = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"Agent repository name to load. Repository values supply missing "
|
||||
"agent configuration; explicitly provided local fields override the "
|
||||
"repository values."
|
||||
),
|
||||
examples=["support_specialist"],
|
||||
)
|
||||
settings: dict[str, Any] = Field(
|
||||
default_factory=dict,
|
||||
description="Additional agent settings passed to the loader.",
|
||||
|
||||
@@ -978,9 +978,10 @@ def _agent_kwargs_from_definition(
|
||||
extra_allowed,
|
||||
skip_unknown=skip_unknown,
|
||||
)
|
||||
for required in ("role", "goal", "backstory"):
|
||||
if required not in defn:
|
||||
errors.append(f"{path}: missing required field '{required}'")
|
||||
if not defn.get("from_repository"):
|
||||
for required in ("role", "goal", "backstory"):
|
||||
if defn.get(required) is None:
|
||||
errors.append(f"{path}: missing required field '{required}'")
|
||||
|
||||
settings = defn.get("settings", {})
|
||||
if settings is None:
|
||||
|
||||
@@ -355,6 +355,18 @@ class TestLoadAgent:
|
||||
with pytest.raises(Exception):
|
||||
load_agent(agent_file)
|
||||
|
||||
def test_load_agent_rejects_null_required_fields(self, tmp_path: Path):
|
||||
agent_def = {
|
||||
"role": None,
|
||||
"goal": "Find information",
|
||||
"backstory": "Expert researcher.",
|
||||
}
|
||||
agent_file = tmp_path / "agent.json"
|
||||
agent_file.write_text(json.dumps(agent_def))
|
||||
|
||||
with pytest.raises(JSONProjectValidationError, match="missing required field 'role'"):
|
||||
load_agent(agent_file)
|
||||
|
||||
def test_load_agent_file_not_found(self):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
load_agent(Path("/nonexistent/agent.json"))
|
||||
|
||||
@@ -88,7 +88,7 @@ def test_flow_definition_json_schema_carries_reference_descriptions():
|
||||
agent_properties = defs["FlowAgentActionDefinition"]["properties"]
|
||||
assert "Individual Agent definition" in agent_properties["with"]["description"]
|
||||
assert "outside of a crew" in agent_properties["with"]["description"]
|
||||
assert "individual inline Agent" in agent_properties["call"]["description"]
|
||||
assert "repository-backed Agent" in agent_properties["call"]["description"]
|
||||
|
||||
expression_rule = FLOW_TEMPLATE_EXPRESSION_RULES[0]
|
||||
code_properties = defs["FlowCodeActionDefinition"]["properties"]
|
||||
|
||||
@@ -1163,6 +1163,55 @@ methods:
|
||||
}
|
||||
|
||||
|
||||
def test_agent_action_runs_repository_yaml_definition(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
):
|
||||
import crewai.agent.core as agent_core
|
||||
from crewai import Agent
|
||||
|
||||
def fake_load_agent_from_repository(from_repository: str) -> dict[str, Any]:
|
||||
assert from_repository == "support_specialist"
|
||||
return {
|
||||
"role": "Repository specialist",
|
||||
"goal": "Answer support questions",
|
||||
"backstory": "Loaded from the agent repository.",
|
||||
"max_iter": 3,
|
||||
}
|
||||
|
||||
async def fake_kickoff_async(
|
||||
self: Agent, messages: str, **_kwargs: Any
|
||||
) -> dict[str, Any]:
|
||||
return {"agent": self.role, "input": messages, "max_iter": self.max_iter}
|
||||
|
||||
monkeypatch.setattr(
|
||||
agent_core,
|
||||
"load_agent_from_repository",
|
||||
fake_load_agent_from_repository,
|
||||
)
|
||||
monkeypatch.setattr(Agent, "kickoff_async", fake_kickoff_async)
|
||||
|
||||
yaml_str = """
|
||||
schema: crewai.flow/v1
|
||||
name: AgentFlow
|
||||
methods:
|
||||
answer:
|
||||
do:
|
||||
call: agent
|
||||
with:
|
||||
from_repository: support_specialist
|
||||
input: "${state.question}"
|
||||
start: true
|
||||
"""
|
||||
|
||||
flow = Flow.from_declaration(contents=yaml_str)
|
||||
|
||||
assert flow.kickoff(inputs={"question": "What is CrewAI?"}) == {
|
||||
"agent": "Repository specialist",
|
||||
"input": "What is CrewAI?",
|
||||
"max_iter": 3,
|
||||
}
|
||||
|
||||
|
||||
def test_agent_action_renders_text_custom_expression_input(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
):
|
||||
@@ -1281,6 +1330,7 @@ def test_agent_action_json_schema_describes_inline_agent_definitions():
|
||||
"role",
|
||||
"goal",
|
||||
"backstory",
|
||||
"from_repository",
|
||||
"settings",
|
||||
"llm",
|
||||
"input",
|
||||
@@ -1437,6 +1487,73 @@ methods:
|
||||
}
|
||||
|
||||
|
||||
def test_crew_action_runs_repository_agent_yaml_definition(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
):
|
||||
import crewai.agent.core as agent_core
|
||||
from crewai import Crew
|
||||
|
||||
def fake_load_agent_from_repository(from_repository: str) -> dict[str, Any]:
|
||||
assert from_repository == "researcher"
|
||||
return {
|
||||
"role": "Repository researcher",
|
||||
"goal": "Research {topic}",
|
||||
"backstory": "Loaded from the agent repository.",
|
||||
"max_iter": 5,
|
||||
}
|
||||
|
||||
async def fake_kickoff_async(
|
||||
self: Crew, inputs: dict[str, Any] | None = None, **_kwargs: Any
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"crew": self.name,
|
||||
"agents": [
|
||||
{"role": agent.role, "max_iter": agent.max_iter}
|
||||
for agent in self.agents
|
||||
],
|
||||
"tasks": [task.description for task in self.tasks],
|
||||
"inputs": inputs,
|
||||
}
|
||||
|
||||
monkeypatch.setattr(
|
||||
agent_core,
|
||||
"load_agent_from_repository",
|
||||
fake_load_agent_from_repository,
|
||||
)
|
||||
monkeypatch.setattr(Crew, "kickoff_async", fake_kickoff_async)
|
||||
|
||||
yaml_str = """
|
||||
schema: crewai.flow/v1
|
||||
name: CrewFlow
|
||||
methods:
|
||||
research:
|
||||
do:
|
||||
call: crew
|
||||
with:
|
||||
name: inline_research
|
||||
agents:
|
||||
researcher:
|
||||
from_repository: researcher
|
||||
tasks:
|
||||
- name: research_task
|
||||
description: Research {topic}
|
||||
expected_output: Findings about {topic}
|
||||
agent: researcher
|
||||
inputs:
|
||||
topic: "${state.topic}"
|
||||
start: true
|
||||
"""
|
||||
|
||||
flow = Flow.from_declaration(contents=yaml_str)
|
||||
|
||||
assert flow.kickoff(inputs={"topic": "AI"}) == {
|
||||
"crew": "inline_research",
|
||||
"agents": [{"role": "Repository researcher", "max_iter": 5}],
|
||||
"tasks": ["Research {topic}"],
|
||||
"inputs": {"topic": "AI"},
|
||||
}
|
||||
|
||||
|
||||
def test_crew_action_runs_crew_from_declaration(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
):
|
||||
@@ -1709,6 +1826,7 @@ def test_crew_action_json_schema_describes_inline_crew_definitions():
|
||||
"role",
|
||||
"goal",
|
||||
"backstory",
|
||||
"from_repository",
|
||||
"settings",
|
||||
"llm",
|
||||
"tools",
|
||||
|
||||
Reference in New Issue
Block a user