mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-01-10 00:28:31 +00:00
Its working but needs a massive clean up
This commit is contained in:
@@ -79,61 +79,61 @@ async def main():
|
||||
result = await agent.kickoff_async("What is the population of Tokyo in 2023?")
|
||||
print(f"Raw response: {result.raw}")
|
||||
|
||||
# Example 2: Query with structured output
|
||||
print("\n=== Example 2: Structured Output ===")
|
||||
structured_query = """
|
||||
Research the impact of climate change on coral reefs.
|
||||
|
||||
YOU MUST format your response as a valid JSON object with the following structure:
|
||||
{
|
||||
"main_findings": "A summary of the main findings",
|
||||
"key_points": ["Point 1", "Point 2", "Point 3"],
|
||||
"sources": ["Source 1", "Source 2"]
|
||||
}
|
||||
|
||||
Include at least 3 key points and 2 sources. Wrap your JSON in ```json and ``` tags.
|
||||
"""
|
||||
# # Example 2: Query with structured output
|
||||
# print("\n=== Example 2: Structured Output ===")
|
||||
# structured_query = """
|
||||
# Research the impact of climate change on coral reefs.
|
||||
|
||||
result = await agent.kickoff_async(structured_query)
|
||||
# YOU MUST format your response as a valid JSON object with the following structure:
|
||||
# {
|
||||
# "main_findings": "A summary of the main findings",
|
||||
# "key_points": ["Point 1", "Point 2", "Point 3"],
|
||||
# "sources": ["Source 1", "Source 2"]
|
||||
# }
|
||||
|
||||
if result.pydantic:
|
||||
# Cast to the specific type for better IDE support
|
||||
research_result = cast(ResearchResult, result.pydantic)
|
||||
print(f"Main findings: {research_result.main_findings}")
|
||||
print("\nKey points:")
|
||||
for i, point in enumerate(research_result.key_points, 1):
|
||||
print(f"{i}. {point}")
|
||||
print("\nSources:")
|
||||
for i, source in enumerate(research_result.sources, 1):
|
||||
print(f"{i}. {source}")
|
||||
else:
|
||||
print(f"Raw response: {result.raw}")
|
||||
print(
|
||||
"\nNote: Structured output was not generated. The LLM may need more explicit instructions to format the response as JSON."
|
||||
)
|
||||
# Include at least 3 key points and 2 sources. Wrap your JSON in ```json and ``` tags.
|
||||
# """
|
||||
|
||||
# Example 3: Multi-turn conversation
|
||||
print("\n=== Example 3: Multi-turn Conversation ===")
|
||||
messages = [
|
||||
{"role": "user", "content": "I'm planning a trip to Japan."},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "That sounds exciting! Japan is a beautiful country with rich culture, delicious food, and stunning landscapes. What would you like to know about Japan to help with your trip planning?",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "What are the best times to visit Tokyo and Kyoto?",
|
||||
},
|
||||
]
|
||||
# result = await agent.kickoff_async(structured_query)
|
||||
|
||||
result = await agent.kickoff_async(messages)
|
||||
print(f"Response: {result.raw}")
|
||||
# if result.pydantic:
|
||||
# # Cast to the specific type for better IDE support
|
||||
# research_result = cast(ResearchResult, result.pydantic)
|
||||
# print(f"Main findings: {research_result.main_findings}")
|
||||
# print("\nKey points:")
|
||||
# for i, point in enumerate(research_result.key_points, 1):
|
||||
# print(f"{i}. {point}")
|
||||
# print("\nSources:")
|
||||
# for i, source in enumerate(research_result.sources, 1):
|
||||
# print(f"{i}. {source}")
|
||||
# else:
|
||||
# print(f"Raw response: {result.raw}")
|
||||
# print(
|
||||
# "\nNote: Structured output was not generated. The LLM may need more explicit instructions to format the response as JSON."
|
||||
# )
|
||||
|
||||
# Print usage metrics if available
|
||||
if result.usage_metrics:
|
||||
print("\nUsage metrics:")
|
||||
for key, value in result.usage_metrics.items():
|
||||
print(f"{key}: {value}")
|
||||
# # Example 3: Multi-turn conversation
|
||||
# print("\n=== Example 3: Multi-turn Conversation ===")
|
||||
# messages = [
|
||||
# {"role": "user", "content": "I'm planning a trip to Japan."},
|
||||
# {
|
||||
# "role": "assistant",
|
||||
# "content": "That sounds exciting! Japan is a beautiful country with rich culture, delicious food, and stunning landscapes. What would you like to know about Japan to help with your trip planning?",
|
||||
# },
|
||||
# {
|
||||
# "role": "user",
|
||||
# "content": "What are the best times to visit Tokyo and Kyoto?",
|
||||
# },
|
||||
# ]
|
||||
|
||||
# result = await agent.kickoff_async(messages)
|
||||
# print(f"Response: {result.raw}")
|
||||
|
||||
# # Print usage metrics if available
|
||||
# if result.usage_metrics:
|
||||
# print("\nUsage metrics:")
|
||||
# for key, value in result.usage_metrics.items():
|
||||
# print(f"{key}: {value}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -2,7 +2,7 @@ import uuid
|
||||
from abc import ABC, abstractmethod
|
||||
from copy import copy as shallow_copy
|
||||
from hashlib import md5
|
||||
from typing import Any, Dict, List, Optional, TypeVar
|
||||
from typing import Any, Callable, Dict, List, Optional, TypeVar
|
||||
|
||||
from pydantic import (
|
||||
UUID4,
|
||||
@@ -150,6 +150,9 @@ class BaseAgent(ABC, BaseModel):
|
||||
default_factory=SecurityConfig,
|
||||
description="Security configuration for the agent, including fingerprinting.",
|
||||
)
|
||||
callbacks: List[Callable] = Field(
|
||||
default=[], description="Callbacks to be used for the agent"
|
||||
)
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from crewai.memory.entity.entity_memory_item import EntityMemoryItem
|
||||
from crewai.memory.long_term.long_term_memory_item import LongTermMemoryItem
|
||||
|
||||
@@ -6,14 +6,11 @@ from typing import Any, Callable, Dict, List, Optional, Union
|
||||
from crewai.agents.agent_builder.base_agent import BaseAgent
|
||||
from crewai.agents.agent_builder.base_agent_executor_mixin import CrewAgentExecutorMixin
|
||||
from crewai.agents.parser import (
|
||||
FINAL_ANSWER_AND_PARSABLE_ACTION_ERROR_MESSAGE,
|
||||
AgentAction,
|
||||
AgentFinish,
|
||||
CrewAgentParser,
|
||||
OutputParserException,
|
||||
)
|
||||
from crewai.agents.tools_handler import ToolsHandler
|
||||
from crewai.lite_agent import LiteAgent
|
||||
from crewai.llm import LLM
|
||||
from crewai.tools.base_tool import BaseTool
|
||||
from crewai.tools.structured_tool import CrewStructuredTool
|
||||
@@ -21,7 +18,6 @@ from crewai.tools.tool_usage import ToolUsage, ToolUsageErrorException
|
||||
from crewai.utilities import I18N, Printer
|
||||
from crewai.utilities.agent_utils import (
|
||||
enforce_rpm_limit,
|
||||
format_answer,
|
||||
format_message_for_llm,
|
||||
get_llm_response,
|
||||
handle_max_iterations_exceeded,
|
||||
@@ -59,7 +55,7 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
|
||||
agent: BaseAgent,
|
||||
prompt: dict[str, str],
|
||||
max_iter: int,
|
||||
tools: List[Union[CrewStructuredTool, BaseTool]],
|
||||
tools: List[CrewStructuredTool],
|
||||
tools_names: str,
|
||||
stop_words: List[str],
|
||||
tools_description: str,
|
||||
|
||||
@@ -4,7 +4,7 @@ import re
|
||||
import uuid
|
||||
from typing import Any, Callable, Dict, List, Optional, Type, Union, cast
|
||||
|
||||
from pydantic import BaseModel, Field, PrivateAttr, model_validator
|
||||
from pydantic import BaseModel, Field, InstanceOf, PrivateAttr, model_validator
|
||||
|
||||
from crewai.agents.agent_builder.base_agent import BaseAgent
|
||||
from crewai.agents.agent_builder.utilities.base_token_process import TokenProcess
|
||||
@@ -12,17 +12,18 @@ from crewai.agents.cache import CacheHandler
|
||||
from crewai.agents.parser import (
|
||||
AgentAction,
|
||||
AgentFinish,
|
||||
CrewAgentParser,
|
||||
OutputParserException,
|
||||
)
|
||||
from crewai.agents.tools_handler import ToolsHandler
|
||||
from crewai.llm import LLM
|
||||
from crewai.tools.base_tool import BaseTool
|
||||
from crewai.tools.structured_tool import CrewStructuredTool
|
||||
from crewai.tools.tool_usage import ToolUsage, ToolUsageErrorException
|
||||
from crewai.types.usage_metrics import UsageMetrics
|
||||
from crewai.utilities import I18N
|
||||
from crewai.utilities.agent_utils import (
|
||||
enforce_rpm_limit,
|
||||
format_message_for_llm,
|
||||
get_llm_response,
|
||||
get_tool_names,
|
||||
handle_max_iterations_exceeded,
|
||||
@@ -32,12 +33,16 @@ from crewai.utilities.agent_utils import (
|
||||
render_text_description_and_args,
|
||||
)
|
||||
from crewai.utilities.events.agent_events import (
|
||||
LiteAgentExecutionCompletedEvent,
|
||||
LiteAgentExecutionErrorEvent,
|
||||
LiteAgentExecutionStartedEvent,
|
||||
)
|
||||
from crewai.utilities.events.crewai_event_bus import crewai_event_bus
|
||||
from crewai.utilities.events.tool_usage_events import ToolUsageStartedEvent
|
||||
from crewai.utilities.events.tool_usage_events import (
|
||||
ToolUsageErrorEvent,
|
||||
ToolUsageStartedEvent,
|
||||
)
|
||||
from crewai.utilities.exceptions.context_window_exceeding_exception import (
|
||||
LLMContextLengthExceededException,
|
||||
)
|
||||
from crewai.utilities.llm_utils import create_llm
|
||||
from crewai.utilities.printer import Printer
|
||||
from crewai.utilities.token_counter_callback import TokenCalcHandler
|
||||
@@ -103,7 +108,9 @@ class LiteAgent(BaseModel):
|
||||
role: str = Field(description="Role of the agent")
|
||||
goal: str = Field(description="Goal of the agent")
|
||||
backstory: str = Field(description="Backstory of the agent")
|
||||
llm: LLM = Field(description="Language model that will run the agent")
|
||||
llm: Union[str, InstanceOf[LLM], Any] = Field(
|
||||
description="Language model that will run the agent"
|
||||
)
|
||||
tools: List[BaseTool] = Field(
|
||||
default_factory=list, description="Tools at agent's disposal"
|
||||
)
|
||||
@@ -119,15 +126,17 @@ class LiteAgent(BaseModel):
|
||||
response_format: Optional[Type[BaseModel]] = Field(
|
||||
default=None, description="Pydantic model for structured output"
|
||||
)
|
||||
|
||||
step_callback: Optional[Any] = Field(
|
||||
default=None,
|
||||
description="Callback to be executed after each step of the agent execution.",
|
||||
)
|
||||
tools_results: List[Dict[str, Any]] = Field(
|
||||
default=[], description="Results of the tools used by the agent."
|
||||
)
|
||||
|
||||
respect_context_window: bool = Field(
|
||||
default=True,
|
||||
description="Whether to respect the context window of the LLM",
|
||||
)
|
||||
callbacks: List[Callable] = Field(
|
||||
default=[], description="Callbacks to be used for the agent"
|
||||
)
|
||||
_parsed_tools: List[CrewStructuredTool] = PrivateAttr(default_factory=list)
|
||||
_token_process: TokenProcess = PrivateAttr(default_factory=TokenProcess)
|
||||
_cache_handler: CacheHandler = PrivateAttr(default_factory=CacheHandler)
|
||||
_times_executed: int = PrivateAttr(default=0)
|
||||
@@ -143,6 +152,7 @@ class LiteAgent(BaseModel):
|
||||
_delegations: Dict[str, int] = PrivateAttr(default_factory=dict)
|
||||
# Internationalization
|
||||
_printer: Printer = PrivateAttr(default_factory=Printer)
|
||||
|
||||
i18n: I18N = Field(default=I18N(), description="Internationalization settings.")
|
||||
request_within_rpm_limit: Optional[Callable[[], bool]] = Field(
|
||||
default=None,
|
||||
@@ -152,16 +162,31 @@ class LiteAgent(BaseModel):
|
||||
default=True,
|
||||
description="Whether to use stop words to prevent the LLM from using tools",
|
||||
)
|
||||
tool_name_to_tool_map: Dict[str, Union[CrewStructuredTool, BaseTool]] = Field(
|
||||
default_factory=dict,
|
||||
description="Mapping of tool names to tool instances",
|
||||
)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def setup_llm(self):
|
||||
"""Set up the LLM and other components after initialization."""
|
||||
if self.llm is None:
|
||||
raise ValueError("LLM must be provided")
|
||||
|
||||
self.llm = create_llm(self.llm)
|
||||
if not isinstance(self.llm, LLM):
|
||||
self.llm = create_llm(self.llm)
|
||||
self.use_stop_words = self.llm.supports_stop_words()
|
||||
raise ValueError("Unable to create LLM instance")
|
||||
|
||||
# Initialize callbacks
|
||||
token_callback = TokenCalcHandler(token_cost_process=self._token_process)
|
||||
self._callbacks = [token_callback]
|
||||
|
||||
return self
|
||||
|
||||
@model_validator(mode="after")
|
||||
def parse_tools(self):
|
||||
"""Parse the tools and convert them to CrewStructuredTool instances."""
|
||||
self._parsed_tools = parse_tools(self.tools)
|
||||
|
||||
# Initialize tool name to tool mapping
|
||||
self.tool_name_to_tool_map = {tool.name: tool for tool in self._parsed_tools}
|
||||
|
||||
return self
|
||||
|
||||
@@ -177,14 +202,14 @@ class LiteAgent(BaseModel):
|
||||
|
||||
def _get_default_system_prompt(self) -> str:
|
||||
"""Get the default system prompt for the agent."""
|
||||
if self.tools:
|
||||
if self._parsed_tools:
|
||||
# Use the prompt template for agents with tools
|
||||
return self.i18n.slice("lite_agent_system_prompt_with_tools").format(
|
||||
role=self.role,
|
||||
backstory=self.backstory,
|
||||
goal=self.goal,
|
||||
tools=render_text_description_and_args(self.tools),
|
||||
tool_names=get_tool_names(self.tools),
|
||||
tools=render_text_description_and_args(self._parsed_tools),
|
||||
tool_names=get_tool_names(self._parsed_tools),
|
||||
)
|
||||
else:
|
||||
# Use the prompt template for agents without tools
|
||||
@@ -211,102 +236,6 @@ class LiteAgent(BaseModel):
|
||||
|
||||
return formatted_messages
|
||||
|
||||
def _extract_structured_output(self, text: str) -> Optional[BaseModel]:
|
||||
"""Extract structured output from text if response_format is set."""
|
||||
if not self.response_format:
|
||||
return None
|
||||
|
||||
try:
|
||||
# Try to extract JSON from the text
|
||||
json_match = re.search(r"```json\s*([\s\S]*?)\s*```", text)
|
||||
if json_match:
|
||||
json_str = json_match.group(1)
|
||||
json_data = json.loads(json_str)
|
||||
else:
|
||||
# Try to parse the entire text as JSON
|
||||
try:
|
||||
json_data = json.loads(text)
|
||||
except json.JSONDecodeError:
|
||||
# If that fails, use a more lenient approach to find JSON-like content
|
||||
potential_json = re.search(r"(\{[\s\S]*\})", text)
|
||||
if potential_json:
|
||||
json_data = json.loads(potential_json.group(1))
|
||||
else:
|
||||
return None
|
||||
|
||||
# Convert to Pydantic model
|
||||
return self.response_format.model_validate(json_data)
|
||||
except Exception as e:
|
||||
if self.verbose:
|
||||
print(f"Error extracting structured output: {e}")
|
||||
return None
|
||||
|
||||
def _preprocess_model_output(self, text: str) -> str:
|
||||
"""Preprocess the model output to correct common formatting issues."""
|
||||
# Skip if the text is empty
|
||||
if not text or text.strip() == "":
|
||||
return "Thought: I need to provide an answer.\n\nFinal Answer: I don't have enough information to provide a complete answer."
|
||||
|
||||
# Remove 'Action' or 'Final Answer' from anywhere after a proper Thought
|
||||
if "Thought:" in text and ("Action:" in text and "Final Answer:" in text):
|
||||
# This is a case where both Action and Final Answer appear - clear conflict
|
||||
# Check which one appears first and keep only that one
|
||||
action_index = text.find("Action:")
|
||||
final_answer_index = text.find("Final Answer:")
|
||||
|
||||
if action_index != -1 and final_answer_index != -1:
|
||||
if action_index < final_answer_index:
|
||||
# Keep only the Action part
|
||||
text = text[:final_answer_index]
|
||||
else:
|
||||
# Keep only the Final Answer part
|
||||
text = text[:action_index] + text[final_answer_index:]
|
||||
|
||||
if self.verbose:
|
||||
print("Removed conflicting Action/Final Answer parts")
|
||||
|
||||
# Check if this looks like a tool usage attempt without proper formatting
|
||||
if any(tool.name in text for tool in self.tools) and "Action:" not in text:
|
||||
# Try to extract tool name and input
|
||||
for tool in self.tools:
|
||||
if tool.name in text:
|
||||
# Find the tool name in the text
|
||||
parts = text.split(tool.name, 1)
|
||||
if len(parts) > 1:
|
||||
# Try to extract input as JSON
|
||||
input_text = parts[1]
|
||||
json_match = re.search(r"(\{[\s\S]*\})", input_text)
|
||||
|
||||
if json_match:
|
||||
# Construct a properly formatted response
|
||||
formatted = "Thought: I need to use a tool to help with this task.\n\n"
|
||||
formatted += f"Action: {tool.name}\n\n"
|
||||
formatted += f"Action Input: {json_match.group(1)}\n"
|
||||
|
||||
if self.verbose:
|
||||
print(f"Reformatted tool usage: {tool.name}")
|
||||
|
||||
return formatted
|
||||
|
||||
# Check if this looks like a final answer without proper formatting
|
||||
if (
|
||||
"Final Answer:" not in text
|
||||
and not any(tool.name in text for tool in self.tools)
|
||||
and "Action:" not in text
|
||||
):
|
||||
# This might be a direct response, format it as a final answer
|
||||
# Don't format if text already has a "Thought:" section
|
||||
if "Thought:" not in text:
|
||||
formatted = "Thought: I can now provide the final answer.\n\n"
|
||||
formatted += f"Final Answer: {text}\n"
|
||||
|
||||
if self.verbose:
|
||||
print("Reformatted as final answer")
|
||||
|
||||
return formatted
|
||||
|
||||
return text
|
||||
|
||||
def kickoff(self, messages: Union[str, List[Dict[str, str]]]) -> LiteAgentOutput:
|
||||
"""
|
||||
Execute the agent with the given messages.
|
||||
@@ -347,7 +276,7 @@ class LiteAgent(BaseModel):
|
||||
"role": self.role,
|
||||
"goal": self.goal,
|
||||
"backstory": self.backstory,
|
||||
"tools": self.tools,
|
||||
"tools": self._parsed_tools,
|
||||
"verbose": self.verbose,
|
||||
}
|
||||
|
||||
@@ -356,7 +285,7 @@ class LiteAgent(BaseModel):
|
||||
self,
|
||||
event=LiteAgentExecutionStartedEvent(
|
||||
agent_info=agent_info,
|
||||
tools=self.tools,
|
||||
tools=self._parsed_tools,
|
||||
messages=messages,
|
||||
),
|
||||
)
|
||||
@@ -364,61 +293,29 @@ class LiteAgent(BaseModel):
|
||||
try:
|
||||
# Execute the agent using invoke loop
|
||||
result = await self._invoke()
|
||||
|
||||
# Extract structured output if response_format is set
|
||||
pydantic_output = None
|
||||
if self.response_format:
|
||||
structured_output = self._extract_structured_output(result)
|
||||
if isinstance(structured_output, BaseModel):
|
||||
pydantic_output = structured_output
|
||||
|
||||
# Create output object
|
||||
usage_metrics = {}
|
||||
if hasattr(self._token_process, "get_summary"):
|
||||
usage_metrics_obj = self._token_process.get_summary()
|
||||
if isinstance(usage_metrics_obj, UsageMetrics):
|
||||
usage_metrics = usage_metrics_obj.model_dump()
|
||||
|
||||
output = LiteAgentOutput(
|
||||
raw=result,
|
||||
pydantic=pydantic_output,
|
||||
agent_role=self.role,
|
||||
usage_metrics=usage_metrics,
|
||||
except AssertionError:
|
||||
self._printer.print(
|
||||
content="Agent failed to reach a final answer. This is likely a bug - please report it.",
|
||||
color="red",
|
||||
)
|
||||
|
||||
# Emit event for agent execution completion
|
||||
crewai_event_bus.emit(
|
||||
self,
|
||||
event=LiteAgentExecutionCompletedEvent(
|
||||
agent_info=agent_info,
|
||||
output=result,
|
||||
),
|
||||
)
|
||||
|
||||
return output
|
||||
|
||||
raise
|
||||
except Exception as e:
|
||||
# Emit event for agent execution error
|
||||
crewai_event_bus.emit(
|
||||
self,
|
||||
event=LiteAgentExecutionErrorEvent(
|
||||
agent_info=agent_info,
|
||||
error=str(e),
|
||||
),
|
||||
)
|
||||
self._handle_unknown_error(e)
|
||||
if e.__class__.__module__.startswith("litellm"):
|
||||
# Do not retry on litellm errors
|
||||
raise e
|
||||
else:
|
||||
raise e
|
||||
|
||||
# Retry if we haven't exceeded the retry limit
|
||||
self._times_executed += 1
|
||||
if self._times_executed <= self._max_retry_limit:
|
||||
if self.verbose:
|
||||
print(
|
||||
f"Retrying agent execution ({self._times_executed}/{self._max_retry_limit})..."
|
||||
)
|
||||
return await self.kickoff_async(messages)
|
||||
# TODO: CREATE AND RETURN LiteAgentOutput
|
||||
return LiteAgentOutput(
|
||||
raw=result.text,
|
||||
pydantic=None, # TODO: Add pydantic output
|
||||
agent_role=self.role,
|
||||
usage_metrics=None, # TODO: Add usage metrics
|
||||
)
|
||||
|
||||
raise e
|
||||
|
||||
async def _invoke(self) -> str:
|
||||
async def _invoke(self) -> AgentFinish:
|
||||
"""
|
||||
Run the agent's thought process until it reaches a conclusion or max iterations.
|
||||
Similar to _invoke_loop in CrewAgentExecutor.
|
||||
@@ -426,18 +323,8 @@ class LiteAgent(BaseModel):
|
||||
Returns:
|
||||
str: The final result of the agent execution.
|
||||
"""
|
||||
# # Set up tools handler for tool execution
|
||||
# tools_handler = ToolsHandler(cache=self._cache_handler)
|
||||
|
||||
# TODO: MOVE TO INIT
|
||||
# Set up callbacks for token tracking
|
||||
token_callback = TokenCalcHandler(token_cost_process=self._token_process)
|
||||
callbacks = [token_callback]
|
||||
|
||||
# # Prepare tool configurations
|
||||
# parsed_tools = parse_tools(self.tools)
|
||||
# tools_description = render_text_description_and_args(parsed_tools)
|
||||
# tools_names = get_tool_names(parsed_tools)
|
||||
# Use the stored callbacks
|
||||
callbacks = self._callbacks
|
||||
|
||||
# Execute the agent loop
|
||||
formatted_answer = None
|
||||
@@ -449,14 +336,14 @@ class LiteAgent(BaseModel):
|
||||
printer=self._printer,
|
||||
i18n=self.i18n,
|
||||
messages=self._messages,
|
||||
llm=self.llm,
|
||||
llm=cast(LLM, self.llm),
|
||||
callbacks=callbacks,
|
||||
)
|
||||
|
||||
enforce_rpm_limit(self.request_within_rpm_limit)
|
||||
|
||||
answer = get_llm_response(
|
||||
llm=self.llm,
|
||||
llm=cast(LLM, self.llm),
|
||||
messages=self._messages,
|
||||
callbacks=callbacks,
|
||||
printer=self._printer,
|
||||
@@ -471,11 +358,31 @@ class LiteAgent(BaseModel):
|
||||
formatted_answer, tool_result
|
||||
)
|
||||
|
||||
self._invoke_step_callback(formatted_answer)
|
||||
self._append_message(formatted_answer.text, role="assistant")
|
||||
except OutputParserException as e:
|
||||
formatted_answer = self._handle_output_parser_exception(e)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
if e.__class__.__module__.startswith("litellm"):
|
||||
# Do not retry on litellm errors
|
||||
raise e
|
||||
if self._is_context_length_exceeded(e):
|
||||
self._handle_context_length()
|
||||
continue
|
||||
else:
|
||||
self._handle_unknown_error(e)
|
||||
raise e
|
||||
|
||||
finally:
|
||||
self._iterations += 1
|
||||
|
||||
# During the invoke loop, formatted_answer alternates between AgentAction
|
||||
# (when the agent is using tools) and eventually becomes AgentFinish
|
||||
# (when the agent reaches a final answer). This assertion confirms we've
|
||||
# reached a final answer and helps type checking understand this transition.
|
||||
assert isinstance(formatted_answer, AgentFinish)
|
||||
self._show_logs(formatted_answer)
|
||||
return formatted_answer
|
||||
|
||||
def _execute_tool_and_check_finality(self, agent_action: AgentAction) -> ToolResult:
|
||||
try:
|
||||
@@ -490,12 +397,12 @@ class LiteAgent(BaseModel):
|
||||
),
|
||||
)
|
||||
tool_usage = ToolUsage(
|
||||
tools=self.tools,
|
||||
original_tools=self.tools, # TODO: INVESTIGATE DIFF BETWEEN THIS AND ABOVE
|
||||
tools_description=render_text_description_and_args(self.tools),
|
||||
tools_names=get_tool_names(self.tools),
|
||||
agent=self,
|
||||
tools=self._parsed_tools,
|
||||
action=agent_action,
|
||||
tools_handler=None,
|
||||
task=None,
|
||||
function_calling_llm=None,
|
||||
)
|
||||
tool_calling = tool_usage.parse_tool_calling(agent_action.text)
|
||||
|
||||
@@ -504,9 +411,9 @@ class LiteAgent(BaseModel):
|
||||
return ToolResult(result=tool_result, result_as_answer=False)
|
||||
else:
|
||||
if tool_calling.tool_name.casefold().strip() in [
|
||||
name.casefold().strip() for name in self.tool_name_to_tool_map
|
||||
tool.name.casefold().strip() for tool in self._parsed_tools
|
||||
] or tool_calling.tool_name.casefold().replace("_", " ") in [
|
||||
name.casefold().strip() for name in self.tool_name_to_tool_map
|
||||
tool.name.casefold().strip() for tool in self._parsed_tools
|
||||
]:
|
||||
tool_result = tool_usage.use(tool_calling, agent_action.text)
|
||||
tool = self.tool_name_to_tool_map.get(tool_calling.tool_name)
|
||||
@@ -515,23 +422,164 @@ class LiteAgent(BaseModel):
|
||||
result=tool_result, result_as_answer=tool.result_as_answer
|
||||
)
|
||||
else:
|
||||
tool_result = self._i18n.errors("wrong_tool_name").format(
|
||||
tool_result = self.i18n.errors("wrong_tool_name").format(
|
||||
tool=tool_calling.tool_name,
|
||||
tools=", ".join([tool.name.casefold() for tool in self.tools]),
|
||||
tools=", ".join(
|
||||
[tool.name.casefold() for tool in self._parsed_tools]
|
||||
),
|
||||
)
|
||||
return ToolResult(result=tool_result, result_as_answer=False)
|
||||
|
||||
except Exception as e:
|
||||
if self.agent:
|
||||
crewai_event_bus.emit(
|
||||
self,
|
||||
event=ToolUsageErrorEvent( # validation error
|
||||
agent_key=self.agent.key,
|
||||
agent_role=self.agent.role,
|
||||
tool_name=agent_action.tool,
|
||||
tool_args=agent_action.tool_input,
|
||||
tool_class=agent_action.tool,
|
||||
error=str(e),
|
||||
),
|
||||
)
|
||||
crewai_event_bus.emit(
|
||||
self,
|
||||
event=ToolUsageErrorEvent(
|
||||
agent_key=self.key,
|
||||
agent_role=self.role,
|
||||
tool_name=agent_action.tool,
|
||||
tool_args=agent_action.tool_input,
|
||||
tool_class=agent_action.tool,
|
||||
error=str(e),
|
||||
),
|
||||
)
|
||||
raise e
|
||||
|
||||
def _handle_agent_action(
|
||||
self, formatted_answer: AgentAction, tool_result: ToolResult
|
||||
) -> Union[AgentAction, AgentFinish]:
|
||||
"""Handle the AgentAction, execute tools, and process the results."""
|
||||
|
||||
formatted_answer.text += f"\nObservation: {tool_result.result}"
|
||||
formatted_answer.result = tool_result.result
|
||||
|
||||
if tool_result.result_as_answer:
|
||||
return AgentFinish(
|
||||
thought="",
|
||||
output=tool_result.result,
|
||||
text=formatted_answer.text,
|
||||
)
|
||||
|
||||
self._show_logs(formatted_answer)
|
||||
return formatted_answer
|
||||
|
||||
def _show_logs(self, formatted_answer: Union[AgentAction, AgentFinish]):
|
||||
if self.verbose:
|
||||
agent_role = self.role.split("\n")[0]
|
||||
if isinstance(formatted_answer, AgentAction):
|
||||
thought = re.sub(r"\n+", "\n", formatted_answer.thought)
|
||||
formatted_json = json.dumps(
|
||||
formatted_answer.tool_input,
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
self._printer.print(
|
||||
content=f"\n\n\033[1m\033[95m# Agent:\033[00m \033[1m\033[92m{agent_role}\033[00m"
|
||||
)
|
||||
if thought and thought != "":
|
||||
self._printer.print(
|
||||
content=f"\033[95m## Thought:\033[00m \033[92m{thought}\033[00m"
|
||||
)
|
||||
self._printer.print(
|
||||
content=f"\033[95m## Using tool:\033[00m \033[92m{formatted_answer.tool}\033[00m"
|
||||
)
|
||||
self._printer.print(
|
||||
content=f"\033[95m## Tool Input:\033[00m \033[92m\n{formatted_json}\033[00m"
|
||||
)
|
||||
self._printer.print(
|
||||
content=f"\033[95m## Tool Output:\033[00m \033[92m\n{formatted_answer.result}\033[00m"
|
||||
)
|
||||
elif isinstance(formatted_answer, AgentFinish):
|
||||
self._printer.print(
|
||||
content=f"\n\n\033[1m\033[95m# Agent:\033[00m \033[1m\033[92m{agent_role}\033[00m"
|
||||
)
|
||||
self._printer.print(
|
||||
content=f"\033[95m## Final Answer:\033[00m \033[92m\n{formatted_answer.output}\033[00m\n\n"
|
||||
)
|
||||
|
||||
def _append_message(self, text: str, role: str = "assistant") -> None:
|
||||
"""Append a message to the message list with the given role."""
|
||||
self._messages.append(format_message_for_llm(text, role=role))
|
||||
|
||||
def _handle_output_parser_exception(self, e: OutputParserException) -> AgentAction:
|
||||
"""Handle OutputParserException by updating messages and formatted_answer."""
|
||||
self._messages.append({"role": "user", "content": e.error})
|
||||
|
||||
formatted_answer = AgentAction(
|
||||
text=e.error,
|
||||
tool="",
|
||||
tool_input="",
|
||||
thought="",
|
||||
)
|
||||
|
||||
MAX_ITERATIONS = 3
|
||||
if self._iterations > MAX_ITERATIONS:
|
||||
self._printer.print(
|
||||
content=f"Error parsing LLM output, agent will retry: {e.error}",
|
||||
color="red",
|
||||
)
|
||||
|
||||
return formatted_answer
|
||||
|
||||
def _is_context_length_exceeded(self, exception: Exception) -> bool:
|
||||
"""Check if the exception is due to context length exceeding."""
|
||||
return LLMContextLengthExceededException(
|
||||
str(exception)
|
||||
)._is_context_limit_error(str(exception))
|
||||
|
||||
def _handle_context_length(self) -> None:
|
||||
if self.respect_context_window:
|
||||
self._printer.print(
|
||||
content="Context length exceeded. Summarizing content to fit the model context window.",
|
||||
color="yellow",
|
||||
)
|
||||
self._summarize_messages()
|
||||
else:
|
||||
self._printer.print(
|
||||
content="Context length exceeded. Consider using smaller text or RAG tools from crewai_tools.",
|
||||
color="red",
|
||||
)
|
||||
raise SystemExit(
|
||||
"Context length exceeded and user opted not to summarize. Consider using smaller text or RAG tools from crewai_tools."
|
||||
)
|
||||
|
||||
def _summarize_messages(self) -> None:
|
||||
messages_groups = []
|
||||
for message in self.messages:
|
||||
content = message["content"]
|
||||
cut_size = cast(LLM, self.llm).get_context_window_size()
|
||||
for i in range(0, len(content), cut_size):
|
||||
messages_groups.append(content[i : i + cut_size])
|
||||
|
||||
summarized_contents = []
|
||||
for group in messages_groups:
|
||||
summary = cast(LLM, self.llm).call(
|
||||
[
|
||||
format_message_for_llm(
|
||||
self.i18n.slice("summarizer_system_message"), role="system"
|
||||
),
|
||||
format_message_for_llm(
|
||||
self.i18n.slice("summarize_instruction").format(group=group),
|
||||
),
|
||||
],
|
||||
callbacks=self.callbacks,
|
||||
)
|
||||
summarized_contents.append(summary)
|
||||
|
||||
merged_summary = " ".join(str(content) for content in summarized_contents)
|
||||
|
||||
self.messages = [
|
||||
format_message_for_llm(
|
||||
self.i18n.slice("summary").format(merged_summary=merged_summary)
|
||||
)
|
||||
]
|
||||
|
||||
def _handle_unknown_error(self, exception: Exception) -> None:
|
||||
"""Handle unknown errors by informing the user."""
|
||||
self._printer.print(
|
||||
content="An unknown error occurred. Please check the details below.",
|
||||
color="red",
|
||||
)
|
||||
self._printer.print(
|
||||
content=f"Error details: {exception}",
|
||||
color="red",
|
||||
)
|
||||
|
||||
@@ -5,14 +5,12 @@ import time
|
||||
from difflib import SequenceMatcher
|
||||
from json import JSONDecodeError
|
||||
from textwrap import dedent
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
|
||||
|
||||
import json5
|
||||
from json_repair import repair_json
|
||||
|
||||
from crewai.agents.agent_builder.base_agent import BaseAgent
|
||||
from crewai.agents.tools_handler import ToolsHandler
|
||||
from crewai.lite_agent import LiteAgent
|
||||
from crewai.task import Task
|
||||
from crewai.telemetry import Telemetry
|
||||
from crewai.tools import BaseTool
|
||||
@@ -31,6 +29,10 @@ from crewai.utilities.events.tool_usage_events import (
|
||||
ToolValidateInputErrorEvent,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from crewai.agents.agent_builder.base_agent import BaseAgent
|
||||
from crewai.lite_agent import LiteAgent
|
||||
|
||||
OPENAI_BIGGER_MODELS = [
|
||||
"gpt-4",
|
||||
"gpt-4o",
|
||||
@@ -67,10 +69,10 @@ class ToolUsage:
|
||||
def __init__(
|
||||
self,
|
||||
tools_handler: Optional[ToolsHandler],
|
||||
tools: List[Union[CrewStructuredTool, BaseTool]],
|
||||
task: Task,
|
||||
tools: List[CrewStructuredTool],
|
||||
task: Optional[Task],
|
||||
function_calling_llm: Any,
|
||||
agent: Union[BaseAgent, LiteAgent],
|
||||
agent: Union["BaseAgent", "LiteAgent"],
|
||||
action: Any,
|
||||
) -> None:
|
||||
self._i18n: I18N = agent.i18n
|
||||
@@ -103,18 +105,21 @@ class ToolUsage:
|
||||
def use(
|
||||
self, calling: Union[ToolCalling, InstructorToolCalling], tool_string: str
|
||||
) -> str:
|
||||
print("USING A TOOL", calling, tool_string)
|
||||
if isinstance(calling, ToolUsageErrorException):
|
||||
error = calling.message
|
||||
if self.agent.verbose:
|
||||
self._printer.print(content=f"\n\n{error}\n", color="red")
|
||||
self.task.increment_tools_errors()
|
||||
if self.task:
|
||||
self.task.increment_tools_errors()
|
||||
return error
|
||||
|
||||
try:
|
||||
tool = self._select_tool(calling.tool_name)
|
||||
except Exception as e:
|
||||
error = getattr(e, "message", str(e))
|
||||
self.task.increment_tools_errors()
|
||||
if self.task:
|
||||
self.task.increment_tools_errors()
|
||||
if self.agent.verbose:
|
||||
self._printer.print(content=f"\n\n{error}\n", color="red")
|
||||
return error
|
||||
@@ -126,7 +131,8 @@ class ToolUsage:
|
||||
|
||||
except Exception as e:
|
||||
error = getattr(e, "message", str(e))
|
||||
self.task.increment_tools_errors()
|
||||
if self.task:
|
||||
self.task.increment_tools_errors()
|
||||
if self.agent.verbose:
|
||||
self._printer.print(content=f"\n\n{error}\n", color="red")
|
||||
return error
|
||||
@@ -139,6 +145,8 @@ class ToolUsage:
|
||||
tool: CrewStructuredTool,
|
||||
calling: Union[ToolCalling, InstructorToolCalling],
|
||||
) -> str:
|
||||
print("USING A TOOL: ", tool)
|
||||
print("Type of tool: ", type(tool))
|
||||
if self._check_tool_repeated_usage(calling=calling): # type: ignore # _check_tool_repeated_usage of "ToolUsage" does not return a value (it only ever returns None)
|
||||
try:
|
||||
result = self._i18n.errors("task_repeated_usage").format(
|
||||
@@ -153,7 +161,8 @@ class ToolUsage:
|
||||
return result # type: ignore # Fix the return type of this function
|
||||
|
||||
except Exception:
|
||||
self.task.increment_tools_errors()
|
||||
if self.task:
|
||||
self.task.increment_tools_errors()
|
||||
|
||||
started_at = time.time()
|
||||
from_cache = False
|
||||
@@ -184,7 +193,8 @@ class ToolUsage:
|
||||
coworker = (
|
||||
calling.arguments.get("coworker") if calling.arguments else None
|
||||
)
|
||||
self.task.increment_delegations(coworker)
|
||||
if self.task:
|
||||
self.task.increment_delegations(coworker)
|
||||
|
||||
if calling.arguments:
|
||||
try:
|
||||
@@ -211,14 +221,16 @@ class ToolUsage:
|
||||
error = ToolUsageErrorException(
|
||||
f'\n{error_message}.\nMoving on then. {self._i18n.slice("format").format(tool_names=self.tools_names)}'
|
||||
).message
|
||||
self.task.increment_tools_errors()
|
||||
if self.task:
|
||||
self.task.increment_tools_errors()
|
||||
if self.agent.verbose:
|
||||
self._printer.print(
|
||||
content=f"\n\n{error_message}\n", color="red"
|
||||
)
|
||||
return error # type: ignore # No return value expected
|
||||
|
||||
self.task.increment_tools_errors()
|
||||
if self.task:
|
||||
self.task.increment_tools_errors()
|
||||
return self.use(calling=calling, tool_string=tool_string) # type: ignore # No return value expected
|
||||
|
||||
if self.tools_handler:
|
||||
@@ -266,13 +278,16 @@ class ToolUsage:
|
||||
return result # type: ignore # No return value expected
|
||||
|
||||
def _format_result(self, result: Any) -> None:
|
||||
self.task.used_tools += 1
|
||||
if self.task:
|
||||
self.task.used_tools += 1
|
||||
if self._should_remember_format(): # type: ignore # "_should_remember_format" of "ToolUsage" does not return a value (it only ever returns None)
|
||||
result = self._remember_format(result=result) # type: ignore # "_remember_format" of "ToolUsage" does not return a value (it only ever returns None)
|
||||
return result
|
||||
|
||||
def _should_remember_format(self) -> bool:
|
||||
return self.task.used_tools % self._remember_format_after_usages == 0
|
||||
if self.task:
|
||||
return self.task.used_tools % self._remember_format_after_usages == 0
|
||||
return False
|
||||
|
||||
def _remember_format(self, result: str) -> None:
|
||||
result = str(result)
|
||||
@@ -308,7 +323,8 @@ class ToolUsage:
|
||||
> 0.85
|
||||
):
|
||||
return tool
|
||||
self.task.increment_tools_errors()
|
||||
if self.task:
|
||||
self.task.increment_tools_errors()
|
||||
tool_selection_data = {
|
||||
"agent_key": self.agent.key,
|
||||
"agent_role": self.agent.role,
|
||||
@@ -421,7 +437,8 @@ class ToolUsage:
|
||||
self._run_attempts += 1
|
||||
if self._run_attempts > self._max_parsing_attempts:
|
||||
self._telemetry.tool_usage_error(llm=self.function_calling_llm)
|
||||
self.task.increment_tools_errors()
|
||||
if self.task:
|
||||
self.task.increment_tools_errors()
|
||||
if self.agent.verbose:
|
||||
self._printer.print(content=f"\n\n{e}\n", color="red")
|
||||
return ToolUsageErrorException( # type: ignore # Incompatible return value type (got "ToolUsageErrorException", expected "ToolCalling | InstructorToolCalling")
|
||||
@@ -452,6 +469,7 @@ class ToolUsage:
|
||||
if isinstance(arguments, dict):
|
||||
return arguments
|
||||
except (ValueError, SyntaxError):
|
||||
repaired_input = repair_json(tool_input)
|
||||
pass # Continue to the next parsing attempt
|
||||
|
||||
# Attempt 3: Parse as JSON5
|
||||
@@ -530,7 +548,7 @@ class ToolUsage:
|
||||
"agent_key": self.agent.key,
|
||||
"agent_role": (self.agent._original_role or self.agent.role),
|
||||
"run_attempts": self._run_attempts,
|
||||
"delegations": self.task.delegations,
|
||||
"delegations": self.task.delegations if self.task else 0,
|
||||
"tool_name": tool.name,
|
||||
"tool_args": tool_calling.arguments,
|
||||
"tool_class": tool.__class__.__name__,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from typing import Any, Callable, Dict, List, Optional, Union
|
||||
from typing import Any, Callable, Dict, List, Optional, Sequence, Union
|
||||
|
||||
from crewai.agent import Agent
|
||||
from crewai.agents.parser import (
|
||||
FINAL_ANSWER_AND_PARSABLE_ACTION_ERROR_MESSAGE,
|
||||
AgentAction,
|
||||
@@ -8,45 +7,34 @@ from crewai.agents.parser import (
|
||||
CrewAgentParser,
|
||||
OutputParserException,
|
||||
)
|
||||
from crewai.lite_agent import ToolResult
|
||||
from crewai.llm import LLM
|
||||
from crewai.tools import BaseTool as CrewAITool
|
||||
from crewai.tools.base_tool import BaseTool
|
||||
from crewai.tools.structured_tool import CrewStructuredTool
|
||||
from crewai.tools.tool_usage import ToolUsage, ToolUsageErrorException
|
||||
from crewai.utilities.events import crewai_event_bus
|
||||
from crewai.utilities.events.tool_usage_events import (
|
||||
ToolUsageErrorEvent,
|
||||
ToolUsageStartedEvent,
|
||||
)
|
||||
from crewai.utilities.i18n import I18N
|
||||
from crewai.utilities.printer import Printer
|
||||
|
||||
|
||||
def parse_tools(tools: List[BaseTool]) -> List[Union[CrewStructuredTool, BaseTool]]:
|
||||
def parse_tools(tools: List[BaseTool]) -> List[CrewStructuredTool]:
|
||||
"""Parse tools to be used for the task."""
|
||||
tools_list = []
|
||||
try:
|
||||
for tool in tools:
|
||||
if isinstance(tool, CrewAITool):
|
||||
tools_list.append(tool.to_structured_tool())
|
||||
else:
|
||||
tools_list.append(tool)
|
||||
except ModuleNotFoundError:
|
||||
tools_list = []
|
||||
for tool in tools:
|
||||
tools_list.append(tool)
|
||||
|
||||
for tool in tools:
|
||||
if isinstance(tool, CrewAITool):
|
||||
tools_list.append(tool.to_structured_tool())
|
||||
else:
|
||||
raise ValueError("Tool is not a CrewStructuredTool or BaseTool")
|
||||
|
||||
return tools_list
|
||||
|
||||
|
||||
def get_tool_names(tools: List[Union[CrewStructuredTool, BaseTool]]) -> str:
|
||||
def get_tool_names(tools: Sequence[Union[CrewStructuredTool, BaseTool]]) -> str:
|
||||
"""Get the names of the tools."""
|
||||
return ", ".join([t.name for t in tools])
|
||||
|
||||
|
||||
def render_text_description_and_args(
|
||||
tools: List[Union[CrewStructuredTool, BaseTool]]
|
||||
tools: Sequence[Union[CrewStructuredTool, BaseTool]]
|
||||
) -> str:
|
||||
"""Render the tool name, description, and args in plain text.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user