| import re |
| from pydantic import Field, model_validator |
| from typing import Optional, List |
|
|
| from ..core.logging import logger |
| from ..core.module import BaseModule |
| from ..core.base_config import Parameter |
| from ..models.base_model import BaseLLM |
| from .action import Action, ActionInput, ActionOutput |
| from ..prompts.agent_generator import AGENT_GENERATION_ACTION |
| from ..prompts.tool_calling import AGENT_GENERATION_TOOLS_PROMPT |
| from ..utils.utils import normalize_text |
|
|
| class AgentGenerationInput(ActionInput): |
| """ |
| Input specification for the agent generation action. |
| """ |
|
|
| goal: str = Field(description="A detailed statement of the workflow's goal, explaining the objectives the entire workflow aims to achieve") |
| workflow: str = Field(description="An overview of the entire workflow, detailing all sub-tasks with their respective names, descriptions, inputs, and outputs") |
| task: str = Field(description="A detailed JSON representation of the sub-task requiring agent generation. It should include the task's name, description, inputs, and outputs.") |
|
|
| history: Optional[str] = Field(default=None, description="Optional field containing previously selected or generated agents.") |
| suggestion: Optional[str] = Field(default=None, description="Optional suggestions to refine the generated agents.") |
| existing_agents: Optional[str] = Field(default=None, description="Optional field containing the description of predefined agents, including each agent's name, role, and available actions.") |
| tools: Optional[str] = Field(default=None, description="Optional field containing the description of tools that agents can use, including each tool's name and functionality.") |
|
|
|
|
| class GeneratedAgent(BaseModule): |
| """ |
| Representation of a generated agent with validation capabilities. |
| """ |
|
|
| name: str |
| description: str |
| inputs: List[Parameter] |
| outputs: List[Parameter] |
| prompt: str |
| tool_names: Optional[List[str]] = None |
|
|
| @classmethod |
| def find_output_name(cls, text: str, outputs: List[str]): |
| def sim(t1: str, t2: str): |
| t1_words = normalize_text(t1).split() |
| t2_words = normalize_text(t2).split() |
| return len(set(t1_words)&set(t2_words)) |
| |
| similarities = [sim(text, output) for output in outputs] |
| max_sim = max(similarities) |
| return outputs[similarities.index(max_sim)] |
|
|
| @model_validator(mode="after") |
| @classmethod |
| def validate_prompt(cls, agent: 'GeneratedAgent'): |
| """Validate and fix the agent's prompt template. |
| |
| This validator ensures that: |
| 1. All input parameters are properly referenced in the prompt |
| 2. Input references use the correct format with braces |
| 3. All output sections match the defined output parameters |
| |
| If there are mismatches in the output sections, it attempts to |
| fix them by finding the most similar output name. |
| |
| Args: |
| agent: The GeneratedAgent instance to validate. |
| |
| Returns: |
| The validated and potentially modified GeneratedAgent. |
| |
| Raises: |
| ValueError: If inputs are missing from the prompt or output sections don't match the defined outputs. |
| """ |
| |
| input_names = [inp.name for inp in agent.inputs] |
| prompt_has_inputs = [name in agent.prompt for name in input_names] |
| if not all(prompt_has_inputs): |
| missing_input_names = [name for name, has_input in zip(input_names, prompt_has_inputs) if not has_input] |
| raise ValueError(f'The prompt miss inputs: {missing_input_names}') |
| |
| |
| pattern = r"### Instructions(.*?)### Output Format" |
| prompt = agent.prompt |
|
|
| def replace_with_braces(match): |
| instructions = match.group(1) |
| for name in input_names: |
| instructions = re.sub(fr'<input>{{*\b{re.escape(name)}\b}}*</input>', fr'<input>{{{name}}}</input>', instructions) |
| return "### Instructions" + instructions + "### Output Format" |
| |
| modified_prompt = re.sub(pattern, replace_with_braces, prompt, flags=re.DOTALL) |
| agent.prompt = modified_prompt |
|
|
| |
| prompt = agent.prompt |
| pattern = r"### Output Format(.*)" |
| outputs_names = [out.name for out in agent.outputs] |
|
|
| def fix_output_names(match): |
| output_format = match.group(1) |
| matches = re.findall(r"## ([^\n#]+)", output_format, flags=re.DOTALL) |
| generated_outputs = [m.strip() for m in matches if m.strip() != "Thought"] |
| |
| if len(generated_outputs) != len(outputs_names): |
| raise ValueError(f"The number of outputs in the prompt is different from that defined in the `outputs` field of the agent. The outputs in the prompt are: {generated_outputs}, while the outputs from the agent's `outputs` field are: {outputs_names}") |
| |
| for generated_output in generated_outputs: |
| if generated_output not in outputs_names: |
| most_similar_output_name = cls.find_output_name(text=generated_output, outputs=outputs_names) |
| output_format = output_format.replace(generated_output, most_similar_output_name) |
| logger.warning(f"Couldn't find output name in prompt ('{generated_output}') in agent's outputs. Replace it with the most similar agent output: '{most_similar_output_name}'") |
| return "### Output Format" + output_format |
| |
| modified_prompt = re.sub(pattern, fix_output_names, prompt, flags=re.DOTALL) |
| agent.prompt = modified_prompt |
|
|
| return agent |
|
|
|
|
| class AgentGenerationOutput(ActionOutput): |
|
|
| selected_agents: List[str] = Field(description="A list of selected agent's names") |
| generated_agents: List[GeneratedAgent] = Field(description="A list of generated agetns to address a sub-task") |
| |
|
|
| class AgentGeneration(Action): |
| """ |
| Action for generating agent specifications for workflow tasks. |
| |
| This action analyzes task requirements and generates appropriate agent |
| specifications, including their prompts, inputs, and outputs. It can either |
| select from existing agents or create new ones tailored to the task. |
| """ |
|
|
| def __init__(self, **kwargs): |
| name = kwargs.pop("name") if "name" in kwargs else AGENT_GENERATION_ACTION["name"] |
| description = kwargs.pop("description") if "description" in kwargs else AGENT_GENERATION_ACTION["description"] |
| prompt = kwargs.pop("prompt") if "prompt" in kwargs else AGENT_GENERATION_ACTION["prompt"] |
| |
| |
| inputs_format = kwargs.pop("inputs_format", None) or AgentGenerationInput |
| outputs_format = kwargs.pop("outputs_format", None) or AgentGenerationOutput |
| tools = kwargs.pop("tools", None) |
| super().__init__(name=name, description=description, prompt=prompt, inputs_format=inputs_format, outputs_format=outputs_format, **kwargs) |
| self.tools = tools |
| |
| def execute(self, llm: Optional[BaseLLM] = None, inputs: Optional[dict] = None, sys_msg: Optional[str]=None, return_prompt: bool = False, **kwargs) -> AgentGenerationOutput: |
| """Execute the agent generation process. |
| |
| This method uses the provided language model to generate agent specifications |
| based on the workflow context and task requirements. |
| |
| Args: |
| llm: The language model to use for generation. |
| inputs: Input data containing workflow and task information. |
| sys_msg: Optional system message for the language model. |
| return_prompt: Whether to return both the generated agents and the prompt used. |
| **kwargs: Additional keyword arguments. |
| |
| Returns: |
| If return_prompt is False (default): The generated agents output. |
| If return_prompt is True: A tuple of (generated agents, prompt used). |
| |
| Raises: |
| ValueError: If the inputs are None or empty. |
| """ |
| if not inputs: |
| logger.error("AgentGeneration action received invalid `inputs`: None or empty.") |
| raise ValueError('The `inputs` to AgentGeneration action is None or empty.') |
| |
| inputs_format: AgentGenerationInput = self.inputs_format |
| outputs_format: AgentGenerationOutput = self.outputs_format |
|
|
| prompt_params_names = inputs_format.get_attrs() |
| prompt_params_values = {param: inputs.get(param, "") for param in prompt_params_names} |
| if self.tools: |
| tool_description = [ |
| { |
| tool.name: [ |
| s["function"]["description"] for s in tool.get_tool_schemas() |
| ], |
| } |
| for tool in self.tools |
| ] |
| prompt_params_values["tools"] = AGENT_GENERATION_TOOLS_PROMPT.format(tools_description=tool_description) |
| prompt = self.prompt.format(**prompt_params_values) |
| agents = llm.generate( |
| prompt = prompt, |
| system_message = sys_msg, |
| parser=outputs_format, |
| parse_mode="json" |
| ) |
| |
| if return_prompt: |
| return agents, prompt |
| |
| return agents |
| |