|
|
import json |
|
|
from pydantic import model_validator |
|
|
from pydantic_core import PydanticUndefined |
|
|
from typing import Optional, Type, Tuple, Union, List, Any |
|
|
|
|
|
from ..core.module import BaseModule |
|
|
from ..core.module_utils import get_type_name |
|
|
from ..core.registry import MODULE_REGISTRY |
|
|
|
|
|
from ..core.parser import Parser |
|
|
from ..core.message import Message |
|
|
from ..models.base_model import BaseLLM, LLMOutputParser |
|
|
from ..tools.tool import Toolkit |
|
|
from ..prompts.context_extraction import CONTEXT_EXTRACTION |
|
|
from ..prompts.template import PromptTemplate |
|
|
|
|
|
|
|
|
class ActionInput(LLMOutputParser): |
|
|
"""Input specification and parsing for actions. |
|
|
|
|
|
This class defines the input requirements for actions and provides methods |
|
|
to generate structured input specifications. It inherits from LLMOutputParser |
|
|
to allow parsing of LLM outputs into structured inputs for actions. |
|
|
|
|
|
Notes: |
|
|
Parameters in ActionInput should be defined in Pydantic Field format. |
|
|
For optional variables, use format: |
|
|
var: Optional[int] = Field(default=None, description="xxx") |
|
|
Remember to add `default=None` for optional parameters. |
|
|
""" |
|
|
|
|
|
@classmethod |
|
|
def get_input_specification(cls, ignore_fields: List[str] = []) -> str: |
|
|
"""Generate a JSON specification of the input requirements. |
|
|
|
|
|
Examines the class fields and produces a structured specification of |
|
|
the input parameters, including their types, descriptions, and whether |
|
|
they are required. |
|
|
|
|
|
Args: |
|
|
ignore_fields (List[str]): List of field names to exclude from the specification. |
|
|
|
|
|
Returns: |
|
|
A JSON string containing the input specification, or an empty string |
|
|
if no fields are defined or all are ignored. |
|
|
""" |
|
|
fields_info = {} |
|
|
attrs = cls.get_attrs() |
|
|
for field_name, field_info in cls.model_fields.items(): |
|
|
if field_name in ignore_fields: |
|
|
continue |
|
|
if field_name not in attrs: |
|
|
continue |
|
|
field_type = get_type_name(field_info.annotation) |
|
|
field_desc = field_info.description if field_info.description is not None else None |
|
|
|
|
|
field_default = str(field_info.default) if field_info.default is not PydanticUndefined else None |
|
|
field_required = True if field_default is None else False |
|
|
description = field_type + ", " |
|
|
if field_desc is not None: |
|
|
description += (field_desc.strip() + ", ") |
|
|
description += ("required" if field_required else "optional") |
|
|
if field_default is not None: |
|
|
description += (", Default value: " + field_default) |
|
|
fields_info[field_name] = description |
|
|
|
|
|
if len(fields_info) == 0: |
|
|
return "" |
|
|
fields_info_str = json.dumps(fields_info, indent=4) |
|
|
return fields_info_str |
|
|
|
|
|
@classmethod |
|
|
def get_required_input_names(cls) -> List[str]: |
|
|
"""Get a list of all required input parameter names. |
|
|
|
|
|
Returns: |
|
|
List[str]: Names of all parameters that are required (don't have default values). |
|
|
""" |
|
|
required_fields = [] |
|
|
attrs = cls.get_attrs() |
|
|
for field_name, field_info in cls.model_fields.items(): |
|
|
if field_name not in attrs: |
|
|
continue |
|
|
field_default = field_info.default |
|
|
|
|
|
if field_default is PydanticUndefined: |
|
|
required_fields.append(field_name) |
|
|
return required_fields |
|
|
|
|
|
|
|
|
class ActionOutput(LLMOutputParser): |
|
|
"""Output representation for actions. |
|
|
|
|
|
This class handles the structured output of actions, providing methods |
|
|
to convert the output to structured data. It inherits from LLMOutputParser |
|
|
to support parsing of LLM outputs into structured action results. |
|
|
""" |
|
|
|
|
|
def to_str(self) -> str: |
|
|
"""Convert the output to a formatted JSON string. |
|
|
|
|
|
Returns: |
|
|
A pretty-printed JSON string representation of the structured data. |
|
|
""" |
|
|
return json.dumps(self.get_structured_data(), indent=4) |
|
|
|
|
|
|
|
|
class Action(BaseModule): |
|
|
"""Base class for all actions in the EvoAgentX framework. |
|
|
|
|
|
Actions represent discrete operations that can be performed by agents. |
|
|
They define inputs, outputs, and execution behavior, and can optionally |
|
|
use tools to accomplish their tasks. |
|
|
|
|
|
Attributes: |
|
|
name (str): Unique identifier for the action. |
|
|
description (str): Human-readable description of what the action does. |
|
|
prompt (Optional[str]): Optional prompt template for this action. |
|
|
tools (Optional[List[Toolkit]]): Optional list of tools that can be used by this action. |
|
|
inputs_format (Optional[Type[ActionInput]]): Optional class defining the expected input structure. |
|
|
outputs_format (Optional[Type[Parser]]): Optional class defining the expected output structure. |
|
|
""" |
|
|
|
|
|
name: str |
|
|
description: str |
|
|
prompt: Optional[str] = None |
|
|
prompt_template: Optional[PromptTemplate] = None |
|
|
tools: Optional[List[Toolkit]] = None |
|
|
inputs_format: Optional[Type[ActionInput]] = None |
|
|
outputs_format: Optional[Type[Parser]] = None |
|
|
|
|
|
def init_module(self): |
|
|
"""Initialize the action module. |
|
|
|
|
|
This method is called after the action is instantiated. |
|
|
Subclasses can override this to perform custom initialization. |
|
|
""" |
|
|
pass |
|
|
|
|
|
def to_dict(self, exclude_none: bool = True, ignore: List[str] = [], **kwargs) -> dict: |
|
|
""" |
|
|
Convert the action to a dictionary for saving. |
|
|
""" |
|
|
data = super().to_dict(exclude_none=exclude_none, ignore=ignore, **kwargs) |
|
|
if self.inputs_format: |
|
|
data["inputs_format"] = self.inputs_format.__name__ |
|
|
if self.outputs_format: |
|
|
data["outputs_format"] = self.outputs_format.__name__ |
|
|
|
|
|
return data |
|
|
|
|
|
@model_validator(mode="before") |
|
|
@classmethod |
|
|
def validate_data(cls, data: Any) -> Any: |
|
|
if "inputs_format" in data and data["inputs_format"] and isinstance(data["inputs_format"], str): |
|
|
|
|
|
data["inputs_format"] = MODULE_REGISTRY.get_module(data["inputs_format"]) |
|
|
if "outputs_format" in data and data["outputs_format"] and isinstance(data["outputs_format"], str): |
|
|
|
|
|
data["outputs_format"] = MODULE_REGISTRY.get_module(data["outputs_format"]) |
|
|
|
|
|
return data |
|
|
|
|
|
def execute(self, llm: Optional[BaseLLM] = None, inputs: Optional[dict] = None, sys_msg: Optional[str]=None, return_prompt: bool = False, **kwargs) -> Optional[Union[Parser, Tuple[Parser, str]]]: |
|
|
"""Execute the action to produce a result. |
|
|
|
|
|
This is the main entry point for executing an action. Subclasses must |
|
|
implement this method to define the action's behavior. |
|
|
|
|
|
Args: |
|
|
llm (Optional[BaseLLM]): The LLM used to execute the action. |
|
|
inputs (Optional[dict]): Input data for the action execution. The input data should be a dictionary that matches the input format of the provided prompt. |
|
|
For example, if the prompt contains a variable `{input_var}`, the `inputs` dictionary should have a key `input_var`, otherwise the variable will be set to empty string. |
|
|
sys_msg (Optional[str]): Optional system message for the LLM. |
|
|
return_prompt (bool): Whether to return the complete prompt passed to the LLM. |
|
|
**kwargs (Any): Additional keyword arguments for the execution. |
|
|
|
|
|
Returns: |
|
|
If `return_prompt` is False, the method returns a Parser object containing the structured result of the action. |
|
|
If `return_prompt` is True, the method returns a tuple containing the Parser object and the complete prompt passed to the LLM. |
|
|
""" |
|
|
raise NotImplementedError(f"`execute` function of {type(self).__name__} is not implemented!") |
|
|
|
|
|
async def async_execute(self, llm: Optional[BaseLLM] = None, inputs: Optional[dict] = None, sys_msg: Optional[str]=None, return_prompt: bool = False, **kwargs) -> Optional[Union[Parser, Tuple[Parser, str]]]: |
|
|
""" |
|
|
Asynchronous execution of the action. |
|
|
|
|
|
This method is the asynchronous counterpart of the `execute` method. |
|
|
It allows the action to be executed asynchronously using an LLM. |
|
|
""" |
|
|
raise NotImplementedError(f"`async_execute` function of {type(self).__name__} is not implemented!") |
|
|
|
|
|
class ContextExtraction(Action): |
|
|
"""Action for extracting structured inputs from context. |
|
|
|
|
|
This action analyzes a conversation context to extract relevant information |
|
|
that can be used as inputs for other actions. It uses the LLM to interpret |
|
|
unstructured contextual information and format it according to the target |
|
|
action's input requirements. |
|
|
""" |
|
|
|
|
|
def __init__(self, **kwargs): |
|
|
name = kwargs.pop("name") if "name" in kwargs else CONTEXT_EXTRACTION["name"] |
|
|
description = kwargs.pop("description") if "description" in kwargs else CONTEXT_EXTRACTION["description"] |
|
|
super().__init__(name=name, description=description, **kwargs) |
|
|
|
|
|
def get_context_from_messages(self, messages: List[Message]) -> str: |
|
|
str_context = "\n\n".join([str(msg) for msg in messages]) |
|
|
return str_context |
|
|
|
|
|
def execute(self, llm: Optional[BaseLLM] = None, action: Action = None, context: List[Message] = None, **kwargs) -> Union[dict, None]: |
|
|
"""Extract structured inputs for an action from conversation context. |
|
|
|
|
|
This method uses the LLM to analyze the conversation context and extract |
|
|
information that matches the input requirements of the target action. |
|
|
|
|
|
Args: |
|
|
llm: The language model to use for extraction. |
|
|
action: The target action whose input requirements (`inputs_format`) define what to extract. |
|
|
context: List of messages providing the conversation context. |
|
|
**kwargs: Additional keyword arguments. |
|
|
|
|
|
Returns: |
|
|
A dictionary containing the extracted inputs for the target action, |
|
|
or None if extraction is not possible (e.g., if the action doesn't |
|
|
require inputs or if context is missing). |
|
|
""" |
|
|
if action is None or context is None: |
|
|
return None |
|
|
|
|
|
action_inputs_cls: Type[ActionInput] = action.inputs_format |
|
|
if action_inputs_cls is None: |
|
|
|
|
|
return None |
|
|
|
|
|
action_inputs_desc = action_inputs_cls.get_input_specification() |
|
|
str_context = self.get_context_from_messages(messages=context) |
|
|
|
|
|
if not action_inputs_desc or not str_context: |
|
|
return None |
|
|
|
|
|
prompt = CONTEXT_EXTRACTION["prompt"].format( |
|
|
context=str_context, |
|
|
action_name=action.name, |
|
|
action_description=action.description, |
|
|
action_inputs=action_inputs_desc |
|
|
) |
|
|
|
|
|
action_inputs = llm.generate( |
|
|
prompt=prompt, |
|
|
system_message=CONTEXT_EXTRACTION["system_prompt"], |
|
|
parser=action_inputs_cls |
|
|
) |
|
|
action_inputs_data = action_inputs.get_structured_data() |
|
|
|
|
|
return action_inputs_data |