mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-05-01 23:32:39 +00:00
Agent State Step 1
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import logging
|
||||
import json
|
||||
from typing import Tuple, cast
|
||||
from typing import Tuple, cast, List, Optional, Dict, Any
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
@@ -16,10 +16,17 @@ from crewai.utilities.events.reasoning_events import (
|
||||
)
|
||||
|
||||
|
||||
class StructuredPlan(BaseModel):
|
||||
"""Structured representation of a task plan."""
|
||||
steps: List[str] = Field(description="List of steps to complete the task")
|
||||
acceptance_criteria: List[str] = Field(description="Criteria that must be met before task is considered complete")
|
||||
|
||||
|
||||
class ReasoningPlan(BaseModel):
|
||||
"""Model representing a reasoning plan for a task."""
|
||||
plan: str = Field(description="The detailed reasoning plan for the task.")
|
||||
ready: bool = Field(description="Whether the agent is ready to execute the task.")
|
||||
structured_plan: Optional[StructuredPlan] = Field(default=None, description="Structured version of the plan")
|
||||
|
||||
|
||||
class AgentReasoningOutput(BaseModel):
|
||||
@@ -31,6 +38,8 @@ class ReasoningFunction(BaseModel):
|
||||
"""Model for function calling with reasoning."""
|
||||
plan: str = Field(description="The detailed reasoning plan for the task.")
|
||||
ready: bool = Field(description="Whether the agent is ready to execute the task.")
|
||||
steps: Optional[List[str]] = Field(default=None, description="List of steps to complete the task")
|
||||
acceptance_criteria: Optional[List[str]] = Field(default=None, description="Criteria that must be met before task is complete")
|
||||
|
||||
|
||||
class AgentReasoning:
|
||||
@@ -119,25 +128,25 @@ class AgentReasoning:
|
||||
Returns:
|
||||
AgentReasoningOutput: The output of the agent reasoning process.
|
||||
"""
|
||||
plan, ready = self.__create_initial_plan()
|
||||
plan, ready, structured_plan = self.__create_initial_plan()
|
||||
|
||||
plan, ready = self.__refine_plan_if_needed(plan, ready)
|
||||
plan, ready, structured_plan = self.__refine_plan_if_needed(plan, ready, structured_plan)
|
||||
|
||||
reasoning_plan = ReasoningPlan(plan=plan, ready=ready)
|
||||
reasoning_plan = ReasoningPlan(plan=plan, ready=ready, structured_plan=structured_plan)
|
||||
return AgentReasoningOutput(plan=reasoning_plan)
|
||||
|
||||
def __create_initial_plan(self) -> Tuple[str, bool]:
|
||||
def __create_initial_plan(self) -> Tuple[str, bool, Optional[StructuredPlan]]:
|
||||
"""
|
||||
Creates the initial reasoning plan for the task.
|
||||
|
||||
Returns:
|
||||
Tuple[str, bool]: The initial plan and whether the agent is ready to execute the task.
|
||||
Tuple[str, bool, Optional[StructuredPlan]]: The initial plan, whether the agent is ready, and structured plan.
|
||||
"""
|
||||
reasoning_prompt = self.__create_reasoning_prompt()
|
||||
|
||||
if self.llm.supports_function_calling():
|
||||
plan, ready = self.__call_with_function(reasoning_prompt, "initial_plan")
|
||||
return plan, ready
|
||||
plan, ready, structured_plan = self.__call_with_function(reasoning_prompt, "initial_plan")
|
||||
return plan, ready, structured_plan
|
||||
else:
|
||||
system_prompt = self.i18n.retrieve("reasoning", "initial_plan").format(
|
||||
role=self.agent.role,
|
||||
@@ -152,18 +161,21 @@ class AgentReasoning:
|
||||
]
|
||||
)
|
||||
|
||||
return self.__parse_reasoning_response(str(response))
|
||||
plan, ready = self.__parse_reasoning_response(str(response))
|
||||
structured_plan = self.__extract_structured_plan(plan)
|
||||
return plan, ready, structured_plan
|
||||
|
||||
def __refine_plan_if_needed(self, plan: str, ready: bool) -> Tuple[str, bool]:
|
||||
def __refine_plan_if_needed(self, plan: str, ready: bool, structured_plan: Optional[StructuredPlan]) -> Tuple[str, bool, Optional[StructuredPlan]]:
|
||||
"""
|
||||
Refines the reasoning plan if the agent is not ready to execute the task.
|
||||
|
||||
Args:
|
||||
plan: The current reasoning plan.
|
||||
ready: Whether the agent is ready to execute the task.
|
||||
structured_plan: The current structured plan.
|
||||
|
||||
Returns:
|
||||
Tuple[str, bool]: The refined plan and whether the agent is ready to execute the task.
|
||||
Tuple[str, bool, Optional[StructuredPlan]]: The refined plan, ready status, and structured plan.
|
||||
"""
|
||||
attempt = 1
|
||||
max_attempts = self.agent.max_reasoning_attempts
|
||||
@@ -185,7 +197,7 @@ class AgentReasoning:
|
||||
refine_prompt = self.__create_refine_prompt(plan)
|
||||
|
||||
if self.llm.supports_function_calling():
|
||||
plan, ready = self.__call_with_function(refine_prompt, "refine_plan")
|
||||
plan, ready, structured_plan = self.__call_with_function(refine_prompt, "refine_plan")
|
||||
else:
|
||||
system_prompt = self.i18n.retrieve("reasoning", "refine_plan").format(
|
||||
role=self.agent.role,
|
||||
@@ -200,6 +212,7 @@ class AgentReasoning:
|
||||
]
|
||||
)
|
||||
plan, ready = self.__parse_reasoning_response(str(response))
|
||||
structured_plan = self.__extract_structured_plan(plan)
|
||||
|
||||
attempt += 1
|
||||
|
||||
@@ -209,9 +222,9 @@ class AgentReasoning:
|
||||
)
|
||||
break
|
||||
|
||||
return plan, ready
|
||||
return plan, ready, structured_plan
|
||||
|
||||
def __call_with_function(self, prompt: str, prompt_type: str) -> Tuple[str, bool]:
|
||||
def __call_with_function(self, prompt: str, prompt_type: str) -> Tuple[str, bool, Optional[StructuredPlan]]:
|
||||
"""
|
||||
Calls the LLM with function calling to get a reasoning plan.
|
||||
|
||||
@@ -220,7 +233,7 @@ class AgentReasoning:
|
||||
prompt_type: The type of prompt (initial_plan or refine_plan).
|
||||
|
||||
Returns:
|
||||
Tuple[str, bool]: A tuple containing the plan and whether the agent is ready.
|
||||
Tuple[str, bool, Optional[StructuredPlan]]: A tuple containing the plan, ready status, and structured plan.
|
||||
"""
|
||||
self.logger.debug(f"Using function calling for {prompt_type} reasoning")
|
||||
|
||||
@@ -239,6 +252,16 @@ class AgentReasoning:
|
||||
"ready": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the agent is ready to execute the task."
|
||||
},
|
||||
"steps": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "List of steps to complete the task"
|
||||
},
|
||||
"acceptance_criteria": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Criteria that must be met before task is considered complete"
|
||||
}
|
||||
},
|
||||
"required": ["plan", "ready"]
|
||||
@@ -254,9 +277,14 @@ class AgentReasoning:
|
||||
)
|
||||
|
||||
# Prepare a simple callable that just returns the tool arguments as JSON
|
||||
def _create_reasoning_plan(plan: str, ready: bool): # noqa: N802
|
||||
def _create_reasoning_plan(plan: str, ready: bool, steps: Optional[List[str]] = None, acceptance_criteria: Optional[List[str]] = None): # noqa: N802
|
||||
"""Return the reasoning plan result in JSON string form."""
|
||||
return json.dumps({"plan": plan, "ready": ready})
|
||||
return json.dumps({
|
||||
"plan": plan,
|
||||
"ready": ready,
|
||||
"steps": steps,
|
||||
"acceptance_criteria": acceptance_criteria
|
||||
})
|
||||
|
||||
response = self.llm.call(
|
||||
[
|
||||
@@ -272,12 +300,19 @@ class AgentReasoning:
|
||||
try:
|
||||
result = json.loads(response)
|
||||
if "plan" in result and "ready" in result:
|
||||
return result["plan"], result["ready"]
|
||||
structured_plan = None
|
||||
if result.get("steps") or result.get("acceptance_criteria"):
|
||||
structured_plan = StructuredPlan(
|
||||
steps=result.get("steps", []),
|
||||
acceptance_criteria=result.get("acceptance_criteria", [])
|
||||
)
|
||||
return result["plan"], result["ready"], structured_plan
|
||||
except (json.JSONDecodeError, KeyError):
|
||||
pass
|
||||
|
||||
response_str = str(response)
|
||||
return response_str, "READY: I am ready to execute the task." in response_str
|
||||
structured_plan = self.__extract_structured_plan(response_str)
|
||||
return response_str, "READY: I am ready to execute the task." in response_str, structured_plan
|
||||
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Error during function calling: {str(e)}. Falling back to text parsing.")
|
||||
@@ -297,10 +332,11 @@ class AgentReasoning:
|
||||
)
|
||||
|
||||
fallback_str = str(fallback_response)
|
||||
return fallback_str, "READY: I am ready to execute the task." in fallback_str
|
||||
structured_plan = self.__extract_structured_plan(fallback_str)
|
||||
return fallback_str, "READY: I am ready to execute the task." in fallback_str, structured_plan
|
||||
except Exception as inner_e:
|
||||
self.logger.error(f"Error during fallback text parsing: {str(inner_e)}")
|
||||
return "Failed to generate a plan due to an error.", True # Default to ready to avoid getting stuck
|
||||
return "Failed to generate a plan due to an error.", True, None # Default to ready to avoid getting stuck
|
||||
|
||||
def __get_agent_backstory(self) -> str:
|
||||
"""
|
||||
@@ -496,7 +532,7 @@ class AgentReasoning:
|
||||
)
|
||||
|
||||
if self.llm.supports_function_calling():
|
||||
plan, ready = self.__call_with_function(mid_execution_prompt, "mid_execution_plan")
|
||||
plan, ready, structured_plan = self.__call_with_function(mid_execution_prompt, "mid_execution_plan")
|
||||
else:
|
||||
# Use the same prompt for system context
|
||||
system_prompt = self.i18n.retrieve("reasoning", "mid_execution_plan").format(
|
||||
@@ -513,8 +549,9 @@ class AgentReasoning:
|
||||
)
|
||||
|
||||
plan, ready = self.__parse_reasoning_response(str(response))
|
||||
structured_plan = self.__extract_structured_plan(plan)
|
||||
|
||||
reasoning_plan = ReasoningPlan(plan=plan, ready=ready)
|
||||
reasoning_plan = ReasoningPlan(plan=plan, ready=ready, structured_plan=structured_plan)
|
||||
return AgentReasoningOutput(plan=reasoning_plan)
|
||||
|
||||
def __create_mid_execution_prompt(
|
||||
@@ -560,7 +597,8 @@ class AgentReasoning:
|
||||
self,
|
||||
current_steps: int,
|
||||
tools_used: list,
|
||||
current_progress: str
|
||||
current_progress: str,
|
||||
tool_usage_stats: Optional[Dict[str, Any]] = None
|
||||
) -> bool:
|
||||
"""
|
||||
Use LLM function calling to determine if adaptive reasoning should be triggered.
|
||||
@@ -569,13 +607,14 @@ class AgentReasoning:
|
||||
current_steps: Number of steps executed so far
|
||||
tools_used: List of tools that have been used
|
||||
current_progress: Summary of progress made so far
|
||||
tool_usage_stats: Optional statistics about tool usage patterns
|
||||
|
||||
Returns:
|
||||
bool: True if reasoning should be triggered, False otherwise.
|
||||
"""
|
||||
try:
|
||||
decision_prompt = self.__create_adaptive_reasoning_decision_prompt(
|
||||
current_steps, tools_used, current_progress
|
||||
current_steps, tools_used, current_progress, tool_usage_stats
|
||||
)
|
||||
|
||||
if self.llm.supports_function_calling():
|
||||
@@ -618,6 +657,11 @@ class AgentReasoning:
|
||||
"reasoning": {
|
||||
"type": "string",
|
||||
"description": "Brief explanation of why reasoning is or isn't needed."
|
||||
},
|
||||
"detected_issues": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "List of specific issues detected (e.g., 'repeated tool failures', 'no progress', 'inefficient approach')"
|
||||
}
|
||||
},
|
||||
"required": ["should_reason", "reasoning"]
|
||||
@@ -625,9 +669,18 @@ class AgentReasoning:
|
||||
}
|
||||
}
|
||||
|
||||
def _decide_reasoning_need(should_reason: bool, reasoning: str):
|
||||
def _decide_reasoning_need(should_reason: bool, reasoning: str, detected_issues: Optional[List[str]] = None):
|
||||
"""Return the reasoning decision result in JSON string form."""
|
||||
return json.dumps({"should_reason": should_reason, "reasoning": reasoning})
|
||||
result = {
|
||||
"should_reason": should_reason,
|
||||
"reasoning": reasoning
|
||||
}
|
||||
if detected_issues:
|
||||
result["detected_issues"] = detected_issues
|
||||
# Append detected issues to reasoning explanation
|
||||
issues_str = ", ".join(detected_issues)
|
||||
result["reasoning"] = f"{reasoning} Detected issues: {issues_str}"
|
||||
return json.dumps(result)
|
||||
|
||||
system_prompt = self.i18n.retrieve("reasoning", "adaptive_reasoning_decision").format(
|
||||
role=self.agent.role,
|
||||
@@ -646,7 +699,11 @@ class AgentReasoning:
|
||||
|
||||
try:
|
||||
result = json.loads(response)
|
||||
return result.get("should_reason", False), result.get("reasoning", "No explanation provided")
|
||||
reasoning_text = result.get("reasoning", "No explanation provided")
|
||||
if result.get("detected_issues"):
|
||||
# Include detected issues in the reasoning text for logging
|
||||
self.logger.debug(f"Adaptive reasoning detected issues: {result['detected_issues']}")
|
||||
return result.get("should_reason", False), reasoning_text
|
||||
except (json.JSONDecodeError, KeyError):
|
||||
return False, "No explanation provided"
|
||||
|
||||
@@ -669,11 +726,27 @@ class AgentReasoning:
|
||||
self,
|
||||
current_steps: int,
|
||||
tools_used: list,
|
||||
current_progress: str
|
||||
current_progress: str,
|
||||
tool_usage_stats: Optional[Dict[str, Any]] = None
|
||||
) -> str:
|
||||
"""Create prompt for adaptive reasoning decision."""
|
||||
tools_used_str = ", ".join(tools_used) if tools_used else "No tools used yet"
|
||||
|
||||
# Add tool usage statistics to the prompt
|
||||
tool_stats_str = ""
|
||||
if tool_usage_stats:
|
||||
tool_stats_str = f"\n\nTOOL USAGE STATISTICS:\n"
|
||||
tool_stats_str += f"- Total tool invocations: {tool_usage_stats.get('total_tool_uses', 0)}\n"
|
||||
tool_stats_str += f"- Unique tools used: {tool_usage_stats.get('unique_tools', 0)}\n"
|
||||
|
||||
if tool_usage_stats.get('tools_by_frequency'):
|
||||
tool_stats_str += "- Tool frequency:\n"
|
||||
for tool, count in tool_usage_stats['tools_by_frequency'].items():
|
||||
tool_stats_str += f" • {tool}: {count} times\n"
|
||||
|
||||
if tool_usage_stats.get('recent_patterns'):
|
||||
tool_stats_str += f"- Recent patterns: {tool_usage_stats['recent_patterns']}\n"
|
||||
|
||||
# Use the prompt from i18n and format it with the current context
|
||||
base_prompt = self.i18n.retrieve("reasoning", "adaptive_reasoning_decision").format(
|
||||
role=self.agent.role,
|
||||
@@ -686,9 +759,72 @@ class AgentReasoning:
|
||||
expected_output=self.task.expected_output,
|
||||
current_steps=current_steps,
|
||||
tools_used=tools_used_str,
|
||||
current_progress=current_progress
|
||||
current_progress=current_progress + tool_stats_str
|
||||
)
|
||||
|
||||
prompt = base_prompt + context_prompt
|
||||
|
||||
return prompt
|
||||
|
||||
def __extract_structured_plan(self, plan: str) -> Optional[StructuredPlan]:
|
||||
"""
|
||||
Extracts a structured plan from the given plan text.
|
||||
|
||||
Args:
|
||||
plan: The plan text.
|
||||
|
||||
Returns:
|
||||
Optional[StructuredPlan]: The extracted structured plan or None if no plan was found.
|
||||
"""
|
||||
if not plan:
|
||||
return None
|
||||
|
||||
import re
|
||||
|
||||
steps = []
|
||||
acceptance_criteria = []
|
||||
|
||||
# Look for numbered steps (1., 2., etc.)
|
||||
step_pattern = r'^\s*(?:\d+\.|\-|\*)\s*(.+)$'
|
||||
|
||||
# Look for acceptance criteria section
|
||||
in_acceptance_section = False
|
||||
|
||||
lines = plan.split('\n')
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
|
||||
# Check if we're entering acceptance criteria section
|
||||
if any(marker in line.lower() for marker in ['acceptance criteria', 'success criteria', 'completion criteria']):
|
||||
in_acceptance_section = True
|
||||
continue
|
||||
|
||||
# Skip empty lines
|
||||
if not line:
|
||||
continue
|
||||
|
||||
# Extract steps or criteria
|
||||
match = re.match(step_pattern, line, re.MULTILINE)
|
||||
if match:
|
||||
content = match.group(1).strip()
|
||||
if in_acceptance_section:
|
||||
acceptance_criteria.append(content)
|
||||
else:
|
||||
steps.append(content)
|
||||
elif line and not line.endswith(':'): # Non-empty line that's not a header
|
||||
if in_acceptance_section:
|
||||
acceptance_criteria.append(line)
|
||||
else:
|
||||
# Check if it looks like a step (starts with action verb)
|
||||
action_verbs = ['create', 'implement', 'design', 'build', 'test', 'verify', 'check', 'ensure', 'analyze', 'review']
|
||||
if any(line.lower().startswith(verb) for verb in action_verbs):
|
||||
steps.append(line)
|
||||
|
||||
# If we found steps or criteria, return structured plan
|
||||
if steps or acceptance_criteria:
|
||||
return StructuredPlan(
|
||||
steps=steps,
|
||||
acceptance_criteria=acceptance_criteria
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user