From 50cc085cb822f804c4fe356ae919112b0697bd1c Mon Sep 17 00:00:00 2001 From: Thiago Castro Ferreira Date: Fri, 28 Feb 2025 12:04:22 -0300 Subject: [PATCH 01/22] Introduction to evolver --- aixplain/modules/team_agent/__init__.py | 83 +++++++++---------------- 1 file changed, 30 insertions(+), 53 deletions(-) diff --git a/aixplain/modules/team_agent/__init__.py b/aixplain/modules/team_agent/__init__.py index 825967e8..57787f10 100644 --- a/aixplain/modules/team_agent/__init__.py +++ b/aixplain/modules/team_agent/__init__.py @@ -123,6 +123,7 @@ def run( max_tokens: int = 2048, max_iterations: int = 30, output_format: OutputFormat = OutputFormat.TEXT, + evolve: bool = False, ) -> Dict: """Runs a team agent call. @@ -139,6 +140,7 @@ def run( max_tokens (int, optional): maximum number of tokens which can be generated by the agents. Defaults to 2048. max_iterations (int, optional): maximum number of iterations between the agents. Defaults to 30. output_format (ResponseFormat, optional): response format. Defaults to TEXT. + evolve (bool, optional): evolve the team agent. Defaults to False. Returns: Dict: parsed output from model """ @@ -155,6 +157,7 @@ def run( max_tokens=max_tokens, max_iterations=max_iterations, output_format=output_format, + evolve=evolve, ) if response["status"] == "FAILED": end = time.time() @@ -162,9 +165,7 @@ def run( return response poll_url = response["url"] end = time.time() - response = self.sync_poll( - poll_url, name=name, timeout=timeout, wait_time=wait_time - ) + response = self.sync_poll(poll_url, name=name, timeout=timeout, wait_time=wait_time) return response except Exception as e: logging.error(f"Team Agent Run: Error in running for {name}: {e}") @@ -187,6 +188,7 @@ def run_async( max_tokens: int = 2048, max_iterations: int = 30, output_format: OutputFormat = OutputFormat.TEXT, + evolve: bool = False, ) -> Dict: """Runs asynchronously a Team Agent call. @@ -201,24 +203,19 @@ def run_async( max_tokens (int, optional): maximum number of tokens which can be generated by the agents. Defaults to 2048. max_iterations (int, optional): maximum number of iterations between the agents. Defaults to 30. output_format (ResponseFormat, optional): response format. Defaults to TEXT. + evolve (bool, optional): evolve the team agent. Defaults to False. Returns: dict: polling URL in response """ from aixplain.factories.file_factory import FileFactory if not self.is_valid: - raise Exception( - "Team Agent is not valid. Please validate the team agent before running." - ) + raise Exception("Team Agent is not valid. Please validate the team agent before running.") - assert ( - data is not None or query is not None - ), "Either 'data' or 'query' must be provided." + assert data is not None or query is not None, "Either 'data' or 'query' must be provided." if data is not None: if isinstance(data, dict): - assert ( - "query" in data and data["query"] is not None - ), "When providing a dictionary, 'query' must be provided." + assert "query" in data and data["query"] is not None, "When providing a dictionary, 'query' must be provided." if session_id is None: session_id = data.pop("session_id", None) if history is None: @@ -232,8 +229,7 @@ def run_async( # process content inputs if content is not None: assert ( - isinstance(query, str) - and FileFactory.check_storage_type(query) == StorageType.TEXT + isinstance(query, str) and FileFactory.check_storage_type(query) == StorageType.TEXT ), "When providing 'content', query must be text." if isinstance(content, list): @@ -243,9 +239,7 @@ def run_async( query += f"\n{input_link}" elif isinstance(content, dict): for key, value in content.items(): - assert ( - "{{" + key + "}}" in query - ), f"Key '{key}' not found in query." + assert "{{" + key + "}}" in query, f"Key '{key}' not found in query." value = FileFactory.to_link(value) query = query.replace("{{" + key + "}}", f"'{value}'") @@ -260,26 +254,17 @@ def run_async( "sessionId": session_id, "history": history, "executionParams": { - "maxTokens": ( - parameters["max_tokens"] - if "max_tokens" in parameters - else max_tokens - ), - "maxIterations": ( - parameters["max_iterations"] - if "max_iterations" in parameters - else max_iterations - ), + "maxTokens": (parameters["max_tokens"] if "max_tokens" in parameters else max_tokens), + "maxIterations": (parameters["max_iterations"] if "max_iterations" in parameters else max_iterations), "outputFormat": output_format.value, }, + "evolve": evolve, } payload.update(parameters) payload = json.dumps(payload) r = _request_with_retry("post", self.url, headers=headers, data=payload) - logging.info( - f"Team Agent Run Async: Start service for {name} - {self.url} - {payload} - {headers}" - ) + logging.info(f"Team Agent Run Async: Start service for {name} - {self.url} - {payload} - {headers}") resp = None try: @@ -309,7 +294,9 @@ def delete(self) -> None: if r.status_code != 200: raise Exception() except Exception: - message = f"Team Agent Deletion Error (HTTP {r.status_code}): Make sure the Team Agent exists and you are the owner." + message = ( + f"Team Agent Deletion Error (HTTP {r.status_code}): Make sure the Team Agent exists and you are the owner." + ) logging.error(message) raise Exception(f"{message}") @@ -318,8 +305,7 @@ def to_dict(self) -> Dict: "id": self.id, "name": self.name, "agents": [ - {"assetId": agent.id, "number": idx, "type": "AGENT", "label": "AGENT"} - for idx, agent in enumerate(self.agents) + {"assetId": agent.id, "number": idx, "type": "AGENT", "label": "AGENT"} for idx, agent in enumerate(self.agents) ], "links": [], "description": self.description, @@ -343,9 +329,7 @@ def _validate(self) -> None: try: llm = ModelFactory.get(self.llm_id) - assert ( - llm.function == Function.TEXT_GENERATION - ), "Large Language Model must be a text generation model." + assert llm.function == Function.TEXT_GENERATION, "Large Language Model must be a text generation model." except Exception: raise Exception(f"Large Language Model with ID '{self.llm_id}' not found.") @@ -362,9 +346,7 @@ def validate(self, raise_exception: bool = False) -> bool: raise e else: logging.warning(f"Team Agent Validation Error: {e}") - logging.warning( - "You won't be able to run the Team Agent until the issues are handled manually." - ) + logging.warning("You won't be able to run the Team Agent until the issues are handled manually.") return self.is_valid @@ -377,8 +359,7 @@ def update(self) -> None: stack = inspect.stack() if len(stack) > 2 and stack[1].function != "save": warnings.warn( - "update() is deprecated and will be removed in a future version. " - "Please use save() instead.", + "update() is deprecated and will be removed in a future version. " "Please use save() instead.", DeprecationWarning, stacklevel=2, ) @@ -390,17 +371,13 @@ def update(self) -> None: payload = self.to_dict() - logging.debug( - f"Start service for PUT Update Team Agent - {url} - {headers} - {json.dumps(payload)}" - ) + logging.debug(f"Start service for PUT Update Team Agent - {url} - {headers} - {json.dumps(payload)}") resp = "No specified error." try: r = _request_with_retry("put", url, headers=headers, json=payload) resp = r.json() except Exception: - raise Exception( - "Team Agent Update Error: Please contact the administrators." - ) + raise Exception("Team Agent Update Error: Please contact the administrators.") if 200 <= r.status_code < 300: return build_team_agent(resp) @@ -414,11 +391,11 @@ def save(self) -> None: def deploy(self) -> None: """Deploy the Team Agent.""" - assert ( - self.status == AssetStatus.DRAFT - ), "Team Agent Deployment Error: Team Agent must be in draft status." - assert ( - self.status != AssetStatus.ONBOARDED - ), "Team Agent Deployment Error: Team Agent must be onboarded." + assert self.status == AssetStatus.DRAFT, "Team Agent Deployment Error: Team Agent must be in draft status." + assert self.status != AssetStatus.ONBOARDED, "Team Agent Deployment Error: Team Agent must be onboarded." self.status = AssetStatus.ONBOARDED self.update() + + def evolve(self, query: Text) -> None: + """Evolve the Team Agent.""" + return self.run_async(query=query, evolve=True) From 819a1d0a14536fe248b1c0da6d364a1d999c5670 Mon Sep 17 00:00:00 2001 From: Thiago Castro Ferreira Date: Tue, 18 Mar 2025 10:38:43 -0300 Subject: [PATCH 02/22] Build from yaml --- .../factories/team_agent_factory/utils.py | 106 +++++++++++++++++- 1 file changed, 105 insertions(+), 1 deletion(-) diff --git a/aixplain/factories/team_agent_factory/utils.py b/aixplain/factories/team_agent_factory/utils.py index 5e865cd0..e9de979e 100644 --- a/aixplain/factories/team_agent_factory/utils.py +++ b/aixplain/factories/team_agent_factory/utils.py @@ -4,8 +4,10 @@ import aixplain.utils.config as config from aixplain.enums.asset_status import AssetStatus from aixplain.modules.agent import Agent +from aixplain.modules.agent.agent_task import AgentTask +from aixplain.modules.agent.tool.model_tool import ModelTool from aixplain.modules.team_agent import TeamAgent -from typing import Dict, Text, List +from typing import Dict, Text, List, Optional from urllib.parse import urljoin GPT_4o_ID = "6646261c6eb563165658bbb1" @@ -61,3 +63,105 @@ def build_team_agent(payload: Dict, agents: List[Agent] = None, api_key: Text = else: raise Exception(f"Team Agent Creation Error: Task dependency not found - {dependency}") return team_agent + + +def parse_tool_from_yaml(tool: str) -> ModelTool: + from aixplain.enums import Function + + if tool.strip() == "translation": + return ModelTool( + function=Function.TRANSLATION, + ) + elif tool.strip() == "speech-recognition": + return ModelTool( + function=Function.SPEECH_RECOGNITION, + ) + elif tool.strip() == "text-to-speech": + return ModelTool( + function=Function.SPEECH_SYNTHESIS, + ) + elif tool.strip() == "serper_search": + return ModelTool(model="65c51c556eb563350f6e1bb1") + elif tool.strip() == "website_search": + return ModelTool(model="6736411cf127849667606689") + elif tool.strip() == "website_scrape": + return ModelTool(model="6748e4746eb5633559668a15") + else: + raise Exception(f"Tool {tool} in yaml not found.") + + +def build_team_agent_from_yaml(yaml_code: str, llm_id: str, api_key: str, team_id: Optional[str] = None) -> TeamAgent: + import yaml + from aixplain.factories import AgentFactory, TeamAgentFactory + + team_config = yaml.safe_load(yaml_code) + + agents_data = team_config["agents"] + tasks_data = team_config.get("tasks", []) + system_data = team_config["system"] if "system" in team_config else {"query": "", "name": "Test Team"} + team_name = system_data["name"] + + # Create agent mapping by name for easier task assignment + agents_mapping = {} + agent_objs = [] + + # Parse agents + for agent_entry in agents_data: + for agent_name, agent_info in agent_entry.items(): + agent_role = agent_info["role"] + agent_goal = agent_info["goal"] + agent_backstory = agent_info["backstory"] + + description = f"## ROLE\n{agent_role}\n\n## GOAL\n{agent_goal}\n\n## BACKSTORY\n{agent_backstory}" + agent_obj = Agent( + id="", + name=agent_name.replace("_", " "), + description=description, + instructions=description, + tasks=[], # Tasks will be assigned later + tools=[parse_tool_from_yaml(tool) for tool in agent_info.get("tools", []) if tool != "language_model"], + llmId=llm_id, + ) + agents_mapping[agent_name] = agent_obj + agent_objs.append(agent_obj) + + # Parse tasks and assign them to the corresponding agents + for task in tasks_data: + for task_name, task_info in task.items(): + description = task_info["description"] + expected_output = task_info["expected_output"] + dependencies = task_info.get("dependencies", []) + agent_name = task_info["agent"] + + task_obj = AgentTask( + name=task_name, + description=description, + expected_output=expected_output, + dependencies=dependencies, + ) + + # Assign the task to the corresponding agent + if agent_name in agents_mapping: + agent = agents_mapping[agent_name] + agent.tasks.append(task_obj) + else: + raise Exception(f"Agent '{agent_name}' referenced in tasks not found.") + + for i, agent in enumerate(agent_objs): + agent_objs[i] = AgentFactory.create( + name=agent.name, + description=agent.instructions, + instructions=agent.instructions, + tools=agent.tools, + llm_id=llm_id, + tasks=agent.tasks, + ) + + return TeamAgentFactory.create( + name=team_name, + agents=agent_objs, + llm_id=llm_id, + api_key=api_key, + use_mentalist=True, + use_inspector=False, + ) From 94603566e83fb053f4a039133c4409e42e16e204 Mon Sep 17 00:00:00 2001 From: Thiago Castro Ferreira Date: Wed, 19 Mar 2025 12:00:02 -0300 Subject: [PATCH 03/22] Addin website-crawl --- aixplain/factories/team_agent_factory/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/aixplain/factories/team_agent_factory/utils.py b/aixplain/factories/team_agent_factory/utils.py index e9de979e..576e5763 100644 --- a/aixplain/factories/team_agent_factory/utils.py +++ b/aixplain/factories/team_agent_factory/utils.py @@ -86,6 +86,8 @@ def parse_tool_from_yaml(tool: str) -> ModelTool: return ModelTool(model="6736411cf127849667606689") elif tool.strip() == "website_scrape": return ModelTool(model="6748e4746eb5633559668a15") + elif tool.strip() == "website_crawl": + return ModelTool(model="6748d4cff12784b6014324e2") else: raise Exception(f"Tool {tool} in yaml not found.") @@ -112,7 +114,7 @@ def build_team_agent_from_yaml(yaml_code: str, llm_id: str, api_key: str, team_i agent_goal = agent_info["goal"] agent_backstory = agent_info["backstory"] - description = f"## ROLE\n{agent_role}\n\n## GOAL\n{agent_goal}\n\n## BACKSTORY\n{agent_backstory}" + description = f"You are an expert {agent_role}. {agent_backstory} Your primary goal is to {agent_goal}. Use your expertise to ensure the success of your tasks." agent_obj = Agent( id="", name=agent_name.replace("_", " "), From 94923d3e6542830071bf13044e2505ee79a8edd0 Mon Sep 17 00:00:00 2001 From: Thiago Castro Ferreira Date: Mon, 24 Mar 2025 18:26:09 -0300 Subject: [PATCH 04/22] Fix syntax of metadata --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 5792718a..337e05e4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,3 @@ [metadata] -description-file=README.md +description_file=README.md license_files=LICENSE.rst From 4e88aebeea66e4cfa453d93667bc7e556ef136c5 Mon Sep 17 00:00:00 2001 From: Cynthia Date: Fri, 28 Mar 2025 14:12:16 -0400 Subject: [PATCH 05/22] Objectify Evolver Response --- .../factories/team_agent_factory/utils.py | 8 +- aixplain/modules/team_agent/__init__.py | 99 +++++++++++++++++-- .../modules/team_agent/evolver_response.py | 57 +++++++++++ .../team_agent/evolver_response_data.py | 70 +++++++++++++ 4 files changed, 227 insertions(+), 7 deletions(-) create mode 100644 aixplain/modules/team_agent/evolver_response.py create mode 100644 aixplain/modules/team_agent/evolver_response_data.py diff --git a/aixplain/factories/team_agent_factory/utils.py b/aixplain/factories/team_agent_factory/utils.py index f8f1f048..0b713e01 100644 --- a/aixplain/factories/team_agent_factory/utils.py +++ b/aixplain/factories/team_agent_factory/utils.py @@ -88,6 +88,8 @@ def parse_tool_from_yaml(tool: str) -> ModelTool: return ModelTool( function=Function.SPEECH_SYNTHESIS, ) + elif tool.strip() == "llm": + return ModelTool(function=Function.TEXT_GENERATION) elif tool.strip() == "serper_search": return ModelTool(model="65c51c556eb563350f6e1bb1") elif tool.strip() == "website_search": @@ -123,9 +125,11 @@ def build_team_agent_from_yaml(yaml_code: str, llm_id: str, api_key: str, team_i agent_backstory = agent_info["backstory"] description = f"You are an expert {agent_role}. {agent_backstory} Your primary goal is to {agent_goal}. Use your expertise to ensure the success of your tasks." + agent_name = agent_name.replace("_", " ") + agent_name = f"{agent_name} agent" if not agent_name.endswith(" agent") else agent_name agent_obj = Agent( id="", - name=agent_name.replace("_", " "), + name=agent_name, description=description, instructions=description, tasks=[], # Tasks will be assigned later @@ -142,6 +146,8 @@ def build_team_agent_from_yaml(yaml_code: str, llm_id: str, api_key: str, team_i expected_output = task_info["expected_output"] dependencies = task_info.get("dependencies", []) agent_name = task_info["agent"] + agent_name = agent_name.replace("_", " ") + agent_name = f"{agent_name} agent" if not agent_name.endswith(" agent") else agent_name task_obj = AgentTask( name=task_name, diff --git a/aixplain/modules/team_agent/__init__.py b/aixplain/modules/team_agent/__init__.py index 49411b94..c103034f 100644 --- a/aixplain/modules/team_agent/__init__.py +++ b/aixplain/modules/team_agent/__init__.py @@ -38,7 +38,10 @@ from aixplain.modules.model import Model from aixplain.modules.agent import Agent, OutputFormat from aixplain.modules.agent.agent_response import AgentResponse +from aixplain.modules.agent.agent_response_data import AgentResponseData from aixplain.modules.agent.utils import process_variables +from aixplain.modules.team_agent.evolver_response_data import EvolverResponseData +from aixplain.modules.team_agent.evolver_response import EvolverResponse from aixplain.utils import config from aixplain.utils.file_utils import _request_with_retry @@ -108,6 +111,7 @@ def __init__( self.additional_info = additional_info self.agents = agents self.llm_id = llm_id + self.api_key = api_key self.use_mentalist = use_mentalist self.use_inspector = use_inspector self.max_inspectors = max_inspectors @@ -171,14 +175,27 @@ def run( output_format=output_format, evolve=evolve, ) - if response["status"] == "FAILED": + if response["status"] == ResponseStatus.FAILED: end = time.time() response["elapsed_time"] = end - start return response poll_url = response["url"] end = time.time() - response = self.sync_poll(poll_url, name=name, timeout=timeout, wait_time=wait_time) - return response + result = self.sync_poll(poll_url, name=name, timeout=timeout, wait_time=wait_time) + result_data = result.data + return AgentResponse( + status=ResponseStatus.SUCCESS, + completed=True, + data=AgentResponseData( + input=result_data.get("input"), + output=result_data.get("output"), + session_id=result_data.get("session_id"), + intermediate_steps=result_data.get("intermediate_steps"), + execution_stats=result_data.get("executionStats"), + ), + used_credits=result_data.get("usedCredits", 0.0), + run_time=result_data.get("runTime", end - start), + ) except Exception as e: logging.error(f"Team Agent Run: Error in running for {name}: {e}") end = time.time() @@ -284,14 +301,84 @@ def run_async( logging.info(f"Result of request for {name} - {r.status_code} - {resp}") poll_url = resp["data"] - response = {"status": "IN_PROGRESS", "url": poll_url} + response = AgentResponse( + status=ResponseStatus.IN_PROGRESS, + url=poll_url, + data=AgentResponseData(input=input_data), + run_time=0.0, + used_credits=0.0, + ) + if evolve: + response = EvolverResponse( + status=ResponseStatus.IN_PROGRESS, + url=poll_url, + data=EvolverResponseData( + evolved_agent="", + current_code="", + evaluation_report="", + comparison_report="", + criteria="", + archive="",), + run_time=0.0, + used_credits=0.0, + ) except Exception: - response = {"status": "FAILED"} msg = f"Error in request for {name} - {traceback.format_exc()}" logging.error(f"Team Agent Run Async: Error in running for {name}: {resp}") if resp is not None: - response["error"] = msg + response = AgentResponse( + status=ResponseStatus.FAILED, + error=msg, + ) + if evolve: + response = EvolverResponse( + status=ResponseStatus.FAILED, + error=msg, + ) return response + + + def poll(self, poll_url: Text, name: Text = "model_process") -> EvolverResponse: + headers = {"x-api-key": self.api_key, "Content-Type": "application/json"} + r = _request_with_retry("get", poll_url, headers=headers) + try: + resp = r.json() + evolver_data = None + + if resp["completed"] is True: + status = ResponseStatus.SUCCESS + resp_data = resp.get("data", {}) + evolver_data = EvolverResponseData.from_dict(resp_data, llm_id=self.llm_id, api_key=self.api_key) + if "error_message" in resp or "supplierError" in resp: + status = ResponseStatus.FAILED + else: + status = ResponseStatus.IN_PROGRESS + response = f"EvolverResponse(status={status}, completed={resp["completed"]})" + logging.debug(f"Single Poll for Model: Status of polling for {name}: {resp}") + + response = EvolverResponse( + status=status, + data=evolver_data or EvolverResponseData( + evolved_agent="", + current_code="", + evaluation_report="", + comparison_report="", + criteria="", + archive="",), + details=resp.get("details", {}), + completed=resp.get("completed", False), + error_message=resp.get("error_message", ""), + used_credits=resp.get("usedCredits", 0), + run_time=resp.get("runTime", 0), + usage=resp.get("usage", None), + ) + + except Exception as e: + logging.error(f"Single Poll for Model: Error of polling for {name}: {e}") + response = f"EvolverResponse(status={ResponseStatus.FAILED}, error_message={str(e)}, completed=False,)" + + return response + def delete(self) -> None: """Delete Corpus service""" diff --git a/aixplain/modules/team_agent/evolver_response.py b/aixplain/modules/team_agent/evolver_response.py new file mode 100644 index 00000000..c2f6fb2b --- /dev/null +++ b/aixplain/modules/team_agent/evolver_response.py @@ -0,0 +1,57 @@ +from typing import Any, Optional, Text, Dict, List, Union +from aixplain.enums import ResponseStatus +from aixplain.modules.model.response import ModelResponse +from aixplain.modules.team_agent.evolver_response_data import EvolverResponseData + + +class EvolverResponse(ModelResponse): + + def __init__( + self, + status: ResponseStatus = ResponseStatus.FAILED, + data: Optional[EvolverResponseData] = None, + details: Optional[Union[Dict, List]] = {}, + completed: bool = False, + error_message: Text = "", + used_credits: float = 0.0, + run_time: float = 0.0, + usage: Optional[Dict] = None, + url: Optional[Text] = None, + **kwargs, + ): + + super().__init__( + status=status, + data="", + details=details, + completed=completed, + error_message=error_message, + used_credits=used_credits, + run_time=run_time, + usage=usage, + url=url, + **kwargs, + ) + self.data = data or EvolverResponseData() + + def __getitem__(self, key: Text) -> Any: + if key == "data": + return self.data.to_dict() + return super().__getitem__(key) + + def __setitem__(self, key: Text, value: Any) -> None: + if key == "data" and isinstance(value, Dict): + self.data = EvolverResponseData.from_dict(value) + elif key == "data" and isinstance(value, EvolverResponseData): + self.data = value + else: + super().__setitem__(key, value) + + def to_dict(self) -> Dict[Text, Any]: + base_dict = super().to_dict() + base_dict["data"] = self.data.to_dict() + return base_dict + + def __repr__(self) -> str: + fields = super().__repr__()[len("ModelResponse(") : -1] + return f"EvolverResponse({fields})" \ No newline at end of file diff --git a/aixplain/modules/team_agent/evolver_response_data.py b/aixplain/modules/team_agent/evolver_response_data.py new file mode 100644 index 00000000..793c5289 --- /dev/null +++ b/aixplain/modules/team_agent/evolver_response_data.py @@ -0,0 +1,70 @@ +from typing import Any, Dict, List, Text, TYPE_CHECKING + +if TYPE_CHECKING: + from aixplain.modules.team_agent import TeamAgent + +class EvolverResponseData: + def __init__( + self, + evolved_agent: 'TeamAgent', + current_code: Text, + evaluation_report: Text, + comparison_report: Text, + criteria: Text, + archive: List[Text], + ) -> None: + self.evolved_agent = evolved_agent + self.current_code = current_code + self.evaluation_report = evaluation_report + self.comparison_report = comparison_report + self.criteria = criteria + self.archive = archive + + @classmethod + def from_dict(cls, data: Dict[str, Any], llm_id: Text, api_key: Text) -> "EvolverResponseData": + from aixplain.factories.team_agent_factory.utils import build_team_agent_from_yaml + + yaml_code = data.get("current_code", "") + evolved_team_agent = build_team_agent_from_yaml( + yaml_code=yaml_code, + llm_id=llm_id, + api_key=api_key + ) + + return cls( + evolved_agent=evolved_team_agent, + current_code=yaml_code, + evaluation_report=data.get("evaluation_report", ""), + comparison_report=data.get("comparison_report", ""), + criteria=data.get("criteria", ""), + archive=data.get("archive", []), + ) + + def to_dict(self) -> Dict[str, Any]: + return { + "evolved_agent": self.evolved_agent, + "current_code": self.current_code, + "evaluation_report": self.evaluation_report, + "comparison_report": self.comparison_report, + "criteria": self.criteria, + "archive": self.archive, + } + + def __getitem__(self, key: str) -> Any: + return getattr(self, key, None) + + def __setitem__(self, key: str, value: Any) -> None: + if hasattr(self, key): + setattr(self, key, value) + else: + raise KeyError(f"{key} is not a valid attribute of {self.__class__.__name__}") + + def __repr__(self) -> str: + return ( + f"{self.__class__.__name__}(" + f"evolved_agent='{self.evolved_agent}', " + f"evaluation_report='{self.evaluation_report}', " + f"comparison_report='{self.comparison_report}', " + f"criteria='{self.criteria}', " + f"archive='{self.archive}')" + ) \ No newline at end of file From 9ce6e36c739f280c2efca217361dae643e69574a Mon Sep 17 00:00:00 2001 From: Cynthia Date: Fri, 28 Mar 2025 18:01:27 -0400 Subject: [PATCH 06/22] debug agent response on poll --- .../factories/team_agent_factory/utils.py | 3 +- aixplain/modules/team_agent/__init__.py | 58 ++++++++++++------- 2 files changed, 38 insertions(+), 23 deletions(-) diff --git a/aixplain/factories/team_agent_factory/utils.py b/aixplain/factories/team_agent_factory/utils.py index 0b713e01..7dc7610d 100644 --- a/aixplain/factories/team_agent_factory/utils.py +++ b/aixplain/factories/team_agent_factory/utils.py @@ -69,7 +69,8 @@ def build_team_agent(payload: Dict, agents: List[Agent] = None, api_key: Text = if task_dependency: team_agent.agents[idx].tasks[i].dependencies[j] = task_dependency else: - raise Exception(f"Team Agent Creation Error: Task dependency not found - {dependency}") + team_agent.agents[idx].tasks[i].dependencies[j] = None + return team_agent diff --git a/aixplain/modules/team_agent/__init__.py b/aixplain/modules/team_agent/__init__.py index c103034f..9ff7e9c7 100644 --- a/aixplain/modules/team_agent/__init__.py +++ b/aixplain/modules/team_agent/__init__.py @@ -338,45 +338,59 @@ def run_async( return response - def poll(self, poll_url: Text, name: Text = "model_process") -> EvolverResponse: + def poll(self, poll_url: Text, name: Text = "model_process", evolve: bool = False) -> EvolverResponse: headers = {"x-api-key": self.api_key, "Content-Type": "application/json"} r = _request_with_retry("get", poll_url, headers=headers) try: resp = r.json() evolver_data = None - + resp_data = resp.get("data", {}) if resp["completed"] is True: status = ResponseStatus.SUCCESS - resp_data = resp.get("data", {}) - evolver_data = EvolverResponseData.from_dict(resp_data, llm_id=self.llm_id, api_key=self.api_key) if "error_message" in resp or "supplierError" in resp: status = ResponseStatus.FAILED else: status = ResponseStatus.IN_PROGRESS - response = f"EvolverResponse(status={status}, completed={resp["completed"]})" + response = f"EvolverResponse(status={status}, completed={resp['completed']})" logging.debug(f"Single Poll for Model: Status of polling for {name}: {resp}") - response = EvolverResponse( - status=status, - data=evolver_data or EvolverResponseData( - evolved_agent="", - current_code="", - evaluation_report="", - comparison_report="", - criteria="", - archive="",), - details=resp.get("details", {}), - completed=resp.get("completed", False), - error_message=resp.get("error_message", ""), - used_credits=resp.get("usedCredits", 0), - run_time=resp.get("runTime", 0), - usage=resp.get("usage", None), - ) + if evolve: + if status == ResponseStatus.SUCCESS: + evolver_data = EvolverResponseData.from_dict(resp_data, llm_id=self.llm_id, api_key=self.api_key) + + response = EvolverResponse( + status=status, + data=evolver_data or EvolverResponseData( + evolved_agent="", + current_code="", + evaluation_report="", + comparison_report="", + criteria="", + archive="",), + details=resp.get("details", {}), + completed=resp.get("completed", False), + error_message=resp.get("error_message", ""), + used_credits=resp.get("usedCredits", 0), + run_time=resp.get("runTime", 0), + usage=resp.get("usage", None), + ) + response = AgentResponse( + status=status, + data=resp.get("data", {}), + details=resp.get("details", {}), + completed=resp.get("completed", False), + error_message=resp.get("error_message", ""), + used_credits=resp.get("usedCredits", 0), + run_time=resp.get("runTime", 0), + usage=resp.get("usage", None), + ) except Exception as e: logging.error(f"Single Poll for Model: Error of polling for {name}: {e}") - response = f"EvolverResponse(status={ResponseStatus.FAILED}, error_message={str(e)}, completed=False,)" + if evolve: + response = f"EvolverResponse(status={ResponseStatus.FAILED}, error_message={str(e)}, completed=False)" + response = f"AgentResponse(status={ResponseStatus.FAILED}, error_message={str(e)}, completed=False)" return response From 64b12e7becbbdf57210746a449543c85f2909b21 Mon Sep 17 00:00:00 2001 From: Thiago Castro Ferreira Date: Mon, 31 Mar 2025 09:24:12 -0300 Subject: [PATCH 07/22] Fixing test model IDs test --- tests/functional/general_assets/asset_functional_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/general_assets/asset_functional_test.py b/tests/functional/general_assets/asset_functional_test.py index e1d65011..2ecdf147 100644 --- a/tests/functional/general_assets/asset_functional_test.py +++ b/tests/functional/general_assets/asset_functional_test.py @@ -93,7 +93,7 @@ def test_model_supplier(ModelFactory): def test_model_ids(): - model_ids = ["674728f51ed8e18fd8a1383f", "674728f51ed8e18fd8a1383c"] + model_ids = ["674728f51ed8e18fd8a1383f", "669a63646eb56306647e1091"] models = ModelFactory.list(model_ids=model_ids)["results"] assert len(models) == 2 assert sorted([model.id for model in models]) == sorted(model_ids) From 288f3d95b0df12c8baf9c2390659e466b0ca3717 Mon Sep 17 00:00:00 2001 From: Thiago Castro Ferreira Date: Sun, 6 Apr 2025 17:42:17 -0300 Subject: [PATCH 08/22] Update Agent Response to take Evolved Data into account --- aixplain/factories/agent_factory/__init__.py | 4 +- aixplain/modules/agent/agent_response.py | 7 +- aixplain/modules/agent/agent_response_data.py | 3 + aixplain/modules/agent/agent_task.py | 11 +- aixplain/modules/team_agent/__init__.py | 128 ++++++------------ .../modules/team_agent/evolver_response.py | 57 -------- .../team_agent/evolver_response_data.py | 13 +- tests/unit/agent/agent_test.py | 4 +- 8 files changed, 63 insertions(+), 164 deletions(-) delete mode 100644 aixplain/modules/team_agent/evolver_response.py diff --git a/aixplain/factories/agent_factory/__init__.py b/aixplain/factories/agent_factory/__init__.py index cefa964b..d9ec92c8 100644 --- a/aixplain/factories/agent_factory/__init__.py +++ b/aixplain/factories/agent_factory/__init__.py @@ -151,9 +151,7 @@ def create( return agent @classmethod - def create_task( - cls, name: Text, description: Text, expected_output: Text, dependencies: Optional[List[Text]] = None - ) -> AgentTask: + def create_task(cls, name: Text, description: Text, expected_output: Text, dependencies: List[Text] = []) -> AgentTask: return AgentTask(name=name, description=description, expected_output=expected_output, dependencies=dependencies) @classmethod diff --git a/aixplain/modules/agent/agent_response.py b/aixplain/modules/agent/agent_response.py index 73c5e839..5718cdfb 100644 --- a/aixplain/modules/agent/agent_response.py +++ b/aixplain/modules/agent/agent_response.py @@ -1,14 +1,17 @@ from aixplain.enums import ResponseStatus -from typing import Any, Dict, Optional, Text, Union, List +from typing import Any, Dict, Optional, Text, Union, List, TYPE_CHECKING from aixplain.modules.agent.agent_response_data import AgentResponseData from aixplain.modules.model.response import ModelResponse +if TYPE_CHECKING: + from aixplain.modules.team_agent.evolver_response_data import EvolverResponseData + class AgentResponse(ModelResponse): def __init__( self, status: ResponseStatus = ResponseStatus.FAILED, - data: Optional[AgentResponseData] = None, + data: Optional[Union[AgentResponseData, "EvolverResponseData"]] = None, details: Optional[Union[Dict, List]] = {}, completed: bool = False, error_message: Text = "", diff --git a/aixplain/modules/agent/agent_response_data.py b/aixplain/modules/agent/agent_response_data.py index 6040be0c..36fc015e 100644 --- a/aixplain/modules/agent/agent_response_data.py +++ b/aixplain/modules/agent/agent_response_data.py @@ -36,6 +36,9 @@ def to_dict(self) -> Dict[str, Any]: "execution_stats": self.execution_stats, } + def get(self, key: str, default: Optional[Any] = None) -> Any: + return getattr(self, key, default) + def __getitem__(self, key): return getattr(self, key, None) diff --git a/aixplain/modules/agent/agent_task.py b/aixplain/modules/agent/agent_task.py index 8d6acd2b..3f9c5572 100644 --- a/aixplain/modules/agent/agent_task.py +++ b/aixplain/modules/agent/agent_task.py @@ -1,4 +1,4 @@ -from typing import List, Optional, Text, Union +from typing import List, Text, Union class AgentTask: @@ -7,7 +7,7 @@ def __init__( name: Text, description: Text, expected_output: Text, - dependencies: Optional[List[Union[Text, "AgentTask"]]] = None, + dependencies: List[Union[Text, "AgentTask"]] = [], ): self.name = name self.description = description @@ -22,8 +22,7 @@ def to_dict(self): "dependencies": self.dependencies, } - if self.dependencies: - for i, dependency in enumerate(agent_task_dict["dependencies"]): - if isinstance(dependency, AgentTask): - agent_task_dict["dependencies"][i] = dependency.name + for i, dependency in enumerate(agent_task_dict.get("dependencies") or []): + if isinstance(dependency, AgentTask): + agent_task_dict["dependencies"][i] = dependency.name return agent_task_dict diff --git a/aixplain/modules/team_agent/__init__.py b/aixplain/modules/team_agent/__init__.py index 9ff7e9c7..f4918046 100644 --- a/aixplain/modules/team_agent/__init__.py +++ b/aixplain/modules/team_agent/__init__.py @@ -41,7 +41,6 @@ from aixplain.modules.agent.agent_response_data import AgentResponseData from aixplain.modules.agent.utils import process_variables from aixplain.modules.team_agent.evolver_response_data import EvolverResponseData -from aixplain.modules.team_agent.evolver_response import EvolverResponse from aixplain.utils import config from aixplain.utils.file_utils import _request_with_retry @@ -111,7 +110,7 @@ def __init__( self.additional_info = additional_info self.agents = agents self.llm_id = llm_id - self.api_key = api_key + self.api_key = api_key self.use_mentalist = use_mentalist self.use_inspector = use_inspector self.max_inspectors = max_inspectors @@ -140,7 +139,7 @@ def run( max_iterations: int = 30, output_format: OutputFormat = OutputFormat.TEXT, evolve: bool = False, - ) -> Dict: + ) -> AgentResponse: """Runs a team agent call. Args: @@ -158,7 +157,7 @@ def run( output_format (ResponseFormat, optional): response format. Defaults to TEXT. evolve (bool, optional): evolve the team agent. Defaults to False. Returns: - Dict: parsed output from model + AgentResponse: parsed output from model """ start = time.time() try: @@ -182,20 +181,7 @@ def run( poll_url = response["url"] end = time.time() result = self.sync_poll(poll_url, name=name, timeout=timeout, wait_time=wait_time) - result_data = result.data - return AgentResponse( - status=ResponseStatus.SUCCESS, - completed=True, - data=AgentResponseData( - input=result_data.get("input"), - output=result_data.get("output"), - session_id=result_data.get("session_id"), - intermediate_steps=result_data.get("intermediate_steps"), - execution_stats=result_data.get("executionStats"), - ), - used_credits=result_data.get("usedCredits", 0.0), - run_time=result_data.get("runTime", end - start), - ) + return result except Exception as e: logging.error(f"Team Agent Run: Error in running for {name}: {e}") end = time.time() @@ -218,7 +204,7 @@ def run_async( max_iterations: int = 30, output_format: OutputFormat = OutputFormat.TEXT, evolve: bool = False, - ) -> Dict: + ) -> AgentResponse: """Runs asynchronously a Team Agent call. Args: @@ -234,7 +220,7 @@ def run_async( output_format (ResponseFormat, optional): response format. Defaults to TEXT. evolve (bool, optional): evolve the team agent. Defaults to False. Returns: - dict: polling URL in response + AgentResponse: polling URL in response """ from aixplain.factories.file_factory import FileFactory @@ -308,92 +294,62 @@ def run_async( run_time=0.0, used_credits=0.0, ) - if evolve: - response = EvolverResponse( - status=ResponseStatus.IN_PROGRESS, - url=poll_url, - data=EvolverResponseData( - evolved_agent="", - current_code="", - evaluation_report="", - comparison_report="", - criteria="", - archive="",), - run_time=0.0, - used_credits=0.0, - ) except Exception: msg = f"Error in request for {name} - {traceback.format_exc()}" logging.error(f"Team Agent Run Async: Error in running for {name}: {resp}") if resp is not None: response = AgentResponse( - status=ResponseStatus.FAILED, - error=msg, - ) - if evolve: - response = EvolverResponse( - status=ResponseStatus.FAILED, - error=msg, - ) + status=ResponseStatus.FAILED, + error=msg, + ) return response - - def poll(self, poll_url: Text, name: Text = "model_process", evolve: bool = False) -> EvolverResponse: + def poll(self, poll_url: Text, name: Text = "model_process") -> AgentResponse: + used_credits, run_time = 0.0, 0.0 + resp, error_message, status = None, None, ResponseStatus.SUCCESS headers = {"x-api-key": self.api_key, "Content-Type": "application/json"} r = _request_with_retry("get", poll_url, headers=headers) try: resp = r.json() - evolver_data = None - resp_data = resp.get("data", {}) if resp["completed"] is True: - status = ResponseStatus.SUCCESS + status = ResponseStatus(resp.get("status", "FAILED")) if "error_message" in resp or "supplierError" in resp: status = ResponseStatus.FAILED + error_message = resp.get("error_message") else: status = ResponseStatus.IN_PROGRESS - response = f"EvolverResponse(status={status}, completed={resp['completed']})" - logging.debug(f"Single Poll for Model: Status of polling for {name}: {resp}") - - if evolve: - if status == ResponseStatus.SUCCESS: - evolver_data = EvolverResponseData.from_dict(resp_data, llm_id=self.llm_id, api_key=self.api_key) - - response = EvolverResponse( - status=status, - data=evolver_data or EvolverResponseData( - evolved_agent="", - current_code="", - evaluation_report="", - comparison_report="", - criteria="", - archive="",), - details=resp.get("details", {}), - completed=resp.get("completed", False), - error_message=resp.get("error_message", ""), - used_credits=resp.get("usedCredits", 0), - run_time=resp.get("runTime", 0), - usage=resp.get("usage", None), - ) - response = AgentResponse( - status=status, - data=resp.get("data", {}), - details=resp.get("details", {}), - completed=resp.get("completed", False), - error_message=resp.get("error_message", ""), - used_credits=resp.get("usedCredits", 0), - run_time=resp.get("runTime", 0), - usage=resp.get("usage", None), - ) + logging.debug(f"Single Poll for Team Agent: Status of polling for {name}: {resp}") + resp_data = resp.get("data") or {} + used_credits = resp_data.get("usedCredits", 0.0) + run_time = resp_data.get("runTime", 0.0) + if "evolved_agent" in resp_data and status == ResponseStatus.SUCCESS: + resp_data = EvolverResponseData.from_dict(resp_data, llm_id=self.llm_id, api_key=self.api_key) + else: + resp_data = AgentResponseData( + input=resp_data.get("input"), + output=resp_data.get("output"), + session_id=resp_data.get("session_id"), + intermediate_steps=resp_data.get("intermediate_steps"), + execution_stats=resp_data.get("executionStats"), + ) except Exception as e: - logging.error(f"Single Poll for Model: Error of polling for {name}: {e}") - if evolve: - response = f"EvolverResponse(status={ResponseStatus.FAILED}, error_message={str(e)}, completed=False)" - - response = f"AgentResponse(status={ResponseStatus.FAILED}, error_message={str(e)}, completed=False)" + logging.error(f"Single Poll for Team Agent: Error of polling for {name}: {e}") + status = ResponseStatus.FAILED + error_message = str(e) + finally: + response = AgentResponse( + status=status, + data=resp_data, + details=resp.get("details", {}), + completed=resp.get("completed", False), + used_credits=used_credits, + run_time=run_time, + usage=resp.get("usage", None), + error_message=error_message, + ) return response - def delete(self) -> None: """Delete Corpus service""" try: diff --git a/aixplain/modules/team_agent/evolver_response.py b/aixplain/modules/team_agent/evolver_response.py deleted file mode 100644 index c2f6fb2b..00000000 --- a/aixplain/modules/team_agent/evolver_response.py +++ /dev/null @@ -1,57 +0,0 @@ -from typing import Any, Optional, Text, Dict, List, Union -from aixplain.enums import ResponseStatus -from aixplain.modules.model.response import ModelResponse -from aixplain.modules.team_agent.evolver_response_data import EvolverResponseData - - -class EvolverResponse(ModelResponse): - - def __init__( - self, - status: ResponseStatus = ResponseStatus.FAILED, - data: Optional[EvolverResponseData] = None, - details: Optional[Union[Dict, List]] = {}, - completed: bool = False, - error_message: Text = "", - used_credits: float = 0.0, - run_time: float = 0.0, - usage: Optional[Dict] = None, - url: Optional[Text] = None, - **kwargs, - ): - - super().__init__( - status=status, - data="", - details=details, - completed=completed, - error_message=error_message, - used_credits=used_credits, - run_time=run_time, - usage=usage, - url=url, - **kwargs, - ) - self.data = data or EvolverResponseData() - - def __getitem__(self, key: Text) -> Any: - if key == "data": - return self.data.to_dict() - return super().__getitem__(key) - - def __setitem__(self, key: Text, value: Any) -> None: - if key == "data" and isinstance(value, Dict): - self.data = EvolverResponseData.from_dict(value) - elif key == "data" and isinstance(value, EvolverResponseData): - self.data = value - else: - super().__setitem__(key, value) - - def to_dict(self) -> Dict[Text, Any]: - base_dict = super().to_dict() - base_dict["data"] = self.data.to_dict() - return base_dict - - def __repr__(self) -> str: - fields = super().__repr__()[len("ModelResponse(") : -1] - return f"EvolverResponse({fields})" \ No newline at end of file diff --git a/aixplain/modules/team_agent/evolver_response_data.py b/aixplain/modules/team_agent/evolver_response_data.py index 793c5289..2ac022d9 100644 --- a/aixplain/modules/team_agent/evolver_response_data.py +++ b/aixplain/modules/team_agent/evolver_response_data.py @@ -3,10 +3,11 @@ if TYPE_CHECKING: from aixplain.modules.team_agent import TeamAgent + class EvolverResponseData: def __init__( self, - evolved_agent: 'TeamAgent', + evolved_agent: "TeamAgent", current_code: Text, evaluation_report: Text, comparison_report: Text, @@ -23,13 +24,9 @@ def __init__( @classmethod def from_dict(cls, data: Dict[str, Any], llm_id: Text, api_key: Text) -> "EvolverResponseData": from aixplain.factories.team_agent_factory.utils import build_team_agent_from_yaml - + yaml_code = data.get("current_code", "") - evolved_team_agent = build_team_agent_from_yaml( - yaml_code=yaml_code, - llm_id=llm_id, - api_key=api_key - ) + evolved_team_agent = build_team_agent_from_yaml(yaml_code=yaml_code, llm_id=llm_id, api_key=api_key) return cls( evolved_agent=evolved_team_agent, @@ -67,4 +64,4 @@ def __repr__(self) -> str: f"comparison_report='{self.comparison_report}', " f"criteria='{self.criteria}', " f"archive='{self.archive}')" - ) \ No newline at end of file + ) diff --git a/tests/unit/agent/agent_test.py b/tests/unit/agent/agent_test.py index 8afda296..54f3d311 100644 --- a/tests/unit/agent/agent_test.py +++ b/tests/unit/agent/agent_test.py @@ -516,13 +516,13 @@ def test_create_agent_task(): assert task.name == "Test Task" assert task.description == "Test Description" assert task.expected_output == "Test Output" - assert task.dependencies is None + assert task.dependencies == [] task_dict = task.to_dict() assert task_dict["name"] == "Test Task" assert task_dict["description"] == "Test Description" assert task_dict["expectedOutput"] == "Test Output" - assert task_dict["dependencies"] is None + assert task_dict["dependencies"] == [] def test_agent_response(): From 90f7e9650b746450c7c0b123a0e316a9b00b01e1 Mon Sep 17 00:00:00 2001 From: Thiago Castro Ferreira Date: Mon, 7 Apr 2025 13:00:14 -0300 Subject: [PATCH 09/22] Adding current output --- aixplain/modules/team_agent/evolver_response_data.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/aixplain/modules/team_agent/evolver_response_data.py b/aixplain/modules/team_agent/evolver_response_data.py index 2ac022d9..cd2499b5 100644 --- a/aixplain/modules/team_agent/evolver_response_data.py +++ b/aixplain/modules/team_agent/evolver_response_data.py @@ -13,6 +13,7 @@ def __init__( comparison_report: Text, criteria: Text, archive: List[Text], + current_output: Text = "", ) -> None: self.evolved_agent = evolved_agent self.current_code = current_code @@ -20,6 +21,7 @@ def __init__( self.comparison_report = comparison_report self.criteria = criteria self.archive = archive + self.current_output = current_output @classmethod def from_dict(cls, data: Dict[str, Any], llm_id: Text, api_key: Text) -> "EvolverResponseData": @@ -35,6 +37,7 @@ def from_dict(cls, data: Dict[str, Any], llm_id: Text, api_key: Text) -> "Evolve comparison_report=data.get("comparison_report", ""), criteria=data.get("criteria", ""), archive=data.get("archive", []), + current_output=data.get("current_output", ""), ) def to_dict(self) -> Dict[str, Any]: @@ -45,6 +48,7 @@ def to_dict(self) -> Dict[str, Any]: "comparison_report": self.comparison_report, "criteria": self.criteria, "archive": self.archive, + "current_output": self.current_output, } def __getitem__(self, key: str) -> Any: @@ -63,5 +67,5 @@ def __repr__(self) -> str: f"evaluation_report='{self.evaluation_report}', " f"comparison_report='{self.comparison_report}', " f"criteria='{self.criteria}', " - f"archive='{self.archive}')" + f"archive='{self.archive}', " ) From f54a8f31b5f391464a3f52da2e83ce2811525968 Mon Sep 17 00:00:00 2001 From: OsujiCC Date: Wed, 16 Apr 2025 17:59:22 -0400 Subject: [PATCH 10/22] evolver functional test (#483) --- tests/functional/team_agent/evolver_test.py | 143 ++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 tests/functional/team_agent/evolver_test.py diff --git a/tests/functional/team_agent/evolver_test.py b/tests/functional/team_agent/evolver_test.py new file mode 100644 index 00000000..fdff6717 --- /dev/null +++ b/tests/functional/team_agent/evolver_test.py @@ -0,0 +1,143 @@ +import pytest +from aixplain.enums.function import Function +from aixplain.enums.supplier import Supplier +from aixplain.enums import ResponseStatus +from aixplain.factories.agent_factory import AgentFactory +from aixplain.factories.team_agent_factory import TeamAgentFactory +import time + + +team_dict = { + "team_agent_name": "Test Text Speech Team", + "llm_id": "6646261c6eb563165658bbb1", + "llm_name": "GPT4o", + "query": "Translate this text into Portuguese: 'This is a test'. Translate to pt and synthesize in audio", + "description": "You are a text translation and speech synthesizing agent. You will be provided a text in the source language and expected to translate and synthesize in the target language.", + "agents": [ + { + "agent_name": "Text Translation agent", + "llm_id": "6646261c6eb563165658bbb1", + "llm_name": "GPT4o", + "description": "## ROLE\nText Translator\n\n## GOAL\nTranslate the text supplied into the users desired language.\n\n## BACKSTORY\nYou are a text translation agent. You will be provided a text in the source language and expected to translate in the target language.", + + "tasks": [ + { + "name": "Text translation", + "description": "Translate a text from source language (English) to target language (Portuguese)", + "expected_output": "target language text", + } + ], + "model_tools": [ + { + "function": "translation", + "supplier": "AWS" + } + ] + }, + { + "agent_name": "Test Speech Synthesis agent", + "llm_id": "6646261c6eb563165658bbb1", + "llm_name": "GPT4o", + "description": "## ROLE\nSpeech Synthesizer\n\n## GOAL\nTranscribe the translated text into speech.\n\n## BACKSTORY\nYou are a speech synthesizing agent. You will be provided a text to synthesize into audio and return the audio link.", + "tasks": [ + { + "name": "Speech synthesis", + "description": "Synthesize a text from text to speech", + "expected_output": "audio link of the synthesized text", + "dependencies": ["Text translation"], + } + ], + "model_tools": [ + { + "function": "speech_synthesis", + "supplier": "Google" + } + ] + } + ] + } + +def parse_tools(tools_info): + tools = [] + for tool in tools_info: + function_enum = Function[tool['function'].upper().replace(' ', '_')] + supplier_enum = Supplier[tool['supplier'].upper().replace(' ', '_')] + tools.append( + AgentFactory.create_model_tool( + function=function_enum, + supplier=supplier_enum + ) + ) + return tools + + +def build_team_agent_from_json(team_config: dict): + agents_data = team_config["agents"] + tasks_data = team_config.get("tasks", []) + + agent_objs = [] + for agent_entry in agents_data: + agent_name = agent_entry["agent_name"] + agent_description = agent_entry["description"] + agent_llm_id = agent_entry.get("llm_id", None) + + agent_tasks = [] + for task in tasks_data: + task_name = task.get("task_name", "") + task_info = task + + if agent_name == task_info["agent"]: + task_obj = AgentFactory.create_task( + name=task_name.replace("_", " "), + description=task_info.get("description", ""), + expected_output=task_info.get("expected_output", ""), + dependencies=[t.replace("_", " ") for t in task_info.get("dependencies", [])] + ) + agent_tasks.append(task_obj) + + if "model_tools" in agent_entry: + agent_tools = parse_tools(agent_entry["model_tools"]) + else: + agent_tools = [] + + + agent_obj = AgentFactory.create( + name=agent_name.replace("_", " "), + description=agent_description, + tools=agent_tools, + tasks=agent_tasks, + llm_id=agent_llm_id + ) + agent_objs.append(agent_obj) + + return TeamAgentFactory.create( + name=team_config["team_agent_name"], + agents=agent_objs, + description=team_config["description"], + llm_id=team_config.get("llm_id", None), + use_inspector=False, + use_mentalist=True + ) + + +@pytest.fixture +def team_agent(): + return build_team_agent_from_json(team_dict) + +def test_evolver_output(team_agent): + query = "Translate this text into Portuguese: 'This is a test'. Translate to pt and synthesize in audio" + response = team_agent.evolve(query) + poll_url = response["url"] + result = team_agent.poll(poll_url) + + while result.status == ResponseStatus.IN_PROGRESS: + time.sleep(30) + result = team_agent.poll(poll_url) + + + assert "system" in result['data']['evolved_agent'].to_dict()['name'].lower(), "System should be in the system name" + assert result['status'] == ResponseStatus.SUCCESS, "Final result should have a 'SUCCESS' status" + assert 'evolved_agent' in result['data'], "Data should contain 'evolved_agent'" + assert 'evaluation_report' in result['data'], "Data should contain 'evaluation_report'" + assert 'criteria' in result['data'], "Data should contain 'criteria'" + assert 'archive' in result['data'], "Data should contain 'archive'" \ No newline at end of file From 321d7a84ebb2b87a50c0b346900fe704f6f1ffca Mon Sep 17 00:00:00 2001 From: ahmetgunduz Date: Thu, 12 Jun 2025 14:09:33 +0300 Subject: [PATCH 11/22] ENG-2225: Evolver Param Edded --- .pre-commit-config.yaml | 18 +- aixplain/enums/__init__.py | 1 + aixplain/enums/evolve_type.py | 6 + aixplain/modules/agent/__init__.py | 85 +++++--- aixplain/modules/agent/evolve_param.py | 216 ++++++++++++++++++++ aixplain/modules/team_agent/__init__.py | 134 +++++------- tests/functional/team_agent/evolver_test.py | 121 +++++------ tests/unit/agent/evolve_param_test.py | 165 +++++++++++++++ 8 files changed, 560 insertions(+), 186 deletions(-) create mode 100644 aixplain/enums/evolve_type.py create mode 100644 aixplain/modules/agent/evolve_param.py create mode 100644 tests/unit/agent/evolve_param_test.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2500c2c8..4ad3e5bc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,18 +8,26 @@ repos: pass_filenames: false types: [python] always_run: true - + - repo: https://github.com/psf/black - rev: 22.10.0 + rev: 25.1.0 hooks: - id: black language_version: python3 args: # arguments to configure black - --line-length=128 - + - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.0.0 # Use the latest version + rev: v5.0.0 # Use the latest version + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-merge-conflict + - id: check-added-large-files + + - repo: https://github.com/pycqa/flake8 + rev: 7.2.0 hooks: - id: flake8 args: # arguments to configure flake8 - - --ignore=E402,E501,E203,W503 \ No newline at end of file + - --ignore=E402,E501,E203,W503 diff --git a/aixplain/enums/__init__.py b/aixplain/enums/__init__.py index 725fdb90..73f1576c 100644 --- a/aixplain/enums/__init__.py +++ b/aixplain/enums/__init__.py @@ -20,3 +20,4 @@ from .asset_status import AssetStatus from .index_stores import IndexStores from .function_type import FunctionType +from .evolve_type import EvolveType diff --git a/aixplain/enums/evolve_type.py b/aixplain/enums/evolve_type.py new file mode 100644 index 00000000..555fdb53 --- /dev/null +++ b/aixplain/enums/evolve_type.py @@ -0,0 +1,6 @@ +from enum import Enum + + +class EvolveType(str, Enum): + TEAM_TUNING = "team_tuning" + INSTRUCTION_TUNING = "instruction_tuning" diff --git a/aixplain/modules/agent/__init__.py b/aixplain/modules/agent/__init__.py index 34ae74ec..a61ab842 100644 --- a/aixplain/modules/agent/__init__.py +++ b/aixplain/modules/agent/__init__.py @@ -36,7 +36,8 @@ from aixplain.modules.agent.agent_response_data import AgentResponseData from aixplain.modules.agent.utils import process_variables from pydantic import BaseModel -from typing import Dict, List, Text, Optional, Union +from typing import Dict, List, Text, Optional, Union, Any +from aixplain.modules.agent.evolve_param import EvolveParam, validate_evolve_param from urllib.parse import urljoin from aixplain.modules.model.llm_model import LLM @@ -260,6 +261,7 @@ def run_async( max_iterations: int = 10, output_format: OutputFormat = OutputFormat.TEXT, expected_output: Optional[Union[BaseModel, Text, dict]] = None, + evolve: Union[Dict[str, Any], EvolveParam, None] = None, ) -> AgentResponse: """Runs asynchronously an agent call. @@ -275,13 +277,16 @@ def run_async( max_iterations (int, optional): maximum number of iterations between the agent and the tools. Defaults to 10. output_format (OutputFormat, optional): response format. Defaults to TEXT. expected_output (Union[BaseModel, Text, dict], optional): expected output. Defaults to None. + output_format (ResponseFormat, optional): response format. Defaults to TEXT. + evolve (Union[Dict[str, Any], EvolveParam, None], optional): evolve the agent configuration. Can be a dictionary, EvolveParam instance, or None. Returns: dict: polling URL in response """ from aixplain.factories.file_factory import FileFactory - if not self.is_valid: - raise Exception("Agent is not valid. Please validate the agent before running.") + # Validate and normalize evolve parameters using the base model + evolve_param = validate_evolve_param(evolve) + evolve_dict = evolve_param.to_dict() if output_format == OutputFormat.JSON: assert expected_output is not None and ( @@ -338,6 +343,7 @@ def run_async( "outputFormat": output_format, "expectedOutput": expected_output, }, + "evolve": json.dumps(evolve_dict), } payload.update(parameters) @@ -376,15 +382,17 @@ def to_dict(self) -> Dict: "llmId": self.llm_id if self.llm is None else self.llm.id, "status": self.status.value, "tasks": [task.to_dict() for task in self.tasks], - "tools": [ - { - "type": "llm", - "description": "main", - "parameters": self.llm.get_parameters().to_list() if self.llm.get_parameters() else None, - } - ] - if self.llm is not None - else [], + "tools": ( + [ + { + "type": "llm", + "description": "main", + "parameters": (self.llm.get_parameters().to_list() if self.llm.get_parameters() else None), + } + ] + if self.llm is not None + else [] + ), } def delete(self) -> None: @@ -395,30 +403,22 @@ def delete(self) -> None: "x-api-key": config.TEAM_API_KEY, "Content-Type": "application/json", } - logging.debug( - f"Start service for DELETE Agent - {url} - {headers}" - ) + logging.debug(f"Start service for DELETE Agent - {url} - {headers}") r = _request_with_retry("delete", url, headers=headers) - logging.debug( - f"Result of request for DELETE Agent - {r.status_code}" - ) + logging.debug(f"Result of request for DELETE Agent - {r.status_code}") if r.status_code != 200: raise Exception() except Exception: try: response_json = r.json() - error_message = response_json.get('message', '').strip('{{}}') + error_message = response_json.get("message", "").strip("{{}}") if r.status_code == 403 and error_message == "err.agent_is_in_use": # Get team agents that use this agent - from aixplain.factories.team_agent_factory import ( - TeamAgentFactory - ) + from aixplain.factories.team_agent_factory import TeamAgentFactory + team_agents = TeamAgentFactory.list()["results"] - using_team_agents = [ - ta for ta in team_agents - if any(agent.id == self.id for agent in ta.agents) - ] + using_team_agents = [ta for ta in team_agents if any(agent.id == self.id for agent in ta.agents)] if using_team_agents: # Scenario 1: User has access to team agents @@ -441,15 +441,9 @@ def delete(self) -> None: "referencing it." ) else: - message = ( - f"Agent Deletion Error (HTTP {r.status_code}): " - f"{error_message}." - ) + message = f"Agent Deletion Error (HTTP {r.status_code}): " f"{error_message}." except ValueError: - message = ( - f"Agent Deletion Error (HTTP {r.status_code}): " - "There was an error in deleting the agent." - ) + message = f"Agent Deletion Error (HTTP {r.status_code}): " "There was an error in deleting the agent." logging.error(message) raise Exception(message) @@ -494,3 +488,26 @@ def save(self) -> None: def __repr__(self): return f"Agent: {self.name} (id={self.id})" + + def evolve(self, evolve_parameters: Union[Dict[str, Any], EvolveParam, None] = None) -> None: + """Evolve the Agent. + + Args: + evolve_parameters (Union[Dict[str, Any], EvolveParam, None]): Evolution parameters. + Can be a dictionary, EvolveParam instance, or None. + + Returns: + AgentResponse: Response from the evolution process. + """ + query = "Placeholder query" + if evolve_parameters is None: + evolve_parameters = EvolveParam(to_evolve=True) + elif isinstance(evolve_parameters, dict): + evolve_parameters = EvolveParam.from_dict(evolve_parameters) + evolve_parameters.to_evolve = True + elif isinstance(evolve_parameters, EvolveParam): + evolve_parameters.to_evolve = True + else: + raise ValueError("evolve_parameters must be a dictionary, EvolveParam instance, or None") + + return self.run_async(query=query, evolve=evolve_parameters) diff --git a/aixplain/modules/agent/evolve_param.py b/aixplain/modules/agent/evolve_param.py new file mode 100644 index 00000000..afa86750 --- /dev/null +++ b/aixplain/modules/agent/evolve_param.py @@ -0,0 +1,216 @@ +__author__ = "aiXplain" + +""" +Copyright 2024 The aiXplain SDK authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Author: aiXplain Team +Date: December 2024 +Description: + EvolveParam Base Model Class for Agent and TeamAgent evolve functionality +""" +from aixplain.enums import EvolveType +from dataclasses import dataclass, field +from typing import Any, Dict, Optional, Union + + +@dataclass +class EvolveParam: + """Base model for evolve parameters used in Agent and TeamAgent evolution. + + Attributes: + to_evolve (bool): Whether to enable evolution. Defaults to False. + criteria (Optional[str]): Custom criteria for evolution evaluation. + max_iterations (Optional[int]): Maximum number of evolution iterations. + temperature (Optional[float]): Temperature for evolution randomness (0.0-1.0). + type (Optional[EvolveType]): Type of evolution. + """ + + to_evolve: bool = False + criteria: Optional[str] = None + max_iterations: Optional[int] = 100 + temperature: Optional[float] = 0.0 + type: Optional[EvolveType] = EvolveType.TEAM_TUNING + additional_params: Optional[Dict[str, Any]] = field(default_factory=dict) + + def __post_init__(self): + """Validate parameters after initialization.""" + self.validate() + + def validate(self) -> None: + """Validate evolve parameters. + + Raises: + ValueError: If any parameter is invalid. + """ + if self.temperature is not None: + if not isinstance(self.temperature, (int, float)): + raise ValueError("temperature must be a number") + if not 0.0 <= self.temperature <= 1.0: + raise ValueError("temperature must be between 0.0 and 1.0") + + if self.max_iterations is not None: + if not isinstance(self.max_iterations, int): + raise ValueError("max_iterations must be an integer") + if self.max_iterations <= 0: + raise ValueError("max_iterations must be positive") + + if self.type is not None: + if not isinstance(self.type, EvolveType): + raise ValueError("type must be a valid EvolveType") + if self.additional_params is not None: + if not isinstance(self.additional_params, dict): + raise ValueError("additional_params must be a dictionary") + + @classmethod + def from_dict(cls, data: Union[Dict[str, Any], None]) -> "EvolveParam": + """Create EvolveParam instance from dictionary. + + Args: + data (Union[Dict[str, Any], None]): Dictionary containing evolve parameters. + + Returns: + EvolveParam: Instance with parameters set from dictionary. + + Raises: + ValueError: If data format is invalid. + """ + if data is None: + return cls() + + if not isinstance(data, dict): + raise ValueError("evolve parameter must be a dictionary or None") + + # Extract known parameters + known_params = { + "to_evolve": data.get("toEvolve", data.get("to_evolve", False)), + "criteria": data.get("criteria"), + "max_iterations": data.get("maxIterations", data.get("max_iterations")), + "temperature": data.get("temperature"), + "type": data.get("type"), + "additional_params": data.get("additional_params"), + } + + # Remove None values + known_params = {k: v for k, v in known_params.items() if v is not None} + + # Collect additional parameters + additional_params = { + k: v + for k, v in data.items() + if k + not in [ + "toEvolve", + "to_evolve", + "criteria", + "maxIterations", + "max_iterations", + "temperature", + "type", + "additional_params", + ] + } + + return cls(additional_params=additional_params, **known_params) + + def to_dict(self) -> Dict[str, Any]: + """Convert EvolveParam instance to dictionary for API calls. + + Returns: + Dict[str, Any]: Dictionary representation with API-compatible keys. + """ + result = { + "toEvolve": self.to_evolve, + } + + # Add optional parameters if they are set + if self.criteria is not None: + result["criteria"] = self.criteria + if self.max_iterations is not None: + result["maxIterations"] = self.max_iterations + if self.temperature is not None: + result["temperature"] = self.temperature + if self.type is not None: + result["type"] = self.type + if self.additional_params is not None: + result.update(self.additional_params) + + return result + + def merge(self, other: Union[Dict[str, Any], "EvolveParam"]) -> "EvolveParam": + """Merge this EvolveParam with another set of parameters. + + Args: + other (Union[Dict[str, Any], EvolveParam]): Other parameters to merge. + + Returns: + EvolveParam: New instance with merged parameters. + """ + if isinstance(other, dict): + other = EvolveParam.from_dict(other) + elif not isinstance(other, EvolveParam): + raise ValueError("other must be a dictionary or EvolveParam instance") + + # Create merged parameters + merged_additional = {**self.additional_params, **other.additional_params} + + return EvolveParam( + to_evolve=other.to_evolve if other.to_evolve else self.to_evolve, + criteria=other.criteria if other.criteria is not None else self.criteria, + max_iterations=(other.max_iterations if other.max_iterations is not None else self.max_iterations), + temperature=(other.temperature if other.temperature is not None else self.temperature), + type=(other.type if other.type is not None else self.type), + additional_params=merged_additional, + ) + + def __repr__(self) -> str: + return ( + f"EvolveParam(" + f"to_evolve={self.to_evolve}, " + f"criteria={self.criteria}, " + f"max_iterations={self.max_iterations}, " + f"temperature={self.temperature}, " + f"type={self.type}, " + f"additional_params={self.additional_params})" + ) + + +def validate_evolve_param( + evolve_param: Union[Dict[str, Any], EvolveParam, None], +) -> EvolveParam: + """Utility function to validate and convert evolve parameters. + + Args: + evolve_param (Union[Dict[str, Any], EvolveParam, None]): Input evolve parameters. + + Returns: + EvolveParam: Validated EvolveParam instance. + + Raises: + ValueError: If parameters are invalid. + """ + if evolve_param is None: + return EvolveParam() + + if isinstance(evolve_param, EvolveParam): + evolve_param.validate() + return evolve_param + + if isinstance(evolve_param, dict): + # Check for required toEvolve key for backward compatibility + if "toEvolve" not in evolve_param and "to_evolve" not in evolve_param: + raise ValueError("evolve parameter must contain 'toEvolve' key") + return EvolveParam.from_dict(evolve_param) + + raise ValueError("evolve parameter must be a dictionary, EvolveParam instance, or None") diff --git a/aixplain/modules/team_agent/__init__.py b/aixplain/modules/team_agent/__init__.py index 7c8c3d53..afcbe1bd 100644 --- a/aixplain/modules/team_agent/__init__.py +++ b/aixplain/modules/team_agent/__init__.py @@ -27,7 +27,7 @@ import traceback import re from enum import Enum -from typing import Dict, List, Text, Optional, Union +from typing import Dict, List, Text, Optional, Union, Any from urllib.parse import urljoin from aixplain.enums import ResponseStatus @@ -39,6 +39,7 @@ from aixplain.modules.agent import Agent, OutputFormat from aixplain.modules.agent.agent_response import AgentResponse from aixplain.modules.agent.agent_response_data import AgentResponseData +from aixplain.modules.agent.evolve_param import EvolveParam, validate_evolve_param from aixplain.modules.agent.utils import process_variables from aixplain.modules.team_agent.inspector import Inspector from aixplain.modules.team_agent.evolver_response_data import EvolverResponseData @@ -135,7 +136,6 @@ def run( max_iterations: int = 30, output_format: OutputFormat = OutputFormat.TEXT, expected_output: Optional[Union[BaseModel, Text, dict]] = None, - evolve: bool = False, ) -> AgentResponse: """Runs a team agent call. @@ -153,12 +153,10 @@ def run( max_iterations (int, optional): maximum number of iterations between the agents. Defaults to 30. output_format (OutputFormat, optional): response format. Defaults to TEXT. expected_output (Union[BaseModel, Text, dict], optional): expected output. Defaults to None. - evolve (bool, optional): evolve the team agent. Defaults to False. Returns: AgentResponse: parsed output from model """ start = time.time() - result_data = {} try: response = self.run_async( data=data, @@ -172,7 +170,6 @@ def run( max_iterations=max_iterations, output_format=output_format, expected_output=expected_output, - evolve=evolve, ) if response["status"] == ResponseStatus.FAILED: end = time.time() @@ -180,9 +177,7 @@ def run( return response poll_url = response["url"] end = time.time() - result = self.sync_poll( - poll_url, name=name, timeout=timeout, wait_time=wait_time - ) + result = self.sync_poll(poll_url, name=name, timeout=timeout, wait_time=wait_time) result_data = result.data return AgentResponse( status=ResponseStatus.SUCCESS, @@ -220,7 +215,7 @@ def run_async( max_iterations: int = 30, output_format: OutputFormat = OutputFormat.TEXT, expected_output: Optional[Union[BaseModel, Text, dict]] = None, - evolve: bool = False, + evolve: Union[Dict[str, Any], EvolveParam, None] = None, ) -> AgentResponse: """Runs asynchronously a Team Agent call. @@ -236,25 +231,23 @@ def run_async( max_iterations (int, optional): maximum number of iterations between the agents. Defaults to 30. output_format (OutputFormat, optional): response format. Defaults to TEXT. expected_output (Union[BaseModel, Text, dict], optional): expected output. Defaults to None. - evolve (bool, optional): evolve the team agent. Defaults to False. + evolve (Union[Dict[str, Any], EvolveParam, None], optional): evolve the team agent configuration. Can be a dictionary, EvolveParam instance, or None. Returns: AgentResponse: polling URL in response """ from aixplain.factories.file_factory import FileFactory + # Validate and normalize evolve parameters using the base model + evolve_param = validate_evolve_param(evolve) + evolve_dict = evolve_param.to_dict() + if not self.is_valid: - raise Exception( - "Team Agent is not valid. Please validate the team agent before running." - ) + raise Exception("Team Agent is not valid. Please validate the team agent before running.") - assert ( - data is not None or query is not None - ), "Either 'data' or 'query' must be provided." + assert data is not None or query is not None, "Either 'data' or 'query' must be provided." if data is not None: if isinstance(data, dict): - assert ( - "query" in data and data["query"] is not None - ), "When providing a dictionary, 'query' must be provided." + assert "query" in data and data["query"] is not None, "When providing a dictionary, 'query' must be provided." if session_id is None: session_id = data.pop("session_id", None) if history is None: @@ -268,8 +261,7 @@ def run_async( # process content inputs if content is not None: assert ( - isinstance(query, str) - and FileFactory.check_storage_type(query) == StorageType.TEXT + isinstance(query, str) and FileFactory.check_storage_type(query) == StorageType.TEXT ), "When providing 'content', query must be text." if isinstance(content, list): @@ -279,9 +271,7 @@ def run_async( query += f"\n{input_link}" elif isinstance(content, dict): for key, value in content.items(): - assert ( - "{{" + key + "}}" in query - ), f"Key '{key}' not found in query." + assert "{{" + key + "}}" in query, f"Key '{key}' not found in query." value = FileFactory.to_link(value) query = query.replace("{{" + key + "}}", f"'{value}'") @@ -301,28 +291,18 @@ def run_async( "sessionId": session_id, "history": history, "executionParams": { - "maxTokens": ( - parameters["max_tokens"] - if "max_tokens" in parameters - else max_tokens - ), - "maxIterations": ( - parameters["max_iterations"] - if "max_iterations" in parameters - else max_iterations - ), + "maxTokens": (parameters["max_tokens"] if "max_tokens" in parameters else max_tokens), + "maxIterations": (parameters["max_iterations"] if "max_iterations" in parameters else max_iterations), "outputFormat": output_format, "expectedOutput": expected_output, }, - "evolve": evolve, + "evolve": json.dumps(evolve_dict), } payload.update(parameters) payload = json.dumps(payload) r = _request_with_retry("post", self.url, headers=headers, data=payload) - logging.info( - f"Team Agent Run Async: Start service for {name} - {self.url} - {payload} - {headers}" - ) + logging.info(f"Team Agent Run Async: Start service for {name} - {self.url} - {payload} - {headers}") resp = None try: @@ -361,17 +341,13 @@ def poll(self, poll_url: Text, name: Text = "model_process") -> AgentResponse: error_message = resp.get("error_message") else: status = ResponseStatus.IN_PROGRESS - logging.debug( - f"Single Poll for Team Agent: Status of polling for {name}: {resp}" - ) + logging.debug(f"Single Poll for Team Agent: Status of polling for {name}: {resp}") resp_data = resp.get("data") or {} used_credits = resp_data.get("usedCredits", 0.0) run_time = resp_data.get("runTime", 0.0) if "evolved_agent" in resp_data and status == ResponseStatus.SUCCESS: - resp_data = EvolverResponseData.from_dict( - resp_data, llm_id=self.llm_id, api_key=self.api_key - ) + resp_data = EvolverResponseData.from_dict(resp_data, llm_id=self.llm_id, api_key=self.api_key) else: resp_data = AgentResponseData( input=resp_data.get("input"), @@ -381,9 +357,7 @@ def poll(self, poll_url: Text, name: Text = "model_process") -> AgentResponse: execution_stats=resp_data.get("executionStats"), ) except Exception as e: - logging.error( - f"Single Poll for Team Agent: Error of polling for {name}: {e}" - ) + logging.error(f"Single Poll for Team Agent: Error of polling for {name}: {e}") status = ResponseStatus.FAILED error_message = str(e) finally: @@ -412,7 +386,9 @@ def delete(self) -> None: if r.status_code != 200: raise Exception() except Exception: - message = f"Team Agent Deletion Error (HTTP {r.status_code}): Make sure the Team Agent exists and you are the owner." + message = ( + f"Team Agent Deletion Error (HTTP {r.status_code}): Make sure the Team Agent exists and you are the owner." + ) logging.error(message) raise Exception(f"{message}") @@ -425,25 +401,16 @@ def to_dict(self) -> Dict: "id": self.id, "name": self.name, "agents": [ - {"assetId": agent.id, "number": idx, "type": "AGENT", "label": "AGENT"} - for idx, agent in enumerate(self.agents) + {"assetId": agent.id, "number": idx, "type": "AGENT", "label": "AGENT"} for idx, agent in enumerate(self.agents) ], "links": [], "description": self.description, "llmId": self.llm.id if self.llm else self.llm_id, - "supervisorId": ( - self.supervisor_llm.id if self.supervisor_llm else self.llm_id - ), + "supervisorId": (self.supervisor_llm.id if self.supervisor_llm else self.llm_id), "plannerId": planner_id, - "inspectors": [ - inspector.model_dump(by_alias=True) for inspector in self.inspectors - ], + "inspectors": [inspector.model_dump(by_alias=True) for inspector in self.inspectors], "inspectorTargets": [target.value for target in self.inspector_targets], - "supplier": ( - self.supplier.value["code"] - if isinstance(self.supplier, Supplier) - else self.supplier - ), + "supplier": (self.supplier.value["code"] if isinstance(self.supplier, Supplier) else self.supplier), "version": self.version, "status": self.status.value, "role": self.instructions, @@ -461,9 +428,7 @@ def _validate(self) -> None: try: llm = get_llm_instance(self.llm_id) - assert ( - llm.function == Function.TEXT_GENERATION - ), "Large Language Model must be a text generation model." + assert llm.function == Function.TEXT_GENERATION, "Large Language Model must be a text generation model." except Exception: raise Exception(f"Large Language Model with ID '{self.llm_id}' not found.") @@ -480,9 +445,7 @@ def validate(self, raise_exception: bool = False) -> bool: raise e else: logging.warning(f"Team Agent Validation Error: {e}") - logging.warning( - "You won't be able to run the Team Agent until the issues are handled manually." - ) + logging.warning("You won't be able to run the Team Agent until the issues are handled manually.") return self.is_valid @@ -495,8 +458,7 @@ def update(self) -> None: stack = inspect.stack() if len(stack) > 2 and stack[1].function != "save": warnings.warn( - "update() is deprecated and will be removed in a future version. " - "Please use save() instead.", + "update() is deprecated and will be removed in a future version. " "Please use save() instead.", DeprecationWarning, stacklevel=2, ) @@ -508,17 +470,13 @@ def update(self) -> None: payload = self.to_dict() - logging.debug( - f"Start service for PUT Update Team Agent - {url} - {headers} - {json.dumps(payload)}" - ) + logging.debug(f"Start service for PUT Update Team Agent - {url} - {headers} - {json.dumps(payload)}") resp = "No specified error." try: r = _request_with_retry("put", url, headers=headers, json=payload) resp = r.json() except Exception: - raise Exception( - "Team Agent Update Error: Please contact the administrators." - ) + raise Exception("Team Agent Update Error: Please contact the administrators.") if 200 <= r.status_code < 300: return build_team_agent(resp) @@ -529,6 +487,26 @@ def update(self) -> None: def __repr__(self): return f"TeamAgent: {self.name} (id={self.id})" - def evolve(self, query: Text) -> None: - """Evolve the Team Agent.""" - return self.run_async(query=query, evolve=True) + def evolve(self, evolve_parameters: Union[Dict[str, Any], EvolveParam, None] = None) -> None: + """Evolve the Team Agent. + + Args: + evolve_parameters (Union[Dict[str, Any], EvolveParam, None]): Evolution parameters. + Can be a dictionary, EvolveParam instance, or None. + + Returns: + AgentResponse: Response from the evolution process. + """ + query = "Placeholder query" + if evolve_parameters is None: + evolve_parameters = EvolveParam(to_evolve=True) + elif isinstance(evolve_parameters, dict): + evolve_parameters = EvolveParam.from_dict(evolve_parameters) + evolve_parameters.to_evolve = True + elif isinstance(evolve_parameters, EvolveParam): + evolve_parameters.to_evolve = True + else: + raise ValueError("evolve_parameters must be a dictionary, EvolveParam instance, or None") + + response = self.run_async(query=query, evolve=evolve_parameters) + return response diff --git a/tests/functional/team_agent/evolver_test.py b/tests/functional/team_agent/evolver_test.py index fdff6717..192d408b 100644 --- a/tests/functional/team_agent/evolver_test.py +++ b/tests/functional/team_agent/evolver_test.py @@ -8,66 +8,51 @@ team_dict = { - "team_agent_name": "Test Text Speech Team", - "llm_id": "6646261c6eb563165658bbb1", - "llm_name": "GPT4o", - "query": "Translate this text into Portuguese: 'This is a test'. Translate to pt and synthesize in audio", - "description": "You are a text translation and speech synthesizing agent. You will be provided a text in the source language and expected to translate and synthesize in the target language.", - "agents": [ - { - "agent_name": "Text Translation agent", - "llm_id": "6646261c6eb563165658bbb1", - "llm_name": "GPT4o", - "description": "## ROLE\nText Translator\n\n## GOAL\nTranslate the text supplied into the users desired language.\n\n## BACKSTORY\nYou are a text translation agent. You will be provided a text in the source language and expected to translate in the target language.", - - "tasks": [ - { - "name": "Text translation", - "description": "Translate a text from source language (English) to target language (Portuguese)", - "expected_output": "target language text", - } - ], - "model_tools": [ - { - "function": "translation", - "supplier": "AWS" - } - ] - }, - { - "agent_name": "Test Speech Synthesis agent", - "llm_id": "6646261c6eb563165658bbb1", - "llm_name": "GPT4o", - "description": "## ROLE\nSpeech Synthesizer\n\n## GOAL\nTranscribe the translated text into speech.\n\n## BACKSTORY\nYou are a speech synthesizing agent. You will be provided a text to synthesize into audio and return the audio link.", - "tasks": [ - { - "name": "Speech synthesis", - "description": "Synthesize a text from text to speech", - "expected_output": "audio link of the synthesized text", - "dependencies": ["Text translation"], - } - ], - "model_tools": [ - { - "function": "speech_synthesis", - "supplier": "Google" - } - ] - } - ] - } + "team_agent_name": "Test Text Speech Team", + "llm_id": "6646261c6eb563165658bbb1", + "llm_name": "GPT4o", + "query": "Translate this text into Portuguese: 'This is a test'. Translate to pt and synthesize in audio", + "description": "You are a text translation and speech synthesizing agent. You will be provided a text in the source language and expected to translate and synthesize in the target language.", + "agents": [ + { + "agent_name": "Text Translation agent", + "llm_id": "6646261c6eb563165658bbb1", + "llm_name": "GPT4o", + "description": "## ROLE\nText Translator\n\n## GOAL\nTranslate the text supplied into the users desired language.\n\n## BACKSTORY\nYou are a text translation agent. You will be provided a text in the source language and expected to translate in the target language.", + "tasks": [ + { + "name": "Text translation", + "description": "Translate a text from source language (English) to target language (Portuguese)", + "expected_output": "target language text", + } + ], + "model_tools": [{"function": "translation", "supplier": "AWS"}], + }, + { + "agent_name": "Test Speech Synthesis agent", + "llm_id": "6646261c6eb563165658bbb1", + "llm_name": "GPT4o", + "description": "## ROLE\nSpeech Synthesizer\n\n## GOAL\nTranscribe the translated text into speech.\n\n## BACKSTORY\nYou are a speech synthesizing agent. You will be provided a text to synthesize into audio and return the audio link.", + "tasks": [ + { + "name": "Speech synthesis", + "description": "Synthesize a text from text to speech", + "expected_output": "audio link of the synthesized text", + "dependencies": ["Text translation"], + } + ], + "model_tools": [{"function": "speech_synthesis", "supplier": "Google"}], + }, + ], +} + def parse_tools(tools_info): tools = [] for tool in tools_info: - function_enum = Function[tool['function'].upper().replace(' ', '_')] - supplier_enum = Supplier[tool['supplier'].upper().replace(' ', '_')] - tools.append( - AgentFactory.create_model_tool( - function=function_enum, - supplier=supplier_enum - ) - ) + function_enum = Function[tool["function"].upper().replace(" ", "_")] + supplier_enum = Supplier[tool["supplier"].upper().replace(" ", "_")] + tools.append(AgentFactory.create_model_tool(function=function_enum, supplier=supplier_enum)) return tools @@ -91,7 +76,7 @@ def build_team_agent_from_json(team_config: dict): name=task_name.replace("_", " "), description=task_info.get("description", ""), expected_output=task_info.get("expected_output", ""), - dependencies=[t.replace("_", " ") for t in task_info.get("dependencies", [])] + dependencies=[t.replace("_", " ") for t in task_info.get("dependencies", [])], ) agent_tasks.append(task_obj) @@ -100,13 +85,12 @@ def build_team_agent_from_json(team_config: dict): else: agent_tools = [] - agent_obj = AgentFactory.create( name=agent_name.replace("_", " "), description=agent_description, tools=agent_tools, tasks=agent_tasks, - llm_id=agent_llm_id + llm_id=agent_llm_id, ) agent_objs.append(agent_obj) @@ -116,7 +100,7 @@ def build_team_agent_from_json(team_config: dict): description=team_config["description"], llm_id=team_config.get("llm_id", None), use_inspector=False, - use_mentalist=True + use_mentalist=True, ) @@ -124,9 +108,9 @@ def build_team_agent_from_json(team_config: dict): def team_agent(): return build_team_agent_from_json(team_dict) + def test_evolver_output(team_agent): - query = "Translate this text into Portuguese: 'This is a test'. Translate to pt and synthesize in audio" - response = team_agent.evolve(query) + response = team_agent.evolve() poll_url = response["url"] result = team_agent.poll(poll_url) @@ -134,10 +118,9 @@ def test_evolver_output(team_agent): time.sleep(30) result = team_agent.poll(poll_url) - - assert "system" in result['data']['evolved_agent'].to_dict()['name'].lower(), "System should be in the system name" - assert result['status'] == ResponseStatus.SUCCESS, "Final result should have a 'SUCCESS' status" - assert 'evolved_agent' in result['data'], "Data should contain 'evolved_agent'" - assert 'evaluation_report' in result['data'], "Data should contain 'evaluation_report'" - assert 'criteria' in result['data'], "Data should contain 'criteria'" - assert 'archive' in result['data'], "Data should contain 'archive'" \ No newline at end of file + assert "system" in result["data"]["evolved_agent"]["name"].lower(), "System should be in the system name" + assert result["status"] == ResponseStatus.SUCCESS, "Final result should have a 'SUCCESS' status" + assert "evolved_agent" in result["data"], "Data should contain 'evolved_agent'" + assert "evaluation_report" in result["data"], "Data should contain 'evaluation_report'" + assert "criteria" in result["data"], "Data should contain 'criteria'" + assert "archive" in result["data"], "Data should contain 'archive'" diff --git a/tests/unit/agent/evolve_param_test.py b/tests/unit/agent/evolve_param_test.py new file mode 100644 index 00000000..ad540f8a --- /dev/null +++ b/tests/unit/agent/evolve_param_test.py @@ -0,0 +1,165 @@ +""" +Unit tests for EvolveParam base model functionality +""" + +import pytest +from aixplain.modules.agent.evolve_param import ( + EvolveParam, + EvolveType, + validate_evolve_param, +) + + +class TestEvolveParam: + """Test class for EvolveParam functionality""" + + def test_default_initialization(self): + """Test EvolveParam default initialization""" + default_param = EvolveParam() + + assert default_param is not None + assert default_param.to_evolve is False + assert default_param.criteria is None + assert default_param.max_iterations == 100 + assert default_param.temperature == 0.0 + assert default_param.type == EvolveType.TEAM_TUNING + assert default_param.additional_params == {} + + # Test to_dict method + result_dict = default_param.to_dict() + assert isinstance(result_dict, dict) + assert "toEvolve" in result_dict + + def test_custom_initialization(self): + """Test EvolveParam custom initialization""" + custom_param = EvolveParam( + to_evolve=True, + criteria="accuracy > 0.8", + max_iterations=5, + temperature=0.7, + type=EvolveType.TEAM_TUNING, + additional_params={"customParam": "custom_value"}, + ) + + assert custom_param.to_evolve is True + assert custom_param.criteria == "accuracy > 0.8" + assert custom_param.max_iterations == 5 + assert custom_param.temperature == 0.7 + assert custom_param.type == EvolveType.TEAM_TUNING + assert custom_param.additional_params == {"customParam": "custom_value"} + + # Test to_dict method + result_dict = custom_param.to_dict() + assert result_dict["toEvolve"] is True + assert result_dict["criteria"] == "accuracy > 0.8" + assert result_dict["maxIterations"] == 5 + assert result_dict["temperature"] == 0.7 + assert result_dict["type"] == EvolveType.TEAM_TUNING + assert result_dict["customParam"] == "custom_value" + + def test_from_dict_with_api_format(self): + """Test EvolveParam from_dict() with API format""" + api_dict = { + "toEvolve": True, + "criteria": "custom criteria", + "maxIterations": 10, + "temperature": 0.5, + "type": EvolveType.TEAM_TUNING, + "customParam": "custom_value", + } + + from_dict_param = EvolveParam.from_dict(api_dict) + + assert from_dict_param.to_evolve is True + assert from_dict_param.criteria == "custom criteria" + assert from_dict_param.max_iterations == 10 + assert from_dict_param.temperature == 0.5 + assert from_dict_param.type == EvolveType.TEAM_TUNING + + # Test round-trip conversion + result_dict = from_dict_param.to_dict() + assert result_dict["toEvolve"] is True + assert result_dict["criteria"] == "custom criteria" + assert result_dict["maxIterations"] == 10 + assert result_dict["temperature"] == 0.5 + + def test_validate_evolve_param_with_none(self): + """Test validate_evolve_param() with None input""" + validated_none = validate_evolve_param(None) + + assert validated_none is not None + assert isinstance(validated_none, EvolveParam) + assert validated_none.to_evolve is False + + result_dict = validated_none.to_dict() + assert "toEvolve" in result_dict + + def test_validate_evolve_param_with_dict(self): + """Test validate_evolve_param() with dictionary input""" + input_dict = {"toEvolve": True, "temperature": 0.3} + validated_dict = validate_evolve_param(input_dict) + + assert isinstance(validated_dict, EvolveParam) + assert validated_dict.to_evolve is True + assert validated_dict.temperature == 0.3 + + result_dict = validated_dict.to_dict() + assert result_dict["toEvolve"] is True + assert result_dict["temperature"] == 0.3 + + def test_validate_evolve_param_with_instance(self): + """Test validate_evolve_param() with EvolveParam instance""" + custom_param = EvolveParam( + to_evolve=True, + criteria="accuracy > 0.8", + max_iterations=5, + temperature=0.7, + type=EvolveType.TEAM_TUNING, + additional_params={"customParam": "custom_value"}, + ) + + validated_instance = validate_evolve_param(custom_param) + + assert validated_instance is custom_param # Should return the same instance + assert validated_instance.to_evolve is True + assert validated_instance.criteria == "accuracy > 0.8" + assert validated_instance.max_iterations == 5 + + def test_invalid_temperature_raises_error(self): + """Test that invalid temperature raises ValueError""" + with pytest.raises(ValueError, match="temperature"): + EvolveParam(temperature=1.5) # Temperature > 1.0 should fail + + def test_validate_evolve_param_missing_to_evolve_key(self): + """Test that missing toEvolve key raises ValueError""" + with pytest.raises(ValueError, match="toEvolve"): + validate_evolve_param({"no_to_evolve": True}) # Missing toEvolve key + + def test_evolve_type_enum_values(self): + """Test that EvolveType enum values work correctly""" + param_team_tuning = EvolveParam(type=EvolveType.TEAM_TUNING) + + assert param_team_tuning.type == EvolveType.TEAM_TUNING + + # Test in to_dict conversion + dict_team_tuning = param_team_tuning.to_dict() + + assert "type" in dict_team_tuning + + def test_empty_criteria_handling(self): + """Test that empty criteria is handled properly""" + param = EvolveParam(criteria="") + + assert param.criteria == "" + + result_dict = param.to_dict() + assert result_dict["criteria"] == "" + + def test_none_criteria_handling(self): + """Test that None criteria is handled properly""" + param = EvolveParam(criteria=None) + + assert param.criteria is None + + result_dict = param.to_dict() + assert "criteria" not in result_dict From 6c6ebc66106c41b22e7e99bda599789e739c2b32 Mon Sep 17 00:00:00 2001 From: ahmetgunduz Date: Fri, 13 Jun 2025 17:19:19 +0300 Subject: [PATCH 12/22] evolve_type param corrected --- aixplain/modules/agent/evolve_param.py | 23 ++++++++++++----------- tests/unit/agent/evolve_param_test.py | 20 ++++++++++---------- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/aixplain/modules/agent/evolve_param.py b/aixplain/modules/agent/evolve_param.py index afa86750..1c98a980 100644 --- a/aixplain/modules/agent/evolve_param.py +++ b/aixplain/modules/agent/evolve_param.py @@ -34,14 +34,14 @@ class EvolveParam: criteria (Optional[str]): Custom criteria for evolution evaluation. max_iterations (Optional[int]): Maximum number of evolution iterations. temperature (Optional[float]): Temperature for evolution randomness (0.0-1.0). - type (Optional[EvolveType]): Type of evolution. + evolve_type (Optional[EvolveType]): Type of evolve. """ to_evolve: bool = False criteria: Optional[str] = None max_iterations: Optional[int] = 100 temperature: Optional[float] = 0.0 - type: Optional[EvolveType] = EvolveType.TEAM_TUNING + evolve_type: Optional[EvolveType] = EvolveType.TEAM_TUNING additional_params: Optional[Dict[str, Any]] = field(default_factory=dict) def __post_init__(self): @@ -66,9 +66,9 @@ def validate(self) -> None: if self.max_iterations <= 0: raise ValueError("max_iterations must be positive") - if self.type is not None: - if not isinstance(self.type, EvolveType): - raise ValueError("type must be a valid EvolveType") + if self.evolve_type is not None: + if not isinstance(self.evolve_type, EvolveType): + raise ValueError("evolve_type must be a valid EvolveType") if self.additional_params is not None: if not isinstance(self.additional_params, dict): raise ValueError("additional_params must be a dictionary") @@ -98,7 +98,7 @@ def from_dict(cls, data: Union[Dict[str, Any], None]) -> "EvolveParam": "criteria": data.get("criteria"), "max_iterations": data.get("maxIterations", data.get("max_iterations")), "temperature": data.get("temperature"), - "type": data.get("type"), + "evolve_type": data.get("evolveType", data.get("evolve_type")), "additional_params": data.get("additional_params"), } @@ -117,7 +117,8 @@ def from_dict(cls, data: Union[Dict[str, Any], None]) -> "EvolveParam": "maxIterations", "max_iterations", "temperature", - "type", + "evolveType", + "evolve_type", "additional_params", ] } @@ -141,8 +142,8 @@ def to_dict(self) -> Dict[str, Any]: result["maxIterations"] = self.max_iterations if self.temperature is not None: result["temperature"] = self.temperature - if self.type is not None: - result["type"] = self.type + if self.evolve_type is not None: + result["evolve_type"] = self.evolve_type if self.additional_params is not None: result.update(self.additional_params) @@ -170,7 +171,7 @@ def merge(self, other: Union[Dict[str, Any], "EvolveParam"]) -> "EvolveParam": criteria=other.criteria if other.criteria is not None else self.criteria, max_iterations=(other.max_iterations if other.max_iterations is not None else self.max_iterations), temperature=(other.temperature if other.temperature is not None else self.temperature), - type=(other.type if other.type is not None else self.type), + evolve_type=(other.evolve_type if other.evolve_type is not None else self.evolve_type), additional_params=merged_additional, ) @@ -181,7 +182,7 @@ def __repr__(self) -> str: f"criteria={self.criteria}, " f"max_iterations={self.max_iterations}, " f"temperature={self.temperature}, " - f"type={self.type}, " + f"evolve_type={self.evolve_type}, " f"additional_params={self.additional_params})" ) diff --git a/tests/unit/agent/evolve_param_test.py b/tests/unit/agent/evolve_param_test.py index ad540f8a..d96eadcc 100644 --- a/tests/unit/agent/evolve_param_test.py +++ b/tests/unit/agent/evolve_param_test.py @@ -22,7 +22,7 @@ def test_default_initialization(self): assert default_param.criteria is None assert default_param.max_iterations == 100 assert default_param.temperature == 0.0 - assert default_param.type == EvolveType.TEAM_TUNING + assert default_param.evolve_type == EvolveType.TEAM_TUNING assert default_param.additional_params == {} # Test to_dict method @@ -37,7 +37,7 @@ def test_custom_initialization(self): criteria="accuracy > 0.8", max_iterations=5, temperature=0.7, - type=EvolveType.TEAM_TUNING, + evolve_type=EvolveType.TEAM_TUNING, additional_params={"customParam": "custom_value"}, ) @@ -45,7 +45,7 @@ def test_custom_initialization(self): assert custom_param.criteria == "accuracy > 0.8" assert custom_param.max_iterations == 5 assert custom_param.temperature == 0.7 - assert custom_param.type == EvolveType.TEAM_TUNING + assert custom_param.evolve_type == EvolveType.TEAM_TUNING assert custom_param.additional_params == {"customParam": "custom_value"} # Test to_dict method @@ -54,7 +54,7 @@ def test_custom_initialization(self): assert result_dict["criteria"] == "accuracy > 0.8" assert result_dict["maxIterations"] == 5 assert result_dict["temperature"] == 0.7 - assert result_dict["type"] == EvolveType.TEAM_TUNING + assert result_dict["evolve_type"] == EvolveType.TEAM_TUNING assert result_dict["customParam"] == "custom_value" def test_from_dict_with_api_format(self): @@ -64,7 +64,7 @@ def test_from_dict_with_api_format(self): "criteria": "custom criteria", "maxIterations": 10, "temperature": 0.5, - "type": EvolveType.TEAM_TUNING, + "evolve_type": EvolveType.TEAM_TUNING, "customParam": "custom_value", } @@ -74,7 +74,7 @@ def test_from_dict_with_api_format(self): assert from_dict_param.criteria == "custom criteria" assert from_dict_param.max_iterations == 10 assert from_dict_param.temperature == 0.5 - assert from_dict_param.type == EvolveType.TEAM_TUNING + assert from_dict_param.evolve_type == EvolveType.TEAM_TUNING # Test round-trip conversion result_dict = from_dict_param.to_dict() @@ -114,7 +114,7 @@ def test_validate_evolve_param_with_instance(self): criteria="accuracy > 0.8", max_iterations=5, temperature=0.7, - type=EvolveType.TEAM_TUNING, + evolve_type=EvolveType.TEAM_TUNING, additional_params={"customParam": "custom_value"}, ) @@ -137,14 +137,14 @@ def test_validate_evolve_param_missing_to_evolve_key(self): def test_evolve_type_enum_values(self): """Test that EvolveType enum values work correctly""" - param_team_tuning = EvolveParam(type=EvolveType.TEAM_TUNING) + param_team_tuning = EvolveParam(evolve_type=EvolveType.TEAM_TUNING) - assert param_team_tuning.type == EvolveType.TEAM_TUNING + assert param_team_tuning.evolve_type == EvolveType.TEAM_TUNING # Test in to_dict conversion dict_team_tuning = param_team_tuning.to_dict() - assert "type" in dict_team_tuning + assert "evolve_type" in dict_team_tuning def test_empty_criteria_handling(self): """Test that empty criteria is handled properly""" From 26469673f166ad81424bb7c139d06139afdb4309 Mon Sep 17 00:00:00 2001 From: ahmetgunduz Date: Fri, 20 Jun 2025 13:26:37 +0300 Subject: [PATCH 13/22] Implement async and sync evolve functionality - Refactor EvolveParam to use new evolution parameters - Remove criteria, temperature, max_iterations - Add max_generations, max_retries, recursion_limit, max_iterations_without_improvement - Add evolve_async and evolve methods to Agent and TeamAgent - Add evolve_utils.py for YAML parsing and agent creation - Update tests to match new EvolveParam implementation This change introduces proper evolution functionality for both Agent and TeamAgent classes with better parameter control and async/sync options. --- aixplain/modules/agent/__init__.py | 76 ++++++++++- aixplain/modules/agent/evolve_param.py | 101 ++++++++------ aixplain/modules/team_agent/__init__.py | 77 ++++++++++- aixplain/utils/evolve_utils.py | 172 ++++++++++++++++++++++++ tests/unit/agent/evolve_param_test.py | 109 ++++++++------- 5 files changed, 440 insertions(+), 95 deletions(-) create mode 100644 aixplain/utils/evolve_utils.py diff --git a/aixplain/modules/agent/__init__.py b/aixplain/modules/agent/__init__.py index a61ab842..c260161c 100644 --- a/aixplain/modules/agent/__init__.py +++ b/aixplain/modules/agent/__init__.py @@ -489,17 +489,17 @@ def save(self) -> None: def __repr__(self): return f"Agent: {self.name} (id={self.id})" - def evolve(self, evolve_parameters: Union[Dict[str, Any], EvolveParam, None] = None) -> None: - """Evolve the Agent. + def evolve_async(self, evolve_parameters: Union[Dict[str, Any], EvolveParam, None] = None) -> AgentResponse: + """Asynchronously evolve the Agent and return a polling URL in the AgentResponse. Args: evolve_parameters (Union[Dict[str, Any], EvolveParam, None]): Evolution parameters. Can be a dictionary, EvolveParam instance, or None. Returns: - AgentResponse: Response from the evolution process. + AgentResponse: Response containing polling URL and status. """ - query = "Placeholder query" + query = "" if evolve_parameters is None: evolve_parameters = EvolveParam(to_evolve=True) elif isinstance(evolve_parameters, dict): @@ -511,3 +511,71 @@ def evolve(self, evolve_parameters: Union[Dict[str, Any], EvolveParam, None] = N raise ValueError("evolve_parameters must be a dictionary, EvolveParam instance, or None") return self.run_async(query=query, evolve=evolve_parameters) + + def evolve( + self, + evolve_parameters: Union[Dict[str, Any], EvolveParam, None] = None, + ) -> AgentResponse: + """Synchronously evolve the Agent and poll for the result. + + Args: + evolve_parameters (Union[Dict[str, Any], EvolveParam, None]): Evolution parameters. + Can be a dictionary, EvolveParam instance, or None. + + Returns: + AgentResponse: Final response from the evolution process. + """ + from aixplain.utils.evolve_utils import from_yaml + from aixplain.enums import EvolveType + + if evolve_parameters is None: + evolve_parameters = EvolveParam(to_evolve=True) + elif isinstance(evolve_parameters, dict): + evolve_parameters = EvolveParam.from_dict(evolve_parameters) + evolve_parameters.to_evolve = True + elif isinstance(evolve_parameters, EvolveParam): + evolve_parameters.to_evolve = True + else: + raise ValueError("evolve_parameters must be a dictionary, EvolveParam instance, or None") + + start = time.time() + try: + response = self.evolve_async(evolve_parameters) + if response["status"] == ResponseStatus.FAILED: + end = time.time() + response["elapsed_time"] = end - start + return response + poll_url = response["url"] + end = time.time() + result = self.sync_poll(poll_url, name="evolve_process", timeout=600) + result_data = result.data + + if "current_code" in result_data and result_data["current_code"] is not None: + if evolve_parameters.evolve_type == EvolveType.TEAM_TUNING: + result_data["evolved_agent"] = from_yaml( + result_data["current_code"], + self.llm_id, + ) + elif evolve_parameters.evolve_type == EvolveType.INSTRUCTION_TUNING: + self.instructions = result_data["current_code"] + self.update() + result_data["evolved_agent"] = self + else: + raise ValueError( + "evolve_parameters.evolve_type must be one of the following: TEAM_TUNING, INSTRUCTION_TUNING" + ) + return AgentResponse( + status=ResponseStatus.SUCCESS, + completed=True, + data=result_data, + used_credits=getattr(result, "used_credits", 0.0), + run_time=getattr(result, "run_time", end - start), + ) + except Exception as e: + logging.error(f"Agent Evolve: Error in evolving: {e}") + end = time.time() + return AgentResponse( + status=ResponseStatus.FAILED, + completed=False, + error_message="No response from the service.", + ) diff --git a/aixplain/modules/agent/evolve_param.py b/aixplain/modules/agent/evolve_param.py index 1c98a980..73a9f0d8 100644 --- a/aixplain/modules/agent/evolve_param.py +++ b/aixplain/modules/agent/evolve_param.py @@ -31,17 +31,20 @@ class EvolveParam: Attributes: to_evolve (bool): Whether to enable evolution. Defaults to False. - criteria (Optional[str]): Custom criteria for evolution evaluation. - max_iterations (Optional[int]): Maximum number of evolution iterations. - temperature (Optional[float]): Temperature for evolution randomness (0.0-1.0). evolve_type (Optional[EvolveType]): Type of evolve. + max_generations (int): Maximum number of generations. + max_retries (int): Maximum number of retries. + recursion_limit (int): Maximum number of recursion. + max_iterations_without_improvement (int): Maximum number of iterations without improvement. + additional_params (Optional[Dict[str, Any]]): Additional parameters. """ to_evolve: bool = False - criteria: Optional[str] = None - max_iterations: Optional[int] = 100 - temperature: Optional[float] = 0.0 evolve_type: Optional[EvolveType] = EvolveType.TEAM_TUNING + max_generations: int = 3 + max_retries: int = 3 + recursion_limit: int = 50 + max_iterations_without_improvement: int = 3 additional_params: Optional[Dict[str, Any]] = field(default_factory=dict) def __post_init__(self): @@ -54,18 +57,6 @@ def validate(self) -> None: Raises: ValueError: If any parameter is invalid. """ - if self.temperature is not None: - if not isinstance(self.temperature, (int, float)): - raise ValueError("temperature must be a number") - if not 0.0 <= self.temperature <= 1.0: - raise ValueError("temperature must be between 0.0 and 1.0") - - if self.max_iterations is not None: - if not isinstance(self.max_iterations, int): - raise ValueError("max_iterations must be an integer") - if self.max_iterations <= 0: - raise ValueError("max_iterations must be positive") - if self.evolve_type is not None: if not isinstance(self.evolve_type, EvolveType): raise ValueError("evolve_type must be a valid EvolveType") @@ -73,6 +64,30 @@ def validate(self) -> None: if not isinstance(self.additional_params, dict): raise ValueError("additional_params must be a dictionary") + if self.max_generations is not None: + if not isinstance(self.max_generations, int): + raise ValueError("max_generations must be an integer") + if self.max_generations <= 0: + raise ValueError("max_generations must be positive") + + if self.max_retries is not None: + if not isinstance(self.max_retries, int): + raise ValueError("max_retries must be an integer") + if self.max_retries <= 0: + raise ValueError("max_retries must be positive") + + if self.recursion_limit is not None: + if not isinstance(self.recursion_limit, int): + raise ValueError("recursion_limit must be an integer") + if self.recursion_limit <= 0: + raise ValueError("recursion_limit must be positive") + + if self.max_iterations_without_improvement is not None: + if not isinstance(self.max_iterations_without_improvement, int): + raise ValueError("max_iterations_without_improvement must be an integer") + if self.max_iterations_without_improvement <= 0: + raise ValueError("max_iterations_without_improvement must be positive") + @classmethod def from_dict(cls, data: Union[Dict[str, Any], None]) -> "EvolveParam": """Create EvolveParam instance from dictionary. @@ -95,10 +110,11 @@ def from_dict(cls, data: Union[Dict[str, Any], None]) -> "EvolveParam": # Extract known parameters known_params = { "to_evolve": data.get("toEvolve", data.get("to_evolve", False)), - "criteria": data.get("criteria"), - "max_iterations": data.get("maxIterations", data.get("max_iterations")), - "temperature": data.get("temperature"), - "evolve_type": data.get("evolveType", data.get("evolve_type")), + "evolve_type": data.get("evolve_type"), + "max_generations": data.get("max_generations"), + "max_retries": data.get("max_retries"), + "recursion_limit": data.get("recursion_limit"), + "max_iterations_without_improvement": data.get("max_iterations_without_improvement"), "additional_params": data.get("additional_params"), } @@ -113,12 +129,11 @@ def from_dict(cls, data: Union[Dict[str, Any], None]) -> "EvolveParam": not in [ "toEvolve", "to_evolve", - "criteria", - "maxIterations", - "max_iterations", - "temperature", - "evolveType", "evolve_type", + "max_generations", + "max_retries", + "recursion_limit", + "max_iterations_without_improvement", "additional_params", ] } @@ -136,14 +151,16 @@ def to_dict(self) -> Dict[str, Any]: } # Add optional parameters if they are set - if self.criteria is not None: - result["criteria"] = self.criteria - if self.max_iterations is not None: - result["maxIterations"] = self.max_iterations - if self.temperature is not None: - result["temperature"] = self.temperature if self.evolve_type is not None: result["evolve_type"] = self.evolve_type + if self.max_generations is not None: + result["max_generations"] = self.max_generations + if self.max_retries is not None: + result["max_retries"] = self.max_retries + if self.recursion_limit is not None: + result["recursion_limit"] = self.recursion_limit + if self.max_iterations_without_improvement is not None: + result["max_iterations_without_improvement"] = self.max_iterations_without_improvement if self.additional_params is not None: result.update(self.additional_params) @@ -168,10 +185,15 @@ def merge(self, other: Union[Dict[str, Any], "EvolveParam"]) -> "EvolveParam": return EvolveParam( to_evolve=other.to_evolve if other.to_evolve else self.to_evolve, - criteria=other.criteria if other.criteria is not None else self.criteria, - max_iterations=(other.max_iterations if other.max_iterations is not None else self.max_iterations), - temperature=(other.temperature if other.temperature is not None else self.temperature), evolve_type=(other.evolve_type if other.evolve_type is not None else self.evolve_type), + max_generations=(other.max_generations if other.max_generations is not None else self.max_generations), + max_retries=(other.max_retries if other.max_retries is not None else self.max_retries), + recursion_limit=(other.recursion_limit if other.recursion_limit is not None else self.recursion_limit), + max_iterations_without_improvement=( + other.max_iterations_without_improvement + if other.max_iterations_without_improvement is not None + else self.max_iterations_without_improvement + ), additional_params=merged_additional, ) @@ -179,10 +201,11 @@ def __repr__(self) -> str: return ( f"EvolveParam(" f"to_evolve={self.to_evolve}, " - f"criteria={self.criteria}, " - f"max_iterations={self.max_iterations}, " - f"temperature={self.temperature}, " f"evolve_type={self.evolve_type}, " + f"max_generations={self.max_generations}, " + f"max_retries={self.max_retries}, " + f"recursion_limit={self.recursion_limit}, " + f"max_iterations_without_improvement={self.max_iterations_without_improvement}, " f"additional_params={self.additional_params})" ) diff --git a/aixplain/modules/team_agent/__init__.py b/aixplain/modules/team_agent/__init__.py index ab2a3d2d..734b2694 100644 --- a/aixplain/modules/team_agent/__init__.py +++ b/aixplain/modules/team_agent/__init__.py @@ -487,17 +487,17 @@ def update(self) -> None: def __repr__(self): return f"TeamAgent: {self.name} (id={self.id})" - def evolve(self, evolve_parameters: Union[Dict[str, Any], EvolveParam, None] = None) -> None: - """Evolve the Team Agent. + def evolve_async(self, evolve_parameters: Union[Dict[str, Any], EvolveParam, None] = None) -> AgentResponse: + """Asynchronously evolve the Team Agent and return a polling URL in the AgentResponse. Args: evolve_parameters (Union[Dict[str, Any], EvolveParam, None]): Evolution parameters. Can be a dictionary, EvolveParam instance, or None. Returns: - AgentResponse: Response from the evolution process. + AgentResponse: Response containing polling URL and status. """ - query = "Placeholder query" + query = "" if evolve_parameters is None: evolve_parameters = EvolveParam(to_evolve=True) elif isinstance(evolve_parameters, dict): @@ -511,3 +511,72 @@ def evolve(self, evolve_parameters: Union[Dict[str, Any], EvolveParam, None] = N response = self.run_async(query=query, evolve=evolve_parameters) return response + def evolve( + self, + evolve_parameters: Union[Dict[str, Any], EvolveParam, None] = None, + ) -> AgentResponse: + """Synchronously evolve the Team Agent and poll for the result. + + Args: + evolve_parameters (Union[Dict[str, Any], EvolveParam, None]): Evolution parameters. + Can be a dictionary, EvolveParam instance, or None. + name (str, optional): Name for the process. Defaults to "evolve_process". + + Returns: + AgentResponse: Final response from the evolution process. + """ + from aixplain.enums import EvolveType + from aixplain.utils.evolve_utils import from_yaml + + if evolve_parameters is None: + evolve_parameters = EvolveParam(to_evolve=True) + elif isinstance(evolve_parameters, dict): + evolve_parameters = EvolveParam.from_dict(evolve_parameters) + evolve_parameters.to_evolve = True + elif isinstance(evolve_parameters, EvolveParam): + evolve_parameters.to_evolve = True + else: + raise ValueError("evolve_parameters must be a dictionary, EvolveParam instance, or None") + start = time.time() + try: + print("Evolve started with parameters: ", evolve_parameters) + print("It might take a while...") + response = self.evolve_async(evolve_parameters) + if response["status"] == ResponseStatus.FAILED: + end = time.time() + response["elapsed_time"] = end - start + return response + poll_url = response["url"] + end = time.time() + result = self.sync_poll(poll_url, name="evolve_process", timeout=600) + result_data = result.data + + if "current_code" in result_data and result_data["current_code"] is not None: + if evolve_parameters.evolve_type == EvolveType.TEAM_TUNING: + result_data["evolved_agent"] = from_yaml( + result_data["current_code"], + self.llm_id, + ) + elif evolve_parameters.evolve_type == EvolveType.INSTRUCTION_TUNING: + self.instructions = result_data["current_code"] + self.update() + result_data["evolved_agent"] = self + else: + raise ValueError( + "evolve_parameters.evolve_type must be one of the following: TEAM_TUNING, INSTRUCTION_TUNING" + ) + return AgentResponse( + status=ResponseStatus.SUCCESS, + completed=True, + data=result_data, + used_credits=getattr(result, "used_credits", 0.0), + run_time=getattr(result, "run_time", end - start), + ) + except Exception as e: + logging.error(f"Team Agent Evolve: Error in evolving: {e}") + end = time.time() + return AgentResponse( + status=ResponseStatus.FAILED, + completed=False, + error_message="No response from the service.", + ) diff --git a/aixplain/utils/evolve_utils.py b/aixplain/utils/evolve_utils.py new file mode 100644 index 00000000..966f1bed --- /dev/null +++ b/aixplain/utils/evolve_utils.py @@ -0,0 +1,172 @@ +import yaml +from typing import Union +from aixplain.enums import Function, Supplier +from aixplain.modules.agent import Agent +from aixplain.modules.team_agent import TeamAgent +from aixplain.factories import AgentFactory, TeamAgentFactory + + +def parse_tools(tools: list) -> list: + obj_tools = [] + for tool in tools: + if tool.strip() == "translation": + obj_tools.append( + AgentFactory.create_model_tool( + function=Function.TRANSLATION, + supplier=Supplier.GOOGLE, + ) + ) + elif tool.strip() == "speech-recognition": + obj_tools.append( + AgentFactory.create_model_tool( + function=Function.SPEECH_RECOGNITION, + supplier=Supplier.GOOGLE, + ) + ) + elif tool.strip() == "text-to-speech": + obj_tools.append( + AgentFactory.create_model_tool( + function=Function.SPEECH_SYNTHESIS, + supplier=Supplier.GOOGLE, + ) + ) + elif tool.strip() == "serper_search": + obj_tools.append(AgentFactory.create_model_tool(model="65c51c556eb563350f6e1bb1")) + elif tool.strip() == "website_search": + obj_tools.append(AgentFactory.create_model_tool(model="6736411cf127849667606689")) + elif tool.strip() == "website_scrape": + obj_tools.append(AgentFactory.create_model_tool(model="6748e4746eb5633559668a15")) + else: + continue + return obj_tools + + +def from_yaml( + yaml_code: str, + llm_id: str, +) -> Union[TeamAgent, Agent]: + team_config = yaml.safe_load(yaml_code) + + agents_data = team_config["agents"] + tasks_data = team_config.get("tasks", []) + system_data = team_config["system"] if "system" in team_config else {"query": ""} + expected_output = system_data.get("expected_output") + team_name = system_data.get("name") + team_description = system_data.get("description", "") + team_instructions = system_data.get("instructions", "") + + # Create agent mapping by name for easier task assignment + agents_mapping = {} + agent_objs = [] + + # Parse agents + for agent_entry in agents_data: + # Handle different YAML structures + if isinstance(agent_entry, dict): + # Case 1: Standard structure - {agent_name: {role: ..., goal: ..., backstory: ...}} + for agent_name, agent_info in agent_entry.items(): + # Check if agent_info is a list (malformed YAML structure) + if isinstance(agent_info, list): + # Handle malformed structure where agent_info is a list + # This happens when YAML has unquoted keys that create nested structures + continue + elif isinstance(agent_info, dict): + agent_role = agent_info["role"] + agent_goal = agent_info["goal"] + agent_backstory = agent_info["backstory"] + + description = f"## ROLE\n{agent_role}\n\n## GOAL\n{agent_goal}\n\n## BACKSTORY\n{agent_backstory}" + agent_obj = AgentFactory.create( + name=agent_name.replace("_", " "), + description=description, + tasks=[], # Tasks will be assigned later + tools=parse_tools(agent_info.get("tools", [])), + llm_id=llm_id, + ) + agents_mapping[agent_name] = agent_obj + agent_objs.append(agent_obj) + elif isinstance(agent_entry, list): + # Case 2: Handle list structure (alternative YAML format) + for item in agent_entry: + if isinstance(item, dict): + for agent_name, agent_info in item.items(): + if isinstance(agent_info, dict): + agent_role = agent_info["role"] + agent_goal = agent_info["goal"] + agent_backstory = agent_info["backstory"] + + description = f"## ROLE\n{agent_role}\n\n## GOAL\n{agent_goal}\n\n## BACKSTORY\n{agent_backstory}" + agent_tools = parse_tools(agent_info.get("tools", [])) + agent_obj = AgentFactory.create( + name=agent_name.replace("_", " "), + description=description, + tools=agent_tools, + tasks=[], + llm_id=llm_id, + ) + agents_mapping[agent_name] = agent_obj + agent_objs.append(agent_obj) + + # Parse tasks and assign them to the corresponding agents + for task_entry in tasks_data: + # Handle different YAML structures for tasks + if isinstance(task_entry, dict): + # Case 1: Standard structure - {task_name: {description: ..., expected_output: ..., agent: ...}} + for task_name, task_info in task_entry.items(): + # Check if task_info is a list (malformed YAML structure) + if isinstance(task_info, list): + # Handle malformed structure where task_info is a list + continue + elif isinstance(task_info, dict): + description = task_info["description"] + expected_output = task_info["expected_output"] + dependencies = task_info.get("dependencies", []) + agent_name = task_info["agent"] + task_obj = AgentFactory.create_task( + name=task_name.replace("_", " "), + description=description, + expected_output=expected_output, + dependencies=dependencies, + ) + + # Assign the task to the corresponding agent + if agent_name in agents_mapping: + agent = agents_mapping[agent_name] + agent.tasks.append(task_obj) + else: + raise ValueError(f"Agent '{agent_name}' referenced in tasks not found.") + elif isinstance(task_entry, list): + # Case 2: Handle list structure (alternative YAML format) + for item in task_entry: + if isinstance(item, dict): + for task_name, task_info in item.items(): + if isinstance(task_info, dict): + description = task_info["description"] + expected_output = task_info["expected_output"] + dependencies = task_info.get("dependencies", []) + agent_name = task_info["agent"] + + task_obj = AgentFactory.create_task( + name=task_name.replace("_", " "), + description=description, + expected_output=expected_output, + dependencies=dependencies, + ) + + # Assign the task to the corresponding agent + if agent_name in agents_mapping: + agent = agents_mapping[agent_name] + agent.tasks.append(task_obj) + else: + raise ValueError(f"Agent '{agent_name}' referenced in tasks not found.") + + team = TeamAgentFactory.create( + name=team_name, + description=team_description, + instructions=team_instructions, + agents=agent_objs, + llm_id=llm_id, + use_mentalist=True, + inspectors=[], + ) + return team diff --git a/tests/unit/agent/evolve_param_test.py b/tests/unit/agent/evolve_param_test.py index d96eadcc..7068320a 100644 --- a/tests/unit/agent/evolve_param_test.py +++ b/tests/unit/agent/evolve_param_test.py @@ -19,10 +19,11 @@ def test_default_initialization(self): assert default_param is not None assert default_param.to_evolve is False - assert default_param.criteria is None - assert default_param.max_iterations == 100 - assert default_param.temperature == 0.0 assert default_param.evolve_type == EvolveType.TEAM_TUNING + assert default_param.max_generations == 3 + assert default_param.max_retries == 3 + assert default_param.recursion_limit == 50 + assert default_param.max_iterations_without_improvement == 3 assert default_param.additional_params == {} # Test to_dict method @@ -34,26 +35,29 @@ def test_custom_initialization(self): """Test EvolveParam custom initialization""" custom_param = EvolveParam( to_evolve=True, - criteria="accuracy > 0.8", - max_iterations=5, - temperature=0.7, + max_generations=5, + max_retries=2, + recursion_limit=30, + max_iterations_without_improvement=4, evolve_type=EvolveType.TEAM_TUNING, additional_params={"customParam": "custom_value"}, ) assert custom_param.to_evolve is True - assert custom_param.criteria == "accuracy > 0.8" - assert custom_param.max_iterations == 5 - assert custom_param.temperature == 0.7 + assert custom_param.max_generations == 5 + assert custom_param.max_retries == 2 + assert custom_param.recursion_limit == 30 + assert custom_param.max_iterations_without_improvement == 4 assert custom_param.evolve_type == EvolveType.TEAM_TUNING assert custom_param.additional_params == {"customParam": "custom_value"} # Test to_dict method result_dict = custom_param.to_dict() assert result_dict["toEvolve"] is True - assert result_dict["criteria"] == "accuracy > 0.8" - assert result_dict["maxIterations"] == 5 - assert result_dict["temperature"] == 0.7 + assert result_dict["max_generations"] == 5 + assert result_dict["max_retries"] == 2 + assert result_dict["recursion_limit"] == 30 + assert result_dict["max_iterations_without_improvement"] == 4 assert result_dict["evolve_type"] == EvolveType.TEAM_TUNING assert result_dict["customParam"] == "custom_value" @@ -61,9 +65,10 @@ def test_from_dict_with_api_format(self): """Test EvolveParam from_dict() with API format""" api_dict = { "toEvolve": True, - "criteria": "custom criteria", - "maxIterations": 10, - "temperature": 0.5, + "max_generations": 10, + "max_retries": 4, + "recursion_limit": 40, + "max_iterations_without_improvement": 5, "evolve_type": EvolveType.TEAM_TUNING, "customParam": "custom_value", } @@ -71,17 +76,19 @@ def test_from_dict_with_api_format(self): from_dict_param = EvolveParam.from_dict(api_dict) assert from_dict_param.to_evolve is True - assert from_dict_param.criteria == "custom criteria" - assert from_dict_param.max_iterations == 10 - assert from_dict_param.temperature == 0.5 + assert from_dict_param.max_generations == 10 + assert from_dict_param.max_retries == 4 + assert from_dict_param.recursion_limit == 40 + assert from_dict_param.max_iterations_without_improvement == 5 assert from_dict_param.evolve_type == EvolveType.TEAM_TUNING # Test round-trip conversion result_dict = from_dict_param.to_dict() assert result_dict["toEvolve"] is True - assert result_dict["criteria"] == "custom criteria" - assert result_dict["maxIterations"] == 10 - assert result_dict["temperature"] == 0.5 + assert result_dict["max_generations"] == 10 + assert result_dict["max_retries"] == 4 + assert result_dict["recursion_limit"] == 40 + assert result_dict["max_iterations_without_improvement"] == 5 def test_validate_evolve_param_with_none(self): """Test validate_evolve_param() with None input""" @@ -96,24 +103,25 @@ def test_validate_evolve_param_with_none(self): def test_validate_evolve_param_with_dict(self): """Test validate_evolve_param() with dictionary input""" - input_dict = {"toEvolve": True, "temperature": 0.3} + input_dict = {"toEvolve": True, "max_generations": 5} validated_dict = validate_evolve_param(input_dict) assert isinstance(validated_dict, EvolveParam) assert validated_dict.to_evolve is True - assert validated_dict.temperature == 0.3 + assert validated_dict.max_generations == 5 result_dict = validated_dict.to_dict() assert result_dict["toEvolve"] is True - assert result_dict["temperature"] == 0.3 + assert result_dict["max_generations"] == 5 def test_validate_evolve_param_with_instance(self): """Test validate_evolve_param() with EvolveParam instance""" custom_param = EvolveParam( to_evolve=True, - criteria="accuracy > 0.8", - max_iterations=5, - temperature=0.7, + max_generations=5, + max_retries=2, + recursion_limit=30, + max_iterations_without_improvement=4, evolve_type=EvolveType.TEAM_TUNING, additional_params={"customParam": "custom_value"}, ) @@ -122,17 +130,17 @@ def test_validate_evolve_param_with_instance(self): assert validated_instance is custom_param # Should return the same instance assert validated_instance.to_evolve is True - assert validated_instance.criteria == "accuracy > 0.8" - assert validated_instance.max_iterations == 5 + assert validated_instance.max_generations == 5 + assert validated_instance.max_retries == 2 - def test_invalid_temperature_raises_error(self): - """Test that invalid temperature raises ValueError""" - with pytest.raises(ValueError, match="temperature"): - EvolveParam(temperature=1.5) # Temperature > 1.0 should fail + def test_invalid_max_generations_raises_error(self): + """Test that invalid max_generations raises ValueError""" + with pytest.raises(ValueError, match="max_generations must be positive"): + EvolveParam(max_generations=0) # max_generations <= 0 should fail def test_validate_evolve_param_missing_to_evolve_key(self): """Test that missing toEvolve key raises ValueError""" - with pytest.raises(ValueError, match="toEvolve"): + with pytest.raises(ValueError, match="evolve parameter must contain 'toEvolve' key"): validate_evolve_param({"no_to_evolve": True}) # Missing toEvolve key def test_evolve_type_enum_values(self): @@ -146,20 +154,25 @@ def test_evolve_type_enum_values(self): assert "evolve_type" in dict_team_tuning - def test_empty_criteria_handling(self): - """Test that empty criteria is handled properly""" - param = EvolveParam(criteria="") + def test_invalid_additional_params_type(self): + """Test that invalid additional_params type raises ValueError""" + with pytest.raises(ValueError, match="additional_params must be a dictionary"): + EvolveParam(additional_params="not a dict") - assert param.criteria == "" - - result_dict = param.to_dict() - assert result_dict["criteria"] == "" - - def test_none_criteria_handling(self): - """Test that None criteria is handled properly""" - param = EvolveParam(criteria=None) + def test_merge_with_dict(self): + """Test merging with a dictionary""" + base_param = EvolveParam(to_evolve=False, max_generations=3, additional_params={"base": "value"}) + merge_dict = { + "toEvolve": True, + "max_generations": 5, + "customParam": "custom_value", + } - assert param.criteria is None + merged = base_param.merge(merge_dict) - result_dict = param.to_dict() - assert "criteria" not in result_dict + assert merged.to_evolve is True + assert merged.max_generations == 5 + assert merged.additional_params == { + "base": "value", + "customParam": "custom_value", + } From 838e31dd59ab0dfd45a76cd84f88e2b9b0c5a466 Mon Sep 17 00:00:00 2001 From: ahmetgunduz Date: Tue, 24 Jun 2025 15:55:38 +0300 Subject: [PATCH 14/22] updates of the coments --- .../factories/team_agent_factory/utils.py | 59 +++++-------------- aixplain/modules/team_agent/__init__.py | 8 +-- aixplain/utils/evolve_utils.py | 19 +++--- 3 files changed, 30 insertions(+), 56 deletions(-) diff --git a/aixplain/factories/team_agent_factory/utils.py b/aixplain/factories/team_agent_factory/utils.py index cb29f9fe..e8e606e8 100644 --- a/aixplain/factories/team_agent_factory/utils.py +++ b/aixplain/factories/team_agent_factory/utils.py @@ -19,9 +19,7 @@ GPT_4o_ID = "6646261c6eb563165658bbb1" -def build_team_agent( - payload: Dict, agents: List[Agent] = None, api_key: Text = config.TEAM_API_KEY -) -> TeamAgent: +def build_team_agent(payload: Dict, agents: List[Agent] = None, api_key: Text = config.TEAM_API_KEY) -> TeamAgent: """Instantiate a new team agent in the platform.""" agents_dict = payload["agents"] payload_agents = agents @@ -39,13 +37,9 @@ def build_team_agent( # Ensure custom classes are instantiated: for compatibility with backend return format inspectors = [ - inspector if isinstance(inspector, Inspector) else Inspector(**inspector) - for inspector in payload.get("inspectors", []) - ] - inspector_targets = [ - InspectorTarget(target.lower()) - for target in payload.get("inspectorTargets", []) + inspector if isinstance(inspector, Inspector) else Inspector(**inspector) for inspector in payload.get("inspectors", []) ] + inspector_targets = [InspectorTarget(target.lower()) for target in payload.get("inspectorTargets", [])] # Get LLMs from tools if present supervisor_llm = None @@ -106,9 +100,7 @@ def build_team_agent( api_key=api_key, status=AssetStatus(payload["status"]), ) - team_agent.url = urljoin( - config.BACKEND_URL, f"sdk/agent-communities/{team_agent.id}/run" - ) + team_agent.url = urljoin(config.BACKEND_URL, f"sdk/agent-communities/{team_agent.id}/run") # fill up dependencies all_tasks = {} @@ -122,9 +114,7 @@ def build_team_agent( if isinstance(dependency, Text): task_dependency = all_tasks.get(dependency, None) if task_dependency: - team_agent.agents[idx].tasks[i].dependencies[ - j - ] = task_dependency + team_agent.agents[idx].tasks[i].dependencies[j] = task_dependency else: team_agent.agents[idx].tasks[i].dependencies[j] = None @@ -134,21 +124,22 @@ def build_team_agent( def parse_tool_from_yaml(tool: str) -> ModelTool: from aixplain.enums import Function - if tool.strip() == "translation": + tool_name = tool.strip() + if tool_name == "translation": return ModelTool( function=Function.TRANSLATION, ) - elif tool.strip() == "speech-recognition": + elif tool_name == "speech-recognition": return ModelTool( function=Function.SPEECH_RECOGNITION, ) - elif tool.strip() == "text-to-speech": + elif tool_name == "text-to-speech": return ModelTool( function=Function.SPEECH_SYNTHESIS, ) - elif tool.strip() == "llm": + elif tool_name == "llm": return ModelTool(function=Function.TEXT_GENERATION) - elif tool.strip() == "serper_search": + elif tool_name == "serper_search": return ModelTool(model="65c51c556eb563350f6e1bb1") elif tool.strip() == "website_search": return ModelTool(model="6736411cf127849667606689") @@ -160,9 +151,7 @@ def parse_tool_from_yaml(tool: str) -> ModelTool: raise Exception(f"Tool {tool} in yaml not found.") -def build_team_agent_from_yaml( - yaml_code: str, llm_id: str, api_key: str, team_id: Optional[str] = None -) -> TeamAgent: +def build_team_agent_from_yaml(yaml_code: str, llm_id: str, api_key: str, team_id: Optional[str] = None) -> TeamAgent: import yaml from aixplain.factories import AgentFactory, TeamAgentFactory @@ -170,11 +159,7 @@ def build_team_agent_from_yaml( agents_data = team_config["agents"] tasks_data = team_config.get("tasks", []) - system_data = ( - team_config["system"] - if "system" in team_config - else {"query": "", "name": "Test Team"} - ) + system_data = team_config["system"] if "system" in team_config else {"query": "", "name": "Test Team"} team_name = system_data["name"] # Create agent mapping by name for easier task assignment @@ -190,22 +175,14 @@ def build_team_agent_from_yaml( description = f"You are an expert {agent_role}. {agent_backstory} Your primary goal is to {agent_goal}. Use your expertise to ensure the success of your tasks." agent_name = agent_name.replace("_", " ") - agent_name = ( - f"{agent_name} agent" - if not agent_name.endswith(" agent") - else agent_name - ) + agent_name = f"{agent_name} agent" if not agent_name.endswith(" agent") else agent_name agent_obj = Agent( id="", name=agent_name, description=description, instructions=description, tasks=[], # Tasks will be assigned later - tools=[ - parse_tool_from_yaml(tool) - for tool in agent_info.get("tools", []) - if tool != "language_model" - ], + tools=[parse_tool_from_yaml(tool) for tool in agent_info.get("tools", []) if tool != "language_model"], llmId=llm_id, ) agents_mapping[agent_name] = agent_obj @@ -219,11 +196,7 @@ def build_team_agent_from_yaml( dependencies = task_info.get("dependencies", []) agent_name = task_info["agent"] agent_name = agent_name.replace("_", " ") - agent_name = ( - f"{agent_name} agent" - if not agent_name.endswith(" agent") - else agent_name - ) + agent_name = f"{agent_name} agent" if not agent_name.endswith(" agent") else agent_name task_obj = AgentTask( name=task_name, diff --git a/aixplain/modules/team_agent/__init__.py b/aixplain/modules/team_agent/__init__.py index 734b2694..30af3f6e 100644 --- a/aixplain/modules/team_agent/__init__.py +++ b/aixplain/modules/team_agent/__init__.py @@ -539,8 +539,8 @@ def evolve( raise ValueError("evolve_parameters must be a dictionary, EvolveParam instance, or None") start = time.time() try: - print("Evolve started with parameters: ", evolve_parameters) - print("It might take a while...") + logging.info(f"Evolve started with parameters: {evolve_parameters}") + logging.info("It might take a while...") response = self.evolve_async(evolve_parameters) if response["status"] == ResponseStatus.FAILED: end = time.time() @@ -550,8 +550,8 @@ def evolve( end = time.time() result = self.sync_poll(poll_url, name="evolve_process", timeout=600) result_data = result.data - - if "current_code" in result_data and result_data["current_code"] is not None: + current_code = result_data.get("current_code") + if current_code is not None: if evolve_parameters.evolve_type == EvolveType.TEAM_TUNING: result_data["evolved_agent"] = from_yaml( result_data["current_code"], diff --git a/aixplain/utils/evolve_utils.py b/aixplain/utils/evolve_utils.py index 966f1bed..130aabba 100644 --- a/aixplain/utils/evolve_utils.py +++ b/aixplain/utils/evolve_utils.py @@ -6,35 +6,36 @@ from aixplain.factories import AgentFactory, TeamAgentFactory -def parse_tools(tools: list) -> list: +def generate_tools_from_str(tools: list) -> list: obj_tools = [] for tool in tools: - if tool.strip() == "translation": + tool_name = tool.strip() + if tool_name == "translation": obj_tools.append( AgentFactory.create_model_tool( function=Function.TRANSLATION, supplier=Supplier.GOOGLE, ) ) - elif tool.strip() == "speech-recognition": + elif tool_name == "speech-recognition": obj_tools.append( AgentFactory.create_model_tool( function=Function.SPEECH_RECOGNITION, supplier=Supplier.GOOGLE, ) ) - elif tool.strip() == "text-to-speech": + elif tool_name == "text-to-speech": obj_tools.append( AgentFactory.create_model_tool( function=Function.SPEECH_SYNTHESIS, supplier=Supplier.GOOGLE, ) ) - elif tool.strip() == "serper_search": + elif tool_name == "serper_search": obj_tools.append(AgentFactory.create_model_tool(model="65c51c556eb563350f6e1bb1")) - elif tool.strip() == "website_search": + elif tool_name == "website_search": obj_tools.append(AgentFactory.create_model_tool(model="6736411cf127849667606689")) - elif tool.strip() == "website_scrape": + elif tool_name == "website_scrape": obj_tools.append(AgentFactory.create_model_tool(model="6748e4746eb5633559668a15")) else: continue @@ -80,7 +81,7 @@ def from_yaml( name=agent_name.replace("_", " "), description=description, tasks=[], # Tasks will be assigned later - tools=parse_tools(agent_info.get("tools", [])), + tools=generate_tools_from_str(agent_info.get("tools", [])), llm_id=llm_id, ) agents_mapping[agent_name] = agent_obj @@ -96,7 +97,7 @@ def from_yaml( agent_backstory = agent_info["backstory"] description = f"## ROLE\n{agent_role}\n\n## GOAL\n{agent_goal}\n\n## BACKSTORY\n{agent_backstory}" - agent_tools = parse_tools(agent_info.get("tools", [])) + agent_tools = generate_tools_from_str(agent_info.get("tools", [])) agent_obj = AgentFactory.create( name=agent_name.replace("_", " "), description=description, From 32195a969930f34512fc78f8905a7ebd77c033e1 Mon Sep 17 00:00:00 2001 From: ahmetgunduz Date: Tue, 24 Jun 2025 17:22:17 +0300 Subject: [PATCH 15/22] Add evolver_llm parameter to evolve methods for Agent and TeamAgent --- aixplain/modules/agent/__init__.py | 89 +++++--- aixplain/modules/agent/evolve_param.py | 30 ++- aixplain/modules/team_agent/__init__.py | 89 +++++--- aixplain/utils/evolve_utils.py | 30 ++- tests/functional/team_agent/evolver_test.py | 12 ++ tests/unit/agent/evolve_param_test.py | 11 +- tests/unit/agent/test_agent_evolve.py | 214 ++++++++++++++++++++ tests/unit/agent/test_evolver_llm_utils.py | 129 ++++++++++++ 8 files changed, 539 insertions(+), 65 deletions(-) create mode 100644 tests/unit/agent/test_agent_evolve.py create mode 100644 tests/unit/agent/test_evolver_llm_utils.py diff --git a/aixplain/modules/agent/__init__.py b/aixplain/modules/agent/__init__.py index c260161c..bfcba248 100644 --- a/aixplain/modules/agent/__init__.py +++ b/aixplain/modules/agent/__init__.py @@ -28,6 +28,7 @@ from aixplain.utils.file_utils import _request_with_retry from aixplain.enums import Function, Supplier, AssetStatus, StorageType, ResponseStatus +from aixplain.enums.evolve_type import EvolveType from aixplain.modules.model import Model from aixplain.modules.agent.agent_task import AgentTask from aixplain.modules.agent.output_format import OutputFormat @@ -489,58 +490,90 @@ def save(self) -> None: def __repr__(self): return f"Agent: {self.name} (id={self.id})" - def evolve_async(self, evolve_parameters: Union[Dict[str, Any], EvolveParam, None] = None) -> AgentResponse: + def evolve_async( + self, + evolve_type: Union[EvolveType, str] = EvolveType.TEAM_TUNING, + max_generations: int = 3, + max_retries: int = 3, + recursion_limit: int = 50, + max_iterations_without_improvement: Optional[int] = 2, + evolver_llm: Optional[Union[Text, LLM]] = None, + ) -> AgentResponse: """Asynchronously evolve the Agent and return a polling URL in the AgentResponse. Args: - evolve_parameters (Union[Dict[str, Any], EvolveParam, None]): Evolution parameters. - Can be a dictionary, EvolveParam instance, or None. + evolve_type (Union[EvolveType, str]): Type of evolution (TEAM_TUNING or INSTRUCTION_TUNING). Defaults to TEAM_TUNING. + max_generations (int): Maximum number of generations to evolve. Defaults to 3. + max_retries (int): Maximum retry attempts. Defaults to 3. + recursion_limit (int): Limit for recursive operations. Defaults to 50. + max_iterations_without_improvement (Optional[int]): Stop condition parameter. Defaults to 2, can be None. + evolver_llm (Optional[Union[Text, LLM]]): LLM to use for evolution. Can be an LLM ID string or LLM object. Defaults to None. Returns: AgentResponse: Response containing polling URL and status. """ + from aixplain.utils.evolve_utils import create_evolver_llm_dict + query = "" - if evolve_parameters is None: - evolve_parameters = EvolveParam(to_evolve=True) - elif isinstance(evolve_parameters, dict): - evolve_parameters = EvolveParam.from_dict(evolve_parameters) - evolve_parameters.to_evolve = True - elif isinstance(evolve_parameters, EvolveParam): - evolve_parameters.to_evolve = True - else: - raise ValueError("evolve_parameters must be a dictionary, EvolveParam instance, or None") + + # Create EvolveParam from individual parameters + evolve_parameters = EvolveParam( + to_evolve=True, + evolve_type=evolve_type, + max_generations=max_generations, + max_retries=max_retries, + recursion_limit=recursion_limit, + max_iterations_without_improvement=max_iterations_without_improvement, + evolver_llm=create_evolver_llm_dict(evolver_llm), + ) return self.run_async(query=query, evolve=evolve_parameters) def evolve( self, - evolve_parameters: Union[Dict[str, Any], EvolveParam, None] = None, + evolve_type: Union[EvolveType, str] = EvolveType.TEAM_TUNING, + max_generations: int = 3, + max_retries: int = 3, + recursion_limit: int = 50, + max_iterations_without_improvement: Optional[int] = 2, + evolver_llm: Optional[Union[Text, LLM]] = None, ) -> AgentResponse: """Synchronously evolve the Agent and poll for the result. Args: - evolve_parameters (Union[Dict[str, Any], EvolveParam, None]): Evolution parameters. - Can be a dictionary, EvolveParam instance, or None. + evolve_type (Union[EvolveType, str]): Type of evolution (TEAM_TUNING or INSTRUCTION_TUNING). Defaults to TEAM_TUNING. + max_generations (int): Maximum number of generations to evolve. Defaults to 3. + max_retries (int): Maximum retry attempts. Defaults to 3. + recursion_limit (int): Limit for recursive operations. Defaults to 50. + max_iterations_without_improvement (Optional[int]): Stop condition parameter. Defaults to 2, can be None. + evolver_llm (Optional[Union[Text, LLM]]): LLM to use for evolution. Can be an LLM ID string or LLM object. Defaults to None. Returns: AgentResponse: Final response from the evolution process. """ - from aixplain.utils.evolve_utils import from_yaml - from aixplain.enums import EvolveType - - if evolve_parameters is None: - evolve_parameters = EvolveParam(to_evolve=True) - elif isinstance(evolve_parameters, dict): - evolve_parameters = EvolveParam.from_dict(evolve_parameters) - evolve_parameters.to_evolve = True - elif isinstance(evolve_parameters, EvolveParam): - evolve_parameters.to_evolve = True - else: - raise ValueError("evolve_parameters must be a dictionary, EvolveParam instance, or None") + from aixplain.utils.evolve_utils import from_yaml, create_evolver_llm_dict + + # Create EvolveParam from individual parameters + evolve_parameters = EvolveParam( + to_evolve=True, + evolve_type=evolve_type, + max_generations=max_generations, + max_retries=max_retries, + recursion_limit=recursion_limit, + max_iterations_without_improvement=max_iterations_without_improvement, + evolver_llm=create_evolver_llm_dict(evolver_llm), + ) start = time.time() try: - response = self.evolve_async(evolve_parameters) + response = self.evolve_async( + evolve_type=evolve_type, + max_generations=max_generations, + max_retries=max_retries, + recursion_limit=recursion_limit, + max_iterations_without_improvement=max_iterations_without_improvement, + evolver_llm=evolver_llm, + ) if response["status"] == ResponseStatus.FAILED: end = time.time() response["elapsed_time"] = end - start diff --git a/aixplain/modules/agent/evolve_param.py b/aixplain/modules/agent/evolve_param.py index 73a9f0d8..c622f90b 100644 --- a/aixplain/modules/agent/evolve_param.py +++ b/aixplain/modules/agent/evolve_param.py @@ -36,6 +36,7 @@ class EvolveParam: max_retries (int): Maximum number of retries. recursion_limit (int): Maximum number of recursion. max_iterations_without_improvement (int): Maximum number of iterations without improvement. + evolver_llm (Optional[Dict[str, Any]]): Evolver LLM configuration with all parameters. additional_params (Optional[Dict[str, Any]]): Additional parameters. """ @@ -44,7 +45,8 @@ class EvolveParam: max_generations: int = 3 max_retries: int = 3 recursion_limit: int = 50 - max_iterations_without_improvement: int = 3 + max_iterations_without_improvement: Optional[int] = 2 + evolver_llm: Optional[Dict[str, Any]] = None additional_params: Optional[Dict[str, Any]] = field(default_factory=dict) def __post_init__(self): @@ -58,8 +60,16 @@ def validate(self) -> None: ValueError: If any parameter is invalid. """ if self.evolve_type is not None: - if not isinstance(self.evolve_type, EvolveType): - raise ValueError("evolve_type must be a valid EvolveType") + if isinstance(self.evolve_type, str): + # Convert string to EvolveType + try: + self.evolve_type = EvolveType(self.evolve_type) + except ValueError: + raise ValueError( + f"evolve_type '{self.evolve_type}' is not a valid EvolveType. Valid values are: {list(EvolveType)}" + ) + elif not isinstance(self.evolve_type, EvolveType): + raise ValueError("evolve_type must be a valid EvolveType or string") if self.additional_params is not None: if not isinstance(self.additional_params, dict): raise ValueError("additional_params must be a dictionary") @@ -84,9 +94,9 @@ def validate(self) -> None: if self.max_iterations_without_improvement is not None: if not isinstance(self.max_iterations_without_improvement, int): - raise ValueError("max_iterations_without_improvement must be an integer") + raise ValueError("max_iterations_without_improvement must be an integer or None") if self.max_iterations_without_improvement <= 0: - raise ValueError("max_iterations_without_improvement must be positive") + raise ValueError("max_iterations_without_improvement must be positive or None") @classmethod def from_dict(cls, data: Union[Dict[str, Any], None]) -> "EvolveParam": @@ -115,6 +125,7 @@ def from_dict(cls, data: Union[Dict[str, Any], None]) -> "EvolveParam": "max_retries": data.get("max_retries"), "recursion_limit": data.get("recursion_limit"), "max_iterations_without_improvement": data.get("max_iterations_without_improvement"), + "evolver_llm": data.get("evolver_llm"), "additional_params": data.get("additional_params"), } @@ -134,6 +145,7 @@ def from_dict(cls, data: Union[Dict[str, Any], None]) -> "EvolveParam": "max_retries", "recursion_limit", "max_iterations_without_improvement", + "evolver_llm", "additional_params", ] } @@ -159,8 +171,10 @@ def to_dict(self) -> Dict[str, Any]: result["max_retries"] = self.max_retries if self.recursion_limit is not None: result["recursion_limit"] = self.recursion_limit - if self.max_iterations_without_improvement is not None: - result["max_iterations_without_improvement"] = self.max_iterations_without_improvement + # Always include max_iterations_without_improvement, even if None + result["max_iterations_without_improvement"] = self.max_iterations_without_improvement + if self.evolver_llm is not None: + result["evolver_llm"] = self.evolver_llm if self.additional_params is not None: result.update(self.additional_params) @@ -194,6 +208,7 @@ def merge(self, other: Union[Dict[str, Any], "EvolveParam"]) -> "EvolveParam": if other.max_iterations_without_improvement is not None else self.max_iterations_without_improvement ), + evolver_llm=(other.evolver_llm if other.evolver_llm is not None else self.evolver_llm), additional_params=merged_additional, ) @@ -206,6 +221,7 @@ def __repr__(self) -> str: f"max_retries={self.max_retries}, " f"recursion_limit={self.recursion_limit}, " f"max_iterations_without_improvement={self.max_iterations_without_improvement}, " + f"evolver_llm={self.evolver_llm}, " f"additional_params={self.additional_params})" ) diff --git a/aixplain/modules/team_agent/__init__.py b/aixplain/modules/team_agent/__init__.py index 30af3f6e..d752a0f4 100644 --- a/aixplain/modules/team_agent/__init__.py +++ b/aixplain/modules/team_agent/__init__.py @@ -35,6 +35,7 @@ from aixplain.enums.supplier import Supplier from aixplain.enums.asset_status import AssetStatus from aixplain.enums.storage_type import StorageType +from aixplain.enums.evolve_type import EvolveType from aixplain.modules.model import Model from aixplain.modules.agent import Agent, OutputFormat from aixplain.modules.agent.agent_response import AgentResponse @@ -487,61 +488,93 @@ def update(self) -> None: def __repr__(self): return f"TeamAgent: {self.name} (id={self.id})" - def evolve_async(self, evolve_parameters: Union[Dict[str, Any], EvolveParam, None] = None) -> AgentResponse: + def evolve_async( + self, + evolve_type: Union[EvolveType, str] = EvolveType.TEAM_TUNING, + max_generations: int = 3, + max_retries: int = 3, + recursion_limit: int = 50, + max_iterations_without_improvement: Optional[int] = 2, + evolver_llm: Optional[Union[Text, LLM]] = None, + ) -> AgentResponse: """Asynchronously evolve the Team Agent and return a polling URL in the AgentResponse. Args: - evolve_parameters (Union[Dict[str, Any], EvolveParam, None]): Evolution parameters. - Can be a dictionary, EvolveParam instance, or None. + evolve_type (Union[EvolveType, str]): Type of evolution (TEAM_TUNING or INSTRUCTION_TUNING). Defaults to TEAM_TUNING. + max_generations (int): Maximum number of generations to evolve. Defaults to 3. + max_retries (int): Maximum retry attempts. Defaults to 3. + recursion_limit (int): Limit for recursive operations. Defaults to 50. + max_iterations_without_improvement (Optional[int]): Stop condition parameter. Defaults to 2, can be None. + evolver_llm (Optional[Union[Text, LLM]]): LLM to use for evolution. Can be an LLM ID string or LLM object. Defaults to None. Returns: AgentResponse: Response containing polling URL and status. """ + from aixplain.utils.evolve_utils import create_evolver_llm_dict + query = "" - if evolve_parameters is None: - evolve_parameters = EvolveParam(to_evolve=True) - elif isinstance(evolve_parameters, dict): - evolve_parameters = EvolveParam.from_dict(evolve_parameters) - evolve_parameters.to_evolve = True - elif isinstance(evolve_parameters, EvolveParam): - evolve_parameters.to_evolve = True - else: - raise ValueError("evolve_parameters must be a dictionary, EvolveParam instance, or None") + + # Create EvolveParam from individual parameters + evolve_parameters = EvolveParam( + to_evolve=True, + evolve_type=evolve_type, + max_generations=max_generations, + max_retries=max_retries, + recursion_limit=recursion_limit, + max_iterations_without_improvement=max_iterations_without_improvement, + evolver_llm=create_evolver_llm_dict(evolver_llm), + ) response = self.run_async(query=query, evolve=evolve_parameters) return response def evolve( self, - evolve_parameters: Union[Dict[str, Any], EvolveParam, None] = None, + evolve_type: Union[EvolveType, str] = EvolveType.TEAM_TUNING, + max_generations: int = 3, + max_retries: int = 3, + recursion_limit: int = 50, + max_iterations_without_improvement: Optional[int] = 2, + evolver_llm: Optional[Union[Text, LLM]] = None, ) -> AgentResponse: """Synchronously evolve the Team Agent and poll for the result. Args: - evolve_parameters (Union[Dict[str, Any], EvolveParam, None]): Evolution parameters. - Can be a dictionary, EvolveParam instance, or None. - name (str, optional): Name for the process. Defaults to "evolve_process". + evolve_type (Union[EvolveType, str]): Type of evolution (TEAM_TUNING or INSTRUCTION_TUNING). Defaults to TEAM_TUNING. + max_generations (int): Maximum number of generations to evolve. Defaults to 3. + max_retries (int): Maximum retry attempts. Defaults to 3. + recursion_limit (int): Limit for recursive operations. Defaults to 50. + max_iterations_without_improvement (Optional[int]): Stop condition parameter. Defaults to 2, can be None. + evolver_llm (Optional[Union[Text, LLM]]): LLM to use for evolution. Can be an LLM ID string or LLM object. Defaults to None. Returns: AgentResponse: Final response from the evolution process. """ from aixplain.enums import EvolveType - from aixplain.utils.evolve_utils import from_yaml - - if evolve_parameters is None: - evolve_parameters = EvolveParam(to_evolve=True) - elif isinstance(evolve_parameters, dict): - evolve_parameters = EvolveParam.from_dict(evolve_parameters) - evolve_parameters.to_evolve = True - elif isinstance(evolve_parameters, EvolveParam): - evolve_parameters.to_evolve = True - else: - raise ValueError("evolve_parameters must be a dictionary, EvolveParam instance, or None") + from aixplain.utils.evolve_utils import from_yaml, create_evolver_llm_dict + + # Create EvolveParam from individual parameters + evolve_parameters = EvolveParam( + to_evolve=True, + evolve_type=evolve_type, + max_generations=max_generations, + max_retries=max_retries, + recursion_limit=recursion_limit, + max_iterations_without_improvement=max_iterations_without_improvement, + evolver_llm=create_evolver_llm_dict(evolver_llm), + ) start = time.time() try: logging.info(f"Evolve started with parameters: {evolve_parameters}") logging.info("It might take a while...") - response = self.evolve_async(evolve_parameters) + response = self.evolve_async( + evolve_type=evolve_type, + max_generations=max_generations, + max_retries=max_retries, + recursion_limit=recursion_limit, + max_iterations_without_improvement=max_iterations_without_improvement, + evolver_llm=evolver_llm, + ) if response["status"] == ResponseStatus.FAILED: end = time.time() response["elapsed_time"] = end - start diff --git a/aixplain/utils/evolve_utils.py b/aixplain/utils/evolve_utils.py index 130aabba..5695819d 100644 --- a/aixplain/utils/evolve_utils.py +++ b/aixplain/utils/evolve_utils.py @@ -1,11 +1,39 @@ import yaml -from typing import Union +from typing import Union, Dict, Any, Optional, Text from aixplain.enums import Function, Supplier from aixplain.modules.agent import Agent from aixplain.modules.team_agent import TeamAgent +from aixplain.modules.model.llm_model import LLM from aixplain.factories import AgentFactory, TeamAgentFactory +def create_evolver_llm_dict(evolver_llm: Optional[Union[Text, LLM]]) -> Optional[Dict[str, Any]]: + """Create a dictionary representation of an evolver LLM for evolution parameters. + + Args: + evolver_llm: Either an LLM ID string or an LLM object instance. + + Returns: + Dictionary with LLM information if evolver_llm is provided, None otherwise. + """ + if evolver_llm is None: + return None + + if isinstance(evolver_llm, LLM): + return { + "id": evolver_llm.id, + "name": evolver_llm.name, + "description": evolver_llm.description, + "supplier": evolver_llm.supplier, + "version": evolver_llm.version, + "function": evolver_llm.function, + "parameters": (evolver_llm.get_parameters().to_list() if evolver_llm.get_parameters() else None), + "temperature": getattr(evolver_llm, "temperature", None), + } + else: + return {"id": evolver_llm} + + def generate_tools_from_str(tools: list) -> list: obj_tools = [] for tool in tools: diff --git a/tests/functional/team_agent/evolver_test.py b/tests/functional/team_agent/evolver_test.py index 192d408b..a1fe5834 100644 --- a/tests/functional/team_agent/evolver_test.py +++ b/tests/functional/team_agent/evolver_test.py @@ -124,3 +124,15 @@ def test_evolver_output(team_agent): assert "evaluation_report" in result["data"], "Data should contain 'evaluation_report'" assert "criteria" in result["data"], "Data should contain 'criteria'" assert "archive" in result["data"], "Data should contain 'archive'" + + +def test_evolver_with_custom_llm_id(team_agent): + """Test evolver functionality with custom LLM ID""" + custom_llm_id = "6646261c6eb563165658bbb1" # GPT-4o ID + + # Test with evolver_llm parameter + response = team_agent.evolve_async(evolver_llm=custom_llm_id) + + assert response is not None + assert "url" in response or response.get("url") is not None + assert response["status"] == ResponseStatus.IN_PROGRESS diff --git a/tests/unit/agent/evolve_param_test.py b/tests/unit/agent/evolve_param_test.py index 7068320a..b108c146 100644 --- a/tests/unit/agent/evolve_param_test.py +++ b/tests/unit/agent/evolve_param_test.py @@ -23,7 +23,8 @@ def test_default_initialization(self): assert default_param.max_generations == 3 assert default_param.max_retries == 3 assert default_param.recursion_limit == 50 - assert default_param.max_iterations_without_improvement == 3 + assert default_param.max_iterations_without_improvement == 2 + assert default_param.evolver_llm is None assert default_param.additional_params == {} # Test to_dict method @@ -40,6 +41,7 @@ def test_custom_initialization(self): recursion_limit=30, max_iterations_without_improvement=4, evolve_type=EvolveType.TEAM_TUNING, + evolver_llm={"id": "test_llm_id", "name": "Test LLM"}, additional_params={"customParam": "custom_value"}, ) @@ -49,6 +51,7 @@ def test_custom_initialization(self): assert custom_param.recursion_limit == 30 assert custom_param.max_iterations_without_improvement == 4 assert custom_param.evolve_type == EvolveType.TEAM_TUNING + assert custom_param.evolver_llm == {"id": "test_llm_id", "name": "Test LLM"} assert custom_param.additional_params == {"customParam": "custom_value"} # Test to_dict method @@ -59,6 +62,7 @@ def test_custom_initialization(self): assert result_dict["recursion_limit"] == 30 assert result_dict["max_iterations_without_improvement"] == 4 assert result_dict["evolve_type"] == EvolveType.TEAM_TUNING + assert result_dict["evolver_llm"] == {"id": "test_llm_id", "name": "Test LLM"} assert result_dict["customParam"] == "custom_value" def test_from_dict_with_api_format(self): @@ -70,6 +74,7 @@ def test_from_dict_with_api_format(self): "recursion_limit": 40, "max_iterations_without_improvement": 5, "evolve_type": EvolveType.TEAM_TUNING, + "evolver_llm": {"id": "api_llm_id", "name": "API LLM"}, "customParam": "custom_value", } @@ -81,6 +86,7 @@ def test_from_dict_with_api_format(self): assert from_dict_param.recursion_limit == 40 assert from_dict_param.max_iterations_without_improvement == 5 assert from_dict_param.evolve_type == EvolveType.TEAM_TUNING + assert from_dict_param.evolver_llm == {"id": "api_llm_id", "name": "API LLM"} # Test round-trip conversion result_dict = from_dict_param.to_dict() @@ -123,6 +129,7 @@ def test_validate_evolve_param_with_instance(self): recursion_limit=30, max_iterations_without_improvement=4, evolve_type=EvolveType.TEAM_TUNING, + evolver_llm={"id": "instance_llm_id"}, additional_params={"customParam": "custom_value"}, ) @@ -165,6 +172,7 @@ def test_merge_with_dict(self): merge_dict = { "toEvolve": True, "max_generations": 5, + "evolver_llm": {"id": "merged_llm_id"}, "customParam": "custom_value", } @@ -172,6 +180,7 @@ def test_merge_with_dict(self): assert merged.to_evolve is True assert merged.max_generations == 5 + assert merged.evolver_llm == {"id": "merged_llm_id"} assert merged.additional_params == { "base": "value", "customParam": "custom_value", diff --git a/tests/unit/agent/test_agent_evolve.py b/tests/unit/agent/test_agent_evolve.py new file mode 100644 index 00000000..858d9585 --- /dev/null +++ b/tests/unit/agent/test_agent_evolve.py @@ -0,0 +1,214 @@ +""" +Unit tests for Agent evolve functionality with evolver_llm parameter +""" + +import pytest +from unittest.mock import Mock, patch +from aixplain.modules.agent import Agent +from aixplain.modules.model.llm_model import LLM +from aixplain.modules.agent.evolve_param import EvolveParam +from aixplain.enums import EvolveType, Function, Supplier, ResponseStatus +from aixplain.modules.agent.agent_response import AgentResponse +from aixplain.modules.agent.agent_response_data import AgentResponseData + + +class TestAgentEvolve: + """Test class for Agent evolve functionality""" + + @pytest.fixture + def mock_agent(self): + """Create a mock Agent for testing""" + agent = Mock(spec=Agent) + agent.id = "test_agent_id" + agent.name = "Test Agent" + agent.api_key = "test_api_key" + return agent + + @pytest.fixture + def mock_llm(self): + """Create a mock LLM for testing""" + llm = Mock(spec=LLM) + llm.id = "test_llm_id" + llm.name = "Test LLM" + llm.description = "Test LLM Description" + llm.supplier = Supplier.OPENAI + llm.version = "1.0.0" + llm.function = Function.TEXT_GENERATION + llm.temperature = 0.7 + + # Mock get_parameters + mock_params = Mock() + mock_params.to_list.return_value = [{"name": "temperature", "type": "float"}] + llm.get_parameters.return_value = mock_params + + return llm + + def test_evolve_async_with_evolver_llm_string(self, mock_agent): + """Test evolve_async with evolver_llm as string ID""" + from aixplain.modules.agent import Agent + + # Create a real Agent instance but mock its methods + agent = Agent( + id="test_agent_id", + name="Test Agent", + description="Test Description", + instructions="Test Instructions", + tools=[], + llm_id="6646261c6eb563165658bbb1", + ) + + # Mock the run_async method + mock_response = AgentResponse( + status=ResponseStatus.IN_PROGRESS, + url="http://test-poll-url.com", + data=AgentResponseData(input="test input"), + run_time=0.0, + used_credits=0.0, + ) + + with patch.object(agent, "run_async", return_value=mock_response) as mock_run_async: + result = agent.evolve_async(evolver_llm="custom_llm_id_123") + + # Verify run_async was called with correct evolve parameter + mock_run_async.assert_called_once() + call_args = mock_run_async.call_args + + # Check that evolve parameter contains evolver_llm + evolve_param = call_args[1]["evolve"] + assert isinstance(evolve_param, EvolveParam) + assert evolve_param.evolver_llm == {"id": "custom_llm_id_123"} + + assert result == mock_response + + def test_evolve_async_with_evolver_llm_object(self, mock_agent, mock_llm): + """Test evolve_async with evolver_llm as LLM object""" + from aixplain.modules.agent import Agent + + # Create a real Agent instance but mock its methods + agent = Agent( + id="test_agent_id", + name="Test Agent", + description="Test Description", + instructions="Test Instructions", + tools=[], + llm_id="6646261c6eb563165658bbb1", + ) + + # Mock the run_async method + mock_response = AgentResponse( + status=ResponseStatus.IN_PROGRESS, + url="http://test-poll-url.com", + data=AgentResponseData(input="test input"), + run_time=0.0, + used_credits=0.0, + ) + + with patch.object(agent, "run_async", return_value=mock_response) as mock_run_async: + result = agent.evolve_async(evolver_llm=mock_llm) + + # Verify run_async was called with correct evolve parameter + mock_run_async.assert_called_once() + call_args = mock_run_async.call_args + + # Check that evolve parameter contains evolver_llm + evolve_param = call_args[1]["evolve"] + assert isinstance(evolve_param, EvolveParam) + + expected_llm_dict = { + "id": "test_llm_id", + "name": "Test LLM", + "description": "Test LLM Description", + "supplier": Supplier.OPENAI, + "version": "1.0.0", + "function": Function.TEXT_GENERATION, + "parameters": [{"name": "temperature", "type": "float"}], + "temperature": 0.7, + } + assert evolve_param.evolver_llm == expected_llm_dict + + assert result == mock_response + + def test_evolve_async_without_evolver_llm(self, mock_agent): + """Test evolve_async without evolver_llm parameter""" + from aixplain.modules.agent import Agent + + # Create a real Agent instance but mock its methods + agent = Agent( + id="test_agent_id", + name="Test Agent", + description="Test Description", + instructions="Test Instructions", + tools=[], + llm_id="6646261c6eb563165658bbb1", + ) + + # Mock the run_async method + mock_response = AgentResponse( + status=ResponseStatus.IN_PROGRESS, + url="http://test-poll-url.com", + data=AgentResponseData(input="test input"), + run_time=0.0, + used_credits=0.0, + ) + + with patch.object(agent, "run_async", return_value=mock_response) as mock_run_async: + result = agent.evolve_async() + + # Verify run_async was called with correct evolve parameter + mock_run_async.assert_called_once() + call_args = mock_run_async.call_args + + # Check that evolve parameter has evolver_llm as None + evolve_param = call_args[1]["evolve"] + assert isinstance(evolve_param, EvolveParam) + assert evolve_param.evolver_llm is None + + assert result == mock_response + + def test_evolve_with_custom_parameters(self, mock_agent): + """Test evolve with custom parameters including evolver_llm""" + from aixplain.modules.agent import Agent + + # Create a real Agent instance but mock its methods + agent = Agent( + id="test_agent_id", + name="Test Agent", + description="Test Description", + instructions="Test Instructions", + tools=[], + llm_id="6646261c6eb563165658bbb1", + ) + + with patch.object(agent, "evolve_async") as mock_evolve_async, patch.object(agent, "sync_poll") as mock_sync_poll: + + # Mock evolve_async response + mock_evolve_async.return_value = {"status": ResponseStatus.IN_PROGRESS, "url": "http://test-poll-url.com"} + + # Mock sync_poll response + mock_result = Mock() + mock_result.data = {"current_code": "test code", "evolved_agent": "evolved_agent_data"} + mock_sync_poll.return_value = mock_result + + result = agent.evolve( + evolve_type=EvolveType.TEAM_TUNING, + max_generations=5, + max_retries=3, + recursion_limit=40, + max_iterations_without_improvement=3, + evolver_llm="custom_evolver_llm_id", + ) + + # Verify evolve_async was called with correct parameters + mock_evolve_async.assert_called_once_with( + evolve_type=EvolveType.TEAM_TUNING, + max_generations=5, + max_retries=3, + recursion_limit=40, + max_iterations_without_improvement=3, + evolver_llm="custom_evolver_llm_id", + ) + + # Verify sync_poll was called + mock_sync_poll.assert_called_once_with("http://test-poll-url.com", name="evolve_process", timeout=600) + + assert result is not None diff --git a/tests/unit/agent/test_evolver_llm_utils.py b/tests/unit/agent/test_evolver_llm_utils.py new file mode 100644 index 00000000..dfcb4ec7 --- /dev/null +++ b/tests/unit/agent/test_evolver_llm_utils.py @@ -0,0 +1,129 @@ +""" +Unit tests for evolver LLM utility functions +""" + +from unittest.mock import Mock +from aixplain.utils.evolve_utils import create_evolver_llm_dict +from aixplain.modules.model.llm_model import LLM +from aixplain.enums import Function, Supplier + + +class TestCreateEvolverLLMDict: + """Test class for create_evolver_llm_dict functionality""" + + def test_create_evolver_llm_dict_with_none(self): + """Test create_evolver_llm_dict with None input""" + result = create_evolver_llm_dict(None) + assert result is None + + def test_create_evolver_llm_dict_with_string_id(self): + """Test create_evolver_llm_dict with LLM ID string""" + llm_id = "test_llm_id_123" + result = create_evolver_llm_dict(llm_id) + + expected = {"id": llm_id} + assert result == expected + + def test_create_evolver_llm_dict_with_llm_object(self): + """Test create_evolver_llm_dict with LLM object""" + # Create a mock LLM object + mock_llm = Mock(spec=LLM) + mock_llm.id = "llm_id_456" + mock_llm.name = "Test LLM Model" + mock_llm.description = "A test LLM model for unit testing" + mock_llm.supplier = Supplier.OPENAI + mock_llm.version = "1.0.0" + mock_llm.function = Function.TEXT_GENERATION + mock_llm.temperature = 0.7 + + # Mock the get_parameters method + mock_parameters = Mock() + mock_parameters.to_list.return_value = [ + {"name": "max_tokens", "type": "integer", "default": 2048}, + {"name": "temperature", "type": "float", "default": 0.7}, + ] + mock_llm.get_parameters.return_value = mock_parameters + + result = create_evolver_llm_dict(mock_llm) + + expected = { + "id": "llm_id_456", + "name": "Test LLM Model", + "description": "A test LLM model for unit testing", + "supplier": Supplier.OPENAI, + "version": "1.0.0", + "function": Function.TEXT_GENERATION, + "parameters": [ + {"name": "max_tokens", "type": "integer", "default": 2048}, + {"name": "temperature", "type": "float", "default": 0.7}, + ], + "temperature": 0.7, + } + assert result == expected + + def test_create_evolver_llm_dict_with_llm_object_no_parameters(self): + """Test create_evolver_llm_dict with LLM object that has no parameters""" + # Create a mock LLM object + mock_llm = Mock(spec=LLM) + mock_llm.id = "llm_id_789" + mock_llm.name = "Simple LLM" + mock_llm.description = "A simple LLM without parameters" + mock_llm.supplier = Supplier.OPENAI + mock_llm.version = "2.0.0" + mock_llm.function = Function.TEXT_GENERATION + mock_llm.temperature = 0.5 + + # Mock get_parameters to return None + mock_llm.get_parameters.return_value = None + + result = create_evolver_llm_dict(mock_llm) + + expected = { + "id": "llm_id_789", + "name": "Simple LLM", + "description": "A simple LLM without parameters", + "supplier": Supplier.OPENAI, + "version": "2.0.0", + "function": Function.TEXT_GENERATION, + "parameters": None, + "temperature": 0.5, + } + assert result == expected + + def test_create_evolver_llm_dict_with_llm_object_no_temperature(self): + """Test create_evolver_llm_dict with LLM object that has no temperature attribute""" + # Create a mock LLM object without temperature + mock_llm = Mock(spec=LLM) + mock_llm.id = "llm_id_999" + mock_llm.name = "No Temp LLM" + mock_llm.description = "LLM without temperature" + mock_llm.supplier = Supplier.GOOGLE + mock_llm.version = "3.0.0" + mock_llm.function = Function.TEXT_GENERATION + + # Remove temperature attribute + del mock_llm.temperature + + # Mock get_parameters to return None + mock_llm.get_parameters.return_value = None + + result = create_evolver_llm_dict(mock_llm) + + expected = { + "id": "llm_id_999", + "name": "No Temp LLM", + "description": "LLM without temperature", + "supplier": Supplier.GOOGLE, + "version": "3.0.0", + "function": Function.TEXT_GENERATION, + "parameters": None, + "temperature": None, + } + assert result == expected + + def test_create_evolver_llm_dict_with_empty_string(self): + """Test create_evolver_llm_dict with empty string""" + result = create_evolver_llm_dict("") + + expected = {"id": ""} + assert result == expected From a240788975c882df2c9e302541542745830a858d Mon Sep 17 00:00:00 2001 From: ahmetgunduz Date: Fri, 18 Jul 2025 12:56:58 +0000 Subject: [PATCH 16/22] Changed the names of parameters to be more intuative --- aixplain/modules/agent/__init__.py | 44 ++++++++++++----------- aixplain/modules/team_agent/__init__.py | 46 ++++++++++++------------- pyproject.toml | 12 +++---- tests/unit/agent/test_agent_evolve.py | 12 +++---- 4 files changed, 58 insertions(+), 56 deletions(-) diff --git a/aixplain/modules/agent/__init__.py b/aixplain/modules/agent/__init__.py index bfcba248..26dbef0a 100644 --- a/aixplain/modules/agent/__init__.py +++ b/aixplain/modules/agent/__init__.py @@ -493,20 +493,20 @@ def __repr__(self): def evolve_async( self, evolve_type: Union[EvolveType, str] = EvolveType.TEAM_TUNING, - max_generations: int = 3, - max_retries: int = 3, + max_successful_generations: int = 3, + max_failed_generation_retries: int = 3, recursion_limit: int = 50, - max_iterations_without_improvement: Optional[int] = 2, + max_non_improving_generations: Optional[int] = 2, evolver_llm: Optional[Union[Text, LLM]] = None, ) -> AgentResponse: """Asynchronously evolve the Agent and return a polling URL in the AgentResponse. Args: evolve_type (Union[EvolveType, str]): Type of evolution (TEAM_TUNING or INSTRUCTION_TUNING). Defaults to TEAM_TUNING. - max_generations (int): Maximum number of generations to evolve. Defaults to 3. - max_retries (int): Maximum retry attempts. Defaults to 3. + max_successful_generations (int): Maximum number of successful generations to evolve. Defaults to 3. + max_failed_generation_retries (int): Maximum retry attempts for failed generations. Defaults to 3. recursion_limit (int): Limit for recursive operations. Defaults to 50. - max_iterations_without_improvement (Optional[int]): Stop condition parameter. Defaults to 2, can be None. + max_non_improving_generations (Optional[int]): Stop condition parameter for non-improving generations. Defaults to 2, can be None. evolver_llm (Optional[Union[Text, LLM]]): LLM to use for evolution. Can be an LLM ID string or LLM object. Defaults to None. Returns: @@ -520,10 +520,10 @@ def evolve_async( evolve_parameters = EvolveParam( to_evolve=True, evolve_type=evolve_type, - max_generations=max_generations, - max_retries=max_retries, + max_generations=max_successful_generations, + max_retries=max_failed_generation_retries, recursion_limit=recursion_limit, - max_iterations_without_improvement=max_iterations_without_improvement, + max_iterations_without_improvement=max_non_improving_generations, evolver_llm=create_evolver_llm_dict(evolver_llm), ) @@ -532,20 +532,20 @@ def evolve_async( def evolve( self, evolve_type: Union[EvolveType, str] = EvolveType.TEAM_TUNING, - max_generations: int = 3, - max_retries: int = 3, + max_successful_generations: int = 3, + max_failed_generation_retries: int = 3, recursion_limit: int = 50, - max_iterations_without_improvement: Optional[int] = 2, + max_non_improving_generations: Optional[int] = 2, evolver_llm: Optional[Union[Text, LLM]] = None, ) -> AgentResponse: """Synchronously evolve the Agent and poll for the result. Args: evolve_type (Union[EvolveType, str]): Type of evolution (TEAM_TUNING or INSTRUCTION_TUNING). Defaults to TEAM_TUNING. - max_generations (int): Maximum number of generations to evolve. Defaults to 3. - max_retries (int): Maximum retry attempts. Defaults to 3. + max_successful_generations (int): Maximum number of successful generations to evolve. Defaults to 3. + max_failed_generation_retries (int): Maximum retry attempts for failed generations. Defaults to 3. recursion_limit (int): Limit for recursive operations. Defaults to 50. - max_iterations_without_improvement (Optional[int]): Stop condition parameter. Defaults to 2, can be None. + max_non_improving_generations (Optional[int]): Stop condition parameter for non-improving generations. Defaults to 2, can be None. evolver_llm (Optional[Union[Text, LLM]]): LLM to use for evolution. Can be an LLM ID string or LLM object. Defaults to None. Returns: @@ -557,21 +557,23 @@ def evolve( evolve_parameters = EvolveParam( to_evolve=True, evolve_type=evolve_type, - max_generations=max_generations, - max_retries=max_retries, + max_generations=max_successful_generations, + max_retries=max_failed_generation_retries, recursion_limit=recursion_limit, - max_iterations_without_improvement=max_iterations_without_improvement, + max_iterations_without_improvement=max_non_improving_generations, evolver_llm=create_evolver_llm_dict(evolver_llm), ) start = time.time() try: + logging.info(f"Evolve started with parameters: {evolve_parameters}") + logging.info("It might take a while...") response = self.evolve_async( evolve_type=evolve_type, - max_generations=max_generations, - max_retries=max_retries, + max_successful_generations=max_successful_generations, + max_failed_generation_retries=max_failed_generation_retries, recursion_limit=recursion_limit, - max_iterations_without_improvement=max_iterations_without_improvement, + max_non_improving_generations=max_non_improving_generations, evolver_llm=evolver_llm, ) if response["status"] == ResponseStatus.FAILED: diff --git a/aixplain/modules/team_agent/__init__.py b/aixplain/modules/team_agent/__init__.py index d752a0f4..2dcf1c3e 100644 --- a/aixplain/modules/team_agent/__init__.py +++ b/aixplain/modules/team_agent/__init__.py @@ -491,20 +491,20 @@ def __repr__(self): def evolve_async( self, evolve_type: Union[EvolveType, str] = EvolveType.TEAM_TUNING, - max_generations: int = 3, - max_retries: int = 3, + max_successful_generations: int = 3, + max_failed_generation_retries: int = 3, recursion_limit: int = 50, - max_iterations_without_improvement: Optional[int] = 2, + max_non_improving_generations: Optional[int] = 2, evolver_llm: Optional[Union[Text, LLM]] = None, ) -> AgentResponse: """Asynchronously evolve the Team Agent and return a polling URL in the AgentResponse. Args: evolve_type (Union[EvolveType, str]): Type of evolution (TEAM_TUNING or INSTRUCTION_TUNING). Defaults to TEAM_TUNING. - max_generations (int): Maximum number of generations to evolve. Defaults to 3. - max_retries (int): Maximum retry attempts. Defaults to 3. + max_successful_generations (int): Maximum number of successful generations to evolve. Defaults to 3. + max_failed_generation_retries (int): Maximum retry attempts for failed generations. Defaults to 3. recursion_limit (int): Limit for recursive operations. Defaults to 50. - max_iterations_without_improvement (Optional[int]): Stop condition parameter. Defaults to 2, can be None. + max_non_improving_generations (Optional[int]): Stop condition parameter for non-improving generations. Defaults to 2, can be None. evolver_llm (Optional[Union[Text, LLM]]): LLM to use for evolution. Can be an LLM ID string or LLM object. Defaults to None. Returns: @@ -514,14 +514,14 @@ def evolve_async( query = "" - # Create EvolveParam from individual parameters + # Create EvolveParam from individual parameters (map new names to old keys) evolve_parameters = EvolveParam( to_evolve=True, evolve_type=evolve_type, - max_generations=max_generations, - max_retries=max_retries, + max_generations=max_successful_generations, + max_retries=max_failed_generation_retries, recursion_limit=recursion_limit, - max_iterations_without_improvement=max_iterations_without_improvement, + max_iterations_without_improvement=max_non_improving_generations, evolver_llm=create_evolver_llm_dict(evolver_llm), ) @@ -531,20 +531,20 @@ def evolve_async( def evolve( self, evolve_type: Union[EvolveType, str] = EvolveType.TEAM_TUNING, - max_generations: int = 3, - max_retries: int = 3, + max_successful_generations: int = 3, + max_failed_generation_retries: int = 3, recursion_limit: int = 50, - max_iterations_without_improvement: Optional[int] = 2, + max_non_improving_generations: Optional[int] = 2, evolver_llm: Optional[Union[Text, LLM]] = None, ) -> AgentResponse: """Synchronously evolve the Team Agent and poll for the result. Args: evolve_type (Union[EvolveType, str]): Type of evolution (TEAM_TUNING or INSTRUCTION_TUNING). Defaults to TEAM_TUNING. - max_generations (int): Maximum number of generations to evolve. Defaults to 3. - max_retries (int): Maximum retry attempts. Defaults to 3. + max_successful_generations (int): Maximum number of successful generations to evolve. Defaults to 3. + max_failed_generation_retries (int): Maximum retry attempts for failed generations. Defaults to 3. recursion_limit (int): Limit for recursive operations. Defaults to 50. - max_iterations_without_improvement (Optional[int]): Stop condition parameter. Defaults to 2, can be None. + max_non_improving_generations (Optional[int]): Stop condition parameter for non-improving generations. Defaults to 2, can be None. evolver_llm (Optional[Union[Text, LLM]]): LLM to use for evolution. Can be an LLM ID string or LLM object. Defaults to None. Returns: @@ -553,14 +553,14 @@ def evolve( from aixplain.enums import EvolveType from aixplain.utils.evolve_utils import from_yaml, create_evolver_llm_dict - # Create EvolveParam from individual parameters + # Create EvolveParam from individual parameters (map new names to old keys) evolve_parameters = EvolveParam( to_evolve=True, evolve_type=evolve_type, - max_generations=max_generations, - max_retries=max_retries, + max_generations=max_successful_generations, + max_retries=max_failed_generation_retries, recursion_limit=recursion_limit, - max_iterations_without_improvement=max_iterations_without_improvement, + max_iterations_without_improvement=max_non_improving_generations, evolver_llm=create_evolver_llm_dict(evolver_llm), ) start = time.time() @@ -569,10 +569,10 @@ def evolve( logging.info("It might take a while...") response = self.evolve_async( evolve_type=evolve_type, - max_generations=max_generations, - max_retries=max_retries, + max_successful_generations=max_successful_generations, + max_failed_generation_retries=max_failed_generation_retries, recursion_limit=recursion_limit, - max_iterations_without_improvement=max_iterations_without_improvement, + max_non_improving_generations=max_non_improving_generations, evolver_llm=evolver_llm, ) if response["status"] == ResponseStatus.FAILED: diff --git a/pyproject.toml b/pyproject.toml index 0bc5ec6a..e67621a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ namespaces = true [project] name = "aiXplain" -version = "0.2.26" +version = "0.2.33" description = "aiXplain SDK adds AI functions to software." readme = "README.md" requires-python = ">=3.5, <4" @@ -43,11 +43,11 @@ classifiers = [ "Topic :: Software Development :: Libraries", ] dependencies = [ - "requests>=2.1.0", - "tqdm>=4.1.0", - "pandas>=1.2.1", - "python-dotenv>=1.0.0", - "validators>=0.20.0", + "requests>=2.1.0", + "tqdm>=4.1.0", + "pandas>=1.2.1", + "python-dotenv>=1.0.0", + "validators>=0.20.0", "filetype>=1.2.0", "click>=7.1.2", "PyYAML>=6.0.1", diff --git a/tests/unit/agent/test_agent_evolve.py b/tests/unit/agent/test_agent_evolve.py index 858d9585..1aa6cd70 100644 --- a/tests/unit/agent/test_agent_evolve.py +++ b/tests/unit/agent/test_agent_evolve.py @@ -191,20 +191,20 @@ def test_evolve_with_custom_parameters(self, mock_agent): result = agent.evolve( evolve_type=EvolveType.TEAM_TUNING, - max_generations=5, - max_retries=3, + max_successful_generations=5, + max_failed_generation_retries=3, recursion_limit=40, - max_iterations_without_improvement=3, + max_non_improving_generations=3, evolver_llm="custom_evolver_llm_id", ) # Verify evolve_async was called with correct parameters mock_evolve_async.assert_called_once_with( evolve_type=EvolveType.TEAM_TUNING, - max_generations=5, - max_retries=3, + max_successful_generations=5, + max_failed_generation_retries=3, recursion_limit=40, - max_iterations_without_improvement=3, + max_non_improving_generations=3, evolver_llm="custom_evolver_llm_id", ) From bcfd6c8ccc8588d71d608162ef23ba1e71a6796a Mon Sep 17 00:00:00 2001 From: ahmetgunduz Date: Thu, 7 Aug 2025 18:54:23 +0000 Subject: [PATCH 17/22] fix typo --- aixplain/enums/__init__.py | 2 +- tests/unit/agent/agent_test.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/aixplain/enums/__init__.py b/aixplain/enums/__init__.py index fa76405a..5c3a71f5 100644 --- a/aixplain/enums/__init__.py +++ b/aixplain/enums/__init__.py @@ -21,4 +21,4 @@ from .index_stores import IndexStores from .function_type import FunctionType from .evolve_type import EvolveType -from .code_interpeter import CodeInterpreterModel +from .code_interpreter import CodeInterpreterModel diff --git a/tests/unit/agent/agent_test.py b/tests/unit/agent/agent_test.py index fe623032..5a930c96 100644 --- a/tests/unit/agent/agent_test.py +++ b/tests/unit/agent/agent_test.py @@ -701,13 +701,13 @@ def test_create_agent_task(): assert task.name == "Test Task" assert task.description == "Test Description" assert task.expected_output == "Test Output" - assert task.dependencies == [] + assert task.dependencies is None task_dict = task.to_dict() assert task_dict["name"] == "Test Task" assert task_dict["description"] == "Test Description" assert task_dict["expectedOutput"] == "Test Output" - assert task_dict["dependencies"] == [] + assert task_dict["dependencies"] is None def test_agent_response(): From 4a2a07e29807cd6e77ef497fb5b57eeb25bf63e1 Mon Sep 17 00:00:00 2001 From: ahmetgunduz Date: Thu, 14 Aug 2025 16:55:38 +0000 Subject: [PATCH 18/22] role to instructions --- aixplain/factories/agent_factory/__init__.py | 3 ++- aixplain/factories/team_agent_factory/utils.py | 6 +++--- aixplain/modules/agent/__init__.py | 2 +- aixplain/modules/team_agent/__init__.py | 4 ++-- aixplain/utils/evolve_utils.py | 12 +++++++----- 5 files changed, 15 insertions(+), 12 deletions(-) diff --git a/aixplain/factories/agent_factory/__init__.py b/aixplain/factories/agent_factory/__init__.py index 4220851c..a6f54e43 100644 --- a/aixplain/factories/agent_factory/__init__.py +++ b/aixplain/factories/agent_factory/__init__.py @@ -80,7 +80,7 @@ def create( Args: name (Text): name of the agent - description (Text): description of the agent role. + description (Text): description of the agent instructions. instructions (Text): instructions of the agent. llm (Optional[Union[LLM, Text]], optional): LLM instance to use as an object or as an ID. llm_id (Optional[Text], optional): ID of LLM to use if no LLM instance provided. Defaults to None. @@ -205,6 +205,7 @@ def create_from_dict(cls, dict: Dict) -> Agent: agent.url = urljoin(config.BACKEND_URL, f"sdk/agents/{agent.id}/run") return agent + @classmethod def create_task( cls, name: Text, diff --git a/aixplain/factories/team_agent_factory/utils.py b/aixplain/factories/team_agent_factory/utils.py index 6a40f50a..c28d0a17 100644 --- a/aixplain/factories/team_agent_factory/utils.py +++ b/aixplain/factories/team_agent_factory/utils.py @@ -31,7 +31,7 @@ def build_team_agent(payload: Dict, agents: List[Agent] = None, api_key: Text = - name: Team agent name - agents: List of agent configurations - description: Optional description - - role: Optional instructions + - instructions: Optional instructions - teamId: Optional supplier information - version: Optional version - cost: Optional cost information @@ -204,11 +204,11 @@ def build_team_agent_from_yaml(yaml_code: str, llm_id: str, api_key: str, team_i # Parse agents for agent_entry in agents_data: for agent_name, agent_info in agent_entry.items(): - agent_role = agent_info["role"] + agent_instructions = agent_info["instructions"] agent_goal = agent_info["goal"] agent_backstory = agent_info["backstory"] - description = f"You are an expert {agent_role}. {agent_backstory} Your primary goal is to {agent_goal}. Use your expertise to ensure the success of your tasks." + description = f"You are an expert {agent_instructions}. {agent_backstory} Your primary goal is to {agent_goal}. Use your expertise to ensure the success of your tasks." agent_name = agent_name.replace("_", " ") agent_name = f"{agent_name} agent" if not agent_name.endswith(" agent") else agent_name agent_obj = Agent( diff --git a/aixplain/modules/agent/__init__.py b/aixplain/modules/agent/__init__.py index a2e066e0..6824b7f1 100644 --- a/aixplain/modules/agent/__init__.py +++ b/aixplain/modules/agent/__init__.py @@ -590,7 +590,7 @@ def from_dict(cls, data: Dict) -> "Agent": id=data["id"], name=data["name"], description=data["description"], - instructions=data.get("role"), + instructions=data.get("instructions"), tools=tools, llm_id=data.get("llmId", "6646261c6eb563165658bbb1"), llm=llm, diff --git a/aixplain/modules/team_agent/__init__.py b/aixplain/modules/team_agent/__init__.py index a718937d..ffb799a9 100644 --- a/aixplain/modules/team_agent/__init__.py +++ b/aixplain/modules/team_agent/__init__.py @@ -539,7 +539,7 @@ def to_dict(self) -> Dict: - supplier (str): The supplier code - version (str): The version number - status (str): The current status - - role (str): The team agent's instructions + - instructions (str): The team agent's instructions """ if self.use_mentalist: planner_id = self.mentalist_llm.id if self.mentalist_llm else self.llm_id @@ -660,7 +660,7 @@ def from_dict(cls, data: Dict) -> "TeamAgent": version=data.get("version"), use_mentalist=use_mentalist, status=status, - instructions=data.get("role"), + instructions=data.get("instructions"), inspectors=inspectors, inspector_targets=inspector_targets, output_format=OutputFormat(data.get("outputFormat", OutputFormat.TEXT)), diff --git a/aixplain/utils/evolve_utils.py b/aixplain/utils/evolve_utils.py index 5695819d..8e1b1c92 100644 --- a/aixplain/utils/evolve_utils.py +++ b/aixplain/utils/evolve_utils.py @@ -92,7 +92,7 @@ def from_yaml( for agent_entry in agents_data: # Handle different YAML structures if isinstance(agent_entry, dict): - # Case 1: Standard structure - {agent_name: {role: ..., goal: ..., backstory: ...}} + # Case 1: Standard structure - {agent_name: {instructions: ..., goal: ..., backstory: ...}} for agent_name, agent_info in agent_entry.items(): # Check if agent_info is a list (malformed YAML structure) if isinstance(agent_info, list): @@ -100,11 +100,11 @@ def from_yaml( # This happens when YAML has unquoted keys that create nested structures continue elif isinstance(agent_info, dict): - agent_role = agent_info["role"] + agent_instructions = agent_info["instructions"] agent_goal = agent_info["goal"] agent_backstory = agent_info["backstory"] - description = f"## ROLE\n{agent_role}\n\n## GOAL\n{agent_goal}\n\n## BACKSTORY\n{agent_backstory}" + description = f"## ROLE\n{agent_instructions}\n\n## GOAL\n{agent_goal}\n\n## BACKSTORY\n{agent_backstory}" agent_obj = AgentFactory.create( name=agent_name.replace("_", " "), description=description, @@ -120,11 +120,13 @@ def from_yaml( if isinstance(item, dict): for agent_name, agent_info in item.items(): if isinstance(agent_info, dict): - agent_role = agent_info["role"] + agent_instructions = agent_info["instructions"] agent_goal = agent_info["goal"] agent_backstory = agent_info["backstory"] - description = f"## ROLE\n{agent_role}\n\n## GOAL\n{agent_goal}\n\n## BACKSTORY\n{agent_backstory}" + description = ( + f"## ROLE\n{agent_instructions}\n\n## GOAL\n{agent_goal}\n\n## BACKSTORY\n{agent_backstory}" + ) agent_tools = generate_tools_from_str(agent_info.get("tools", [])) agent_obj = AgentFactory.create( name=agent_name.replace("_", " "), From cc8a31a4b28b0862a53e55f2b391c516f50274f3 Mon Sep 17 00:00:00 2001 From: ahmetgunduz Date: Thu, 14 Aug 2025 17:59:38 +0000 Subject: [PATCH 19/22] updated from dict method --- .../factories/team_agent_factory/utils.py | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/aixplain/factories/team_agent_factory/utils.py b/aixplain/factories/team_agent_factory/utils.py index c28d0a17..6f086342 100644 --- a/aixplain/factories/team_agent_factory/utils.py +++ b/aixplain/factories/team_agent_factory/utils.py @@ -203,25 +203,26 @@ def build_team_agent_from_yaml(yaml_code: str, llm_id: str, api_key: str, team_i # Parse agents for agent_entry in agents_data: - for agent_name, agent_info in agent_entry.items(): - agent_instructions = agent_info["instructions"] - agent_goal = agent_info["goal"] - agent_backstory = agent_info["backstory"] - - description = f"You are an expert {agent_instructions}. {agent_backstory} Your primary goal is to {agent_goal}. Use your expertise to ensure the success of your tasks." - agent_name = agent_name.replace("_", " ") - agent_name = f"{agent_name} agent" if not agent_name.endswith(" agent") else agent_name - agent_obj = Agent( - id="", - name=agent_name, - description=description, - instructions=description, - tasks=[], # Tasks will be assigned later - tools=[parse_tool_from_yaml(tool) for tool in agent_info.get("tools", []) if tool != "language_model"], - llmId=llm_id, - ) - agents_mapping[agent_name] = agent_obj - agent_objs.append(agent_obj) + agent_name = list(agent_entry.keys())[0] + agent_info = agent_entry[agent_name] + agent_instructions = agent_info["instructions"] + agent_goal = agent_info["goal"] + agent_backstory = agent_info["backstory"] + + description = f"You are an expert {agent_instructions}. {agent_backstory} Your primary goal is to {agent_goal}. Use your expertise to ensure the success of your tasks." + agent_name = agent_name.replace("_", " ") + agent_name = f"{agent_name} agent" if not agent_name.endswith(" agent") else agent_name + agent_obj = Agent( + id="", + name=agent_name, + description=description, + instructions=description, + tasks=[], # Tasks will be assigned later + tools=[parse_tool_from_yaml(tool) for tool in agent_info.get("tools", []) if tool != "language_model"], + llmId=llm_id, + ) + agents_mapping[agent_name] = agent_obj + agent_objs.append(agent_obj) # Parse tasks and assign them to the corresponding agents for task in tasks_data: From 8791cb2d1908efbaa4a1f6129ad4fc2dfde95076 Mon Sep 17 00:00:00 2001 From: ahmetgunduz Date: Thu, 14 Aug 2025 18:11:07 +0000 Subject: [PATCH 20/22] corrected response reading and updated inspectors key --- aixplain/factories/team_agent_factory/utils.py | 2 +- aixplain/modules/team_agent/__init__.py | 2 +- tests/functional/team_agent/evolver_test.py | 2 +- tests/functional/team_agent/team_agent_functional_test.py | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/aixplain/factories/team_agent_factory/utils.py b/aixplain/factories/team_agent_factory/utils.py index 6f086342..a66b0cce 100644 --- a/aixplain/factories/team_agent_factory/utils.py +++ b/aixplain/factories/team_agent_factory/utils.py @@ -264,5 +264,5 @@ def build_team_agent_from_yaml(yaml_code: str, llm_id: str, api_key: str, team_i llm_id=llm_id, api_key=api_key, use_mentalist=True, - use_inspector=False, + inspectors=[], ) diff --git a/aixplain/modules/team_agent/__init__.py b/aixplain/modules/team_agent/__init__.py index ffb799a9..bdc85169 100644 --- a/aixplain/modules/team_agent/__init__.py +++ b/aixplain/modules/team_agent/__init__.py @@ -881,7 +881,7 @@ def evolve( end = time.time() result = self.sync_poll(poll_url, name="evolve_process", timeout=600) result_data = result.data - current_code = result_data.get("current_code") + current_code = result_data.get("current_code") if isinstance(result_data, dict) else result_data.current_code if current_code is not None: if evolve_parameters.evolve_type == EvolveType.TEAM_TUNING: result_data["evolved_agent"] = from_yaml( diff --git a/tests/functional/team_agent/evolver_test.py b/tests/functional/team_agent/evolver_test.py index a1fe5834..4edf0c39 100644 --- a/tests/functional/team_agent/evolver_test.py +++ b/tests/functional/team_agent/evolver_test.py @@ -99,7 +99,7 @@ def build_team_agent_from_json(team_config: dict): agents=agent_objs, description=team_config["description"], llm_id=team_config.get("llm_id", None), - use_inspector=False, + inspectors=[], use_mentalist=True, ) diff --git a/tests/functional/team_agent/team_agent_functional_test.py b/tests/functional/team_agent/team_agent_functional_test.py index 5b9b4972..299eb0b2 100644 --- a/tests/functional/team_agent/team_agent_functional_test.py +++ b/tests/functional/team_agent/team_agent_functional_test.py @@ -288,7 +288,7 @@ def test_team_agent_with_instructions(delete_agents_and_team_agents): instructions="Use only 'Agent 2' to solve the tasks.", llm_id="6646261c6eb563165658bbb1", use_mentalist=True, - use_inspector=False, + inspectors=[], ) response = team_agent.run(data="Translate 'cat' to Portuguese") @@ -405,7 +405,7 @@ class Response(BaseModel): description="Team agent", llm_id="6646261c6eb563165658bbb1", use_mentalist=False, - use_inspector=False, + inspectors=[], ) # Run the team agent @@ -481,7 +481,7 @@ def test_team_agent_with_slack_connector(): description="Team agent", llm_id="6646261c6eb563165658bbb1", use_mentalist=False, - use_inspector=False, + inspectors=[], ) response = team_agent.run( From 7f2bdf4883ff93ad0f9f5f0d8eca77c135bd4256 Mon Sep 17 00:00:00 2001 From: ahmetgunduz Date: Mon, 18 Aug 2025 17:42:39 +0000 Subject: [PATCH 21/22] added evolve type check --- .../factories/team_agent_factory/utils.py | 40 +++++++++++++++++++ aixplain/modules/team_agent/__init__.py | 16 +++++++- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/aixplain/factories/team_agent_factory/utils.py b/aixplain/factories/team_agent_factory/utils.py index a66b0cce..46e6480d 100644 --- a/aixplain/factories/team_agent_factory/utils.py +++ b/aixplain/factories/team_agent_factory/utils.py @@ -186,10 +186,50 @@ def parse_tool_from_yaml(tool: str) -> ModelTool: raise Exception(f"Tool {tool} in yaml not found.") +import yaml + + +def is_yaml_formatted(text): + """ + Check if a string is valid YAML format with additional validation. + + Args: + text (str): The string to check + + Returns: + bool: True if valid YAML, False otherwise + """ + if not text or not isinstance(text, str): + return False + + # Strip whitespace + text = text.strip() + + # Empty string is valid YAML + if not text: + return True + + try: + parsed = yaml.safe_load(text) + + # If it's just a plain string without YAML structure, + # we might want to consider it as non-YAML + # This is optional depending on your requirements + if isinstance(parsed, str) and "\n" not in text and ":" not in text: + return False + + return True + except yaml.YAMLError: + return False + + def build_team_agent_from_yaml(yaml_code: str, llm_id: str, api_key: str, team_id: Optional[str] = None) -> TeamAgent: import yaml from aixplain.factories import AgentFactory, TeamAgentFactory + # check if it is a yaml or just as string + if not is_yaml_formatted(yaml_code): + return None team_config = yaml.safe_load(yaml_code) agents_data = team_config["agents"] diff --git a/aixplain/modules/team_agent/__init__.py b/aixplain/modules/team_agent/__init__.py index bdc85169..61287411 100644 --- a/aixplain/modules/team_agent/__init__.py +++ b/aixplain/modules/team_agent/__init__.py @@ -431,8 +431,18 @@ def poll(self, poll_url: Text, name: Text = "model_process") -> AgentResponse: resp_data = resp.get("data") or {} used_credits = resp_data.get("usedCredits", 0.0) run_time = resp_data.get("runTime", 0.0) + evolve_type = resp_data.get("evolve_type", EvolveType.TEAM_TUNING.value) if "evolved_agent" in resp_data and status == ResponseStatus.SUCCESS: - resp_data = EvolverResponseData.from_dict(resp_data, llm_id=self.llm_id, api_key=self.api_key) + if evolve_type == EvolveType.INSTRUCTION_TUNING.value: + # return this class as it is but replace its description and instructions + evolved_agent = self + current_code = resp_data.get("current_code", "") + evolved_agent.description = current_code + evolved_agent.instructions = current_code + evolved_agent.update() + resp_data["evolved_agent"] = evolved_agent + else: + resp_data = EvolverResponseData.from_dict(resp_data, llm_id=self.llm_id, api_key=self.api_key) else: resp_data = AgentResponseData( input=resp_data.get("input"), @@ -442,7 +452,9 @@ def poll(self, poll_url: Text, name: Text = "model_process") -> AgentResponse: execution_stats=resp_data.get("executionStats"), ) except Exception as e: - logging.error(f"Single Poll for Team Agent: Error of polling for {name}: {e}") + import traceback + + logging.error(f"Single Poll for Team Agent: Error of polling for {name}: {e}, traceback: {traceback.format_exc()}") status = ResponseStatus.FAILED error_message = str(e) finally: From de029bfcb1d4202e8b91735a848518ba51d3cde9 Mon Sep 17 00:00:00 2001 From: ahmetgunduz Date: Mon, 18 Aug 2025 17:45:28 +0000 Subject: [PATCH 22/22] added evolve type check --- aixplain/modules/team_agent/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/aixplain/modules/team_agent/__init__.py b/aixplain/modules/team_agent/__init__.py index 61287411..7faf3f64 100644 --- a/aixplain/modules/team_agent/__init__.py +++ b/aixplain/modules/team_agent/__init__.py @@ -902,6 +902,7 @@ def evolve( ) elif evolve_parameters.evolve_type == EvolveType.INSTRUCTION_TUNING: self.instructions = result_data["current_code"] + self.description = result_data["current_code"] self.update() result_data["evolved_agent"] = self else: