From 2d79899bca1e283f092610e7968bf70e7fe4ad08 Mon Sep 17 00:00:00 2001 From: "zaina.abushaban" Date: Mon, 21 Jul 2025 16:51:53 +0300 Subject: [PATCH 1/8] session id agent init changes --- aixplain/modules/agent/__init__.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/aixplain/modules/agent/__init__.py b/aixplain/modules/agent/__init__.py index e3ca6f89..5a61a00c 100644 --- a/aixplain/modules/agent/__init__.py +++ b/aixplain/modules/agent/__init__.py @@ -25,6 +25,7 @@ import re import time import traceback +from datetime import datetime from aixplain.utils.file_utils import _request_with_retry from aixplain.enums import Function, Supplier, AssetStatus, StorageType, ResponseStatus @@ -156,6 +157,16 @@ def validate(self, raise_exception: bool = False) -> bool: logging.warning(f"Agent Validation Error: {e}") logging.warning("You won't be able to run the Agent until the issues are handled manually.") return self.is_valid + + def generate_session_id(self, history: list = None) -> str: + timestamp = datetime.now().strftime('%Y%m%d%H%M%S') + session_id = f"{self.id}_{timestamp}" + + if history: + resp=self.run(query=str(history), session_id=session_id) + logging.info(resp.data.output) + + return session_id def run( self, @@ -193,6 +204,13 @@ def run( Dict: parsed output from model """ start = time.time() + if session_id is not None and history is not None: + raise ValueError("Provide either `session_id` or `history`, not both.") + + if session_id is not None: + if not session_id.startswith(f"{self.id}_"): + raise ValueError(f"Session ID '{session_id}' does not belong to this Agent.") + result_data = {} try: response = self.run_async( @@ -278,6 +296,14 @@ def run_async( Returns: dict: polling URL in response """ + + if session_id is not None and history is not None: + raise ValueError("Provide either `session_id` or `history`, not both.") + + if session_id is not None: + if not session_id.startswith(f"{self.id}_"): + raise ValueError(f"Session ID '{session_id}' does not belong to this Agent.") + from aixplain.factories.file_factory import FileFactory if not self.is_valid: From d84cc114e3e25e8265da20090c6fc4f3b5b98d48 Mon Sep 17 00:00:00 2001 From: "zaina.abushaban" Date: Thu, 24 Jul 2025 13:23:44 +0300 Subject: [PATCH 2/8] history check --- aixplain/modules/agent/__init__.py | 10 ++++++++-- aixplain/modules/agent/utils.py | 26 +++++++++++++++++++++++++ aixplain/modules/team_agent/__init__.py | 7 ++++++- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/aixplain/modules/agent/__init__.py b/aixplain/modules/agent/__init__.py index 5a61a00c..9364bd87 100644 --- a/aixplain/modules/agent/__init__.py +++ b/aixplain/modules/agent/__init__.py @@ -35,7 +35,7 @@ from aixplain.modules.agent.tool import Tool 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.agent.utils import process_variables, validate_history from pydantic import BaseModel from typing import Dict, List, Text, Optional, Union from urllib.parse import urljoin @@ -159,6 +159,8 @@ def validate(self, raise_exception: bool = False) -> bool: return self.is_valid def generate_session_id(self, history: list = None) -> str: + if history: + validate_history(history) timestamp = datetime.now().strftime('%Y%m%d%H%M%S') session_id = f"{self.id}_{timestamp}" @@ -210,7 +212,8 @@ def run( if session_id is not None: if not session_id.startswith(f"{self.id}_"): raise ValueError(f"Session ID '{session_id}' does not belong to this Agent.") - + if history: + validate_history(history) result_data = {} try: response = self.run_async( @@ -303,6 +306,9 @@ def run_async( if session_id is not None: if not session_id.startswith(f"{self.id}_"): raise ValueError(f"Session ID '{session_id}' does not belong to this Agent.") + + if history: + validate_history(history) from aixplain.factories.file_factory import FileFactory diff --git a/aixplain/modules/agent/utils.py b/aixplain/modules/agent/utils.py index 684c82db..14e19860 100644 --- a/aixplain/modules/agent/utils.py +++ b/aixplain/modules/agent/utils.py @@ -29,3 +29,29 @@ def process_variables( input_data[variable] = parameters.pop(variable) return input_data + + +def validate_history(history): + """ + Validates that `history` is a list of dicts, each with 'role' and 'content' keys. + Raises a ValueError if validation fails. + """ + if not isinstance(history, list): + raise ValueError("History must be a list of message dictionaries.") + + allowed_roles = {"user", "assistant"} + + for i, item in enumerate(history): + if not isinstance(item, dict): + raise ValueError(f"History item at index {i} is not a dict: {item}") + + if "role" not in item or "content" not in item: + raise ValueError(f"History item at index {i} is missing 'role' or 'content': {item}") + + if item["role"] not in allowed_roles: + raise ValueError(f"Invalid role '{item['role']}' at index {i}. Allowed roles: {allowed_roles}") + + if not isinstance(item["content"], str): + raise ValueError(f"'content' at index {i} must be a string. Got: {type(item['content'])}") + + return True diff --git a/aixplain/modules/team_agent/__init__.py b/aixplain/modules/team_agent/__init__.py index fa1aca9e..11160593 100644 --- a/aixplain/modules/team_agent/__init__.py +++ b/aixplain/modules/team_agent/__init__.py @@ -39,7 +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.utils import process_variables +from aixplain.modules.agent.utils import process_variables, validate_history from aixplain.modules.team_agent.inspector import Inspector from aixplain.utils import config from aixplain.utils.request_utils import _request_with_retry @@ -155,6 +155,8 @@ def run( """ start = time.time() result_data = {} + if history: + validate_history(history) try: response = self.run_async( data=data, @@ -231,6 +233,9 @@ def run_async( Returns: dict: polling URL in response """ + if history: + validate_history(history) + from aixplain.factories.file_factory import FileFactory if not self.is_valid: From e3874de9529852910dc10e6afeddffefa67a9c79 Mon Sep 17 00:00:00 2001 From: "zaina.abushaban" Date: Fri, 25 Jul 2025 12:09:25 +0300 Subject: [PATCH 3/8] history validation --- aixplain/modules/team_agent/__init__.py | 129 +++++++++++++++++++++++- 1 file changed, 125 insertions(+), 4 deletions(-) diff --git a/aixplain/modules/team_agent/__init__.py b/aixplain/modules/team_agent/__init__.py index 11160593..9e714da5 100644 --- a/aixplain/modules/team_agent/__init__.py +++ b/aixplain/modules/team_agent/__init__.py @@ -340,6 +340,29 @@ def delete(self) -> None: logging.error(message) raise Exception(f"{message}") + def _serialize_agent(self, agent, idx: int) -> Dict: + """Serialize an agent for the to_dict method.""" + base_dict = {"assetId": agent.id, "number": idx, "type": "AGENT", "label": "AGENT"} + + # Try to get additional data from agent's to_dict method + try: + if hasattr(agent, "to_dict") and callable(getattr(agent, "to_dict")): + agent_dict = agent.to_dict() + # Ensure it's actually a dictionary and not a Mock or other object + if isinstance(agent_dict, dict) and hasattr(agent_dict, "items"): + try: + # Add all fields except 'id' to avoid duplication with 'assetId' + additional_data = {k: v for k, v in agent_dict.items() if k not in ["id"]} + base_dict.update(additional_data) + except (TypeError, AttributeError): + # If items() doesn't work or iteration fails, skip the additional data + pass + except Exception: + # If anything goes wrong, just use the base dictionary + pass + + return base_dict + def to_dict(self) -> Dict: if self.use_mentalist: planner_id = self.mentalist_llm.id if self.mentalist_llm else self.llm_id @@ -348,9 +371,7 @@ def to_dict(self) -> Dict: return { "id": self.id, "name": self.name, - "agents": [ - {"assetId": agent.id, "number": idx, "type": "AGENT", "label": "AGENT"} for idx, agent in enumerate(self.agents) - ], + "agents": [self._serialize_agent(agent, idx) for idx, agent in enumerate(self.agents)], "links": [], "description": self.description, "llmId": self.llm.id if self.llm else self.llm_id, @@ -364,6 +385,107 @@ def to_dict(self) -> Dict: "role": self.instructions, } + @classmethod + def from_dict(cls, data: Dict) -> "TeamAgent": + """Create a TeamAgent instance from a dictionary representation. + + Args: + data: Dictionary containing TeamAgent parameters + + Returns: + TeamAgent instance + """ + from aixplain.factories.agent_factory import AgentFactory + from aixplain.factories.model_factory import ModelFactory + from aixplain.enums import AssetStatus + from aixplain.modules.team_agent import Inspector, InspectorTarget + + # Extract agents from agents list using proper agent loading + agents = [] + if "agents" in data: + for agent_data in data["agents"]: + if "assetId" in agent_data: + try: + # Load agent using AgentFactory + agent = AgentFactory.get(agent_data["assetId"]) + agents.append(agent) + except Exception as e: + # Log warning but continue processing other agents + import logging + + logging.warning(f"Failed to load agent {agent_data['assetId']}: {e}") + + # Extract inspectors using proper model validation + inspectors = [] + if "inspectors" in data: + for inspector_data in data["inspectors"]: + try: + if hasattr(Inspector, "model_validate"): + inspectors.append(Inspector.model_validate(inspector_data)) + else: + inspectors.append(Inspector(**inspector_data)) + except Exception as e: + import logging + + logging.warning(f"Failed to create inspector from data: {e}") + + # Extract inspector targets + inspector_targets = [InspectorTarget.STEPS] # default + if "inspectorTargets" in data: + inspector_targets = [InspectorTarget(target) for target in data["inspectorTargets"]] + + # Extract status + status = AssetStatus.DRAFT + if "status" in data: + if isinstance(data["status"], str): + status = AssetStatus(data["status"]) + else: + status = data["status"] + + # Extract LLM instances using proper model loading + llm = None + supervisor_llm = None + mentalist_llm = None + + try: + if "llmId" in data: + llm = ModelFactory.get(data["llmId"]) + except Exception: + pass # llm remains None, will use llm_id + + try: + if "supervisorId" in data and data["supervisorId"] != data.get("llmId"): + supervisor_llm = ModelFactory.get(data["supervisorId"]) + except Exception: + pass # supervisor_llm remains None + + try: + if "plannerId" in data and data["plannerId"]: + mentalist_llm = ModelFactory.get(data["plannerId"]) + except Exception: + pass # mentalist_llm remains None + + # Determine if mentalist is used + use_mentalist = data.get("plannerId") is not None + + return cls( + id=data["id"], + name=data["name"], + agents=agents, + description=data.get("description", ""), + llm_id=data.get("llmId", "6646261c6eb563165658bbb1"), + llm=llm, + supervisor_llm=supervisor_llm, + mentalist_llm=mentalist_llm, + supplier=data.get("supplier", "aiXplain"), + version=data.get("version"), + use_mentalist=use_mentalist, + status=status, + instructions=data.get("role"), + inspectors=inspectors, + inspector_targets=inspector_targets, + ) + def _validate(self) -> None: from aixplain.utils.llm_utils import get_llm_instance @@ -434,4 +556,3 @@ def update(self) -> None: def __repr__(self): return f"TeamAgent: {self.name} (id={self.id})" - From 1c6e1972091290c954b8b226d0e3f1b426176989 Mon Sep 17 00:00:00 2001 From: "zaina.abushaban" Date: Tue, 29 Jul 2025 17:20:38 +0300 Subject: [PATCH 4/8] Changes for history structure --- aixplain/modules/agent/__init__.py | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/aixplain/modules/agent/__init__.py b/aixplain/modules/agent/__init__.py index 9364bd87..18abccfe 100644 --- a/aixplain/modules/agent/__init__.py +++ b/aixplain/modules/agent/__init__.py @@ -165,11 +165,28 @@ def generate_session_id(self, history: list = None) -> str: session_id = f"{self.id}_{timestamp}" if history: - resp=self.run(query=str(history), session_id=session_id) - logging.info(resp.data.output) + response = self.run_async( + query="/", + history=history, + session_id=session_id, + allow_history_and_session_id=True + ) + + if response.status == ResponseStatus.FAILED: + logging.error(f"Failed to initialize session {session_id}: {response.error}") + return session_id + poll_url = response.url + result = self.sync_poll(poll_url, name="model_process", timeout=300, wait_time=0.5) + + if result.get("status") == ResponseStatus.SUCCESS: + return session_id + else: + logging.error(f"Session {session_id} initialization failed: {result}") + return session_id + def run( self, data: Optional[Union[Dict, Text]] = None, @@ -281,6 +298,7 @@ def run_async( max_iterations: int = 10, output_format: OutputFormat = OutputFormat.TEXT, expected_output: Optional[Union[BaseModel, Text, dict]] = None, + allow_history_and_session_id: Optional[bool] = False ) -> AgentResponse: """Runs asynchronously an agent call. @@ -300,7 +318,7 @@ def run_async( dict: polling URL in response """ - if session_id is not None and history is not None: + if session_id is not None and history is not None and not allow_history_and_session_id: raise ValueError("Provide either `session_id` or `history`, not both.") if session_id is not None: @@ -370,8 +388,8 @@ def run_async( "outputFormat": output_format, "expectedOutput": expected_output, }, + "allowHistoryAndSessionId": allow_history_and_session_id } - payload.update(parameters) payload = json.dumps(payload) From 3c92f8f931b909bff984bf8e75f3dae075fd61a3 Mon Sep 17 00:00:00 2001 From: "zaina.abushaban" Date: Thu, 7 Aug 2025 11:50:56 +0300 Subject: [PATCH 5/8] added example to history validation error message --- aixplain/modules/agent/utils.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/aixplain/modules/agent/utils.py b/aixplain/modules/agent/utils.py index 14e19860..5674aee8 100644 --- a/aixplain/modules/agent/utils.py +++ b/aixplain/modules/agent/utils.py @@ -37,7 +37,11 @@ def validate_history(history): Raises a ValueError if validation fails. """ if not isinstance(history, list): - raise ValueError("History must be a list of message dictionaries.") + raise ValueError( + "History must be a list of message dictionaries. " + "Example: [{'role': 'user', 'content': 'Hello'}, {'role': 'assistant', 'content': 'Hi there!'}]" + ) + allowed_roles = {"user", "assistant"} From fc6a2b64694cabdf4b94ee36f662eb39eb86b95a Mon Sep 17 00:00:00 2001 From: "zaina.abushaban" Date: Thu, 7 Aug 2025 11:52:49 +0300 Subject: [PATCH 6/8] Added examples to all error messages --- aixplain/modules/agent/utils.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/aixplain/modules/agent/utils.py b/aixplain/modules/agent/utils.py index 5674aee8..09076014 100644 --- a/aixplain/modules/agent/utils.py +++ b/aixplain/modules/agent/utils.py @@ -47,15 +47,27 @@ def validate_history(history): for i, item in enumerate(history): if not isinstance(item, dict): - raise ValueError(f"History item at index {i} is not a dict: {item}") - + raise ValueError( + f"History item at index {i} is not a dict: {item}. " + "Each item must be a dictionary like: {'role': 'user', 'content': 'Hello'}" + ) + if "role" not in item or "content" not in item: - raise ValueError(f"History item at index {i} is missing 'role' or 'content': {item}") + raise ValueError( + f"History item at index {i} is missing 'role' or 'content': {item}. " + "Example of a valid message: {'role': 'assistant', 'content': 'Hi there!'}" + ) if item["role"] not in allowed_roles: - raise ValueError(f"Invalid role '{item['role']}' at index {i}. Allowed roles: {allowed_roles}") + raise ValueError( + f"Invalid role '{item['role']}' at index {i}. Allowed roles: {allowed_roles}. " + "Example: {'role': 'user', 'content': 'Tell me a joke'}" + ) if not isinstance(item["content"], str): - raise ValueError(f"'content' at index {i} must be a string. Got: {type(item['content'])}") + raise ValueError( + f"'content' at index {i} must be a string. Got: {type(item['content'])}. " + "Example: {'role': 'assistant', 'content': 'Sure! Here’s one...'}" + ) return True From 3a90e5e2f824cb9672d3c4feb356ce8b932c8f7c Mon Sep 17 00:00:00 2001 From: "zaina.abushaban" Date: Mon, 11 Aug 2025 11:01:27 +0300 Subject: [PATCH 7/8] fixed allow both session id and history --- aixplain/modules/agent/__init__.py | 45 +++++++++++++++++++----------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/aixplain/modules/agent/__init__.py b/aixplain/modules/agent/__init__.py index 18abccfe..2e758989 100644 --- a/aixplain/modules/agent/__init__.py +++ b/aixplain/modules/agent/__init__.py @@ -164,27 +164,42 @@ def generate_session_id(self, history: list = None) -> str: timestamp = datetime.now().strftime('%Y%m%d%H%M%S') session_id = f"{self.id}_{timestamp}" - if history: - response = self.run_async( - query="/", - history=history, - session_id=session_id, - allow_history_and_session_id=True - ) + if not history: + return session_id - if response.status == ResponseStatus.FAILED: - logging.error(f"Failed to initialize session {session_id}: {response.error}") - return session_id + try: + validate_history(history) + headers = {"x-api-key": self.api_key, "Content-Type": "application/json"} + + payload = { + "id": self.id, + "query": "/", + "sessionId": session_id, + "history": history, + "executionParams": { + "maxTokens": 2048, + "maxIterations": 10, + "outputFormat": OutputFormat.TEXT.value, + "expectedOutput": None, + }, + "allowHistoryAndSessionId": True + } + + r = _request_with_retry("post", self.url, headers=headers, data=json.dumps(payload)) + resp = r.json() + poll_url = resp.get("data") - poll_url = response.url result = self.sync_poll(poll_url, name="model_process", timeout=300, wait_time=0.5) if result.get("status") == ResponseStatus.SUCCESS: return session_id else: logging.error(f"Session {session_id} initialization failed: {result}") - - return session_id + return session_id + + except Exception as e: + logging.error(f"Failed to initialize session {session_id}: {e}") + return session_id def run( @@ -298,7 +313,6 @@ def run_async( max_iterations: int = 10, output_format: OutputFormat = OutputFormat.TEXT, expected_output: Optional[Union[BaseModel, Text, dict]] = None, - allow_history_and_session_id: Optional[bool] = False ) -> AgentResponse: """Runs asynchronously an agent call. @@ -318,7 +332,7 @@ def run_async( dict: polling URL in response """ - if session_id is not None and history is not None and not allow_history_and_session_id: + if session_id is not None and history is not None: raise ValueError("Provide either `session_id` or `history`, not both.") if session_id is not None: @@ -388,7 +402,6 @@ def run_async( "outputFormat": output_format, "expectedOutput": expected_output, }, - "allowHistoryAndSessionId": allow_history_and_session_id } payload.update(parameters) payload = json.dumps(payload) From 8045687fce2a3c69eed0bb6b6b0a39094312cfbe Mon Sep 17 00:00:00 2001 From: "zaina.abushaban" Date: Mon, 11 Aug 2025 11:17:12 +0300 Subject: [PATCH 8/8] changes to team agents --- aixplain/modules/team_agent/__init__.py | 55 +++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/aixplain/modules/team_agent/__init__.py b/aixplain/modules/team_agent/__init__.py index 9e714da5..bbee70e2 100644 --- a/aixplain/modules/team_agent/__init__.py +++ b/aixplain/modules/team_agent/__init__.py @@ -29,6 +29,7 @@ from enum import Enum from typing import Dict, List, Text, Optional, Union from urllib.parse import urljoin +from datetime import datetime from aixplain.enums import ResponseStatus from aixplain.enums.function import Function @@ -118,6 +119,47 @@ def __init__( self.status = status self.is_valid = True + def generate_session_id(self, history: list = None) -> str: + timestamp = datetime.now().strftime('%Y%m%d%H%M%S') + session_id = f"{self.id}_{timestamp}" + + if not history: + return session_id + + try: + validate_history(history) + headers = {"x-api-key": self.api_key, "Content-Type": "application/json"} + + payload = { + "id": self.id, + "query": "/", + "sessionId": session_id, + "history": history, + "executionParams": { + "maxTokens": 2048, + "maxIterations": 30, + "outputFormat": OutputFormat.TEXT.value, + "expectedOutput": None, + }, + "allowHistoryAndSessionId": True + } + + r = _request_with_retry("post", self.url, headers=headers, data=json.dumps(payload)) + resp = r.json() + poll_url = resp.get("data") + + result = self.sync_poll(poll_url, name="model_process", timeout=300, wait_time=0.5) + + if result.get("status") == ResponseStatus.SUCCESS: + return session_id + else: + logging.error(f"Team session init failed for {session_id}: {result}") + return session_id + except Exception as e: + logging.error(f"Failed to initialize team session {session_id}: {e}") + return session_id + + def run( self, data: Optional[Union[Dict, Text]] = None, @@ -155,6 +197,12 @@ def run( """ start = time.time() result_data = {} + if session_id is not None and history is not None: + raise ValueError("Provide either `session_id` or `history`, not both.") + + if session_id is not None: + if not session_id.startswith(f"{self.id}_"): + raise ValueError(f"Session ID '{session_id}' does not belong to this Agent.") if history: validate_history(history) try: @@ -233,6 +281,13 @@ def run_async( Returns: dict: polling URL in response """ + if session_id is not None and history is not None: + raise ValueError("Provide either `session_id` or `history`, not both.") + + if session_id is not None: + if not session_id.startswith(f"{self.id}_"): + raise ValueError(f"Session ID '{session_id}' does not belong to this Agent.") + if history: validate_history(history)