From 3186cd54a602110213467eb4e9707e53fc090aef Mon Sep 17 00:00:00 2001 From: "zaina.abushaban" Date: Thu, 19 Mar 2026 17:18:49 +0300 Subject: [PATCH 1/2] added datetime extracted --- aixplain/utils/user_info_utils.py | 28 ++++++++++++++++++++++ aixplain/v1/modules/agent/__init__.py | 3 +++ aixplain/v1/modules/team_agent/__init__.py | 3 +++ aixplain/v2/agent.py | 2 ++ tests/unit/agent/agent_test.py | 24 +++++++++++++++++++ 5 files changed, 60 insertions(+) create mode 100644 aixplain/utils/user_info_utils.py diff --git a/aixplain/utils/user_info_utils.py b/aixplain/utils/user_info_utils.py new file mode 100644 index 00000000..0cc6cde3 --- /dev/null +++ b/aixplain/utils/user_info_utils.py @@ -0,0 +1,28 @@ +"""Helpers for attaching client-side user metadata to execution payloads. + +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. +""" + +from datetime import datetime +from typing import Dict + + +def build_user_info() -> Dict[str, str]: + """Build user metadata for execution payloads. + + Returns: + Dict[str, str]: User metadata derived from the local client system. + """ + return {"datetime": datetime.now().astimezone().isoformat()} diff --git a/aixplain/v1/modules/agent/__init__.py b/aixplain/v1/modules/agent/__init__.py index adf4f734..96af8787 100644 --- a/aixplain/v1/modules/agent/__init__.py +++ b/aixplain/v1/modules/agent/__init__.py @@ -48,6 +48,7 @@ from urllib.parse import urljoin from aixplain.modules.model.llm_model import LLM from aixplain.utils.convert_datatype_utils import normalize_expected_output +from aixplain.utils.user_info_utils import build_user_info from aixplain.utils import config from aixplain.modules.mixins import DeployableMixin @@ -266,6 +267,7 @@ def generate_session_id(self, history: list = None) -> str: "query": "/", "sessionId": session_id, "history": history, + "userInfo": build_user_info(), "executionParams": { "maxTokens": 2048, "maxIterations": 10, @@ -835,6 +837,7 @@ def run_async( "query": input_data, "sessionId": session_id, "history": history, + "userInfo": build_user_info(), "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), diff --git a/aixplain/v1/modules/team_agent/__init__.py b/aixplain/v1/modules/team_agent/__init__.py index 8147332e..6ec88c62 100644 --- a/aixplain/v1/modules/team_agent/__init__.py +++ b/aixplain/v1/modules/team_agent/__init__.py @@ -52,6 +52,7 @@ from aixplain.utils.convert_datatype_utils import normalize_expected_output from aixplain.utils import config from aixplain.utils.request_utils import _request_with_retry +from aixplain.utils.user_info_utils import build_user_info from aixplain.modules.model.llm_model import LLM from aixplain.modules.mixins import DeployableMixin from pydantic import BaseModel @@ -262,6 +263,7 @@ def generate_session_id(self, history: list = None) -> str: "query": "/", "sessionId": session_id, "history": history, + "userInfo": build_user_info(), "executionParams": { "maxTokens": 2048, "maxIterations": 30, @@ -801,6 +803,7 @@ def run_async( "query": input_data, "sessionId": session_id, "history": history, + "userInfo": build_user_info(), "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), diff --git a/aixplain/v2/agent.py b/aixplain/v2/agent.py index 356a7020..fd1ee2f2 100644 --- a/aixplain/v2/agent.py +++ b/aixplain/v2/agent.py @@ -15,6 +15,7 @@ from .enums import AssetStatus, ResponseStatus from .model import Model from .mixins import ToolableMixin +from ..utils.user_info_utils import build_user_info from .resource import ( BaseResource, @@ -955,6 +956,7 @@ def build_run_payload(self, **kwargs: Unpack[AgentRunParams]) -> dict: "id": self.id, "executionParams": execution_params, "runResponseGeneration": run_response_generation, + "userInfo": build_user_info(), } # Add query back if present diff --git a/tests/unit/agent/agent_test.py b/tests/unit/agent/agent_test.py index ce08db4a..75e260f3 100644 --- a/tests/unit/agent/agent_test.py +++ b/tests/unit/agent/agent_test.py @@ -1,5 +1,6 @@ import pytest import requests_mock +from datetime import datetime from aixplain.factories import AgentFactory from aixplain.enums.asset_status import AssetStatus from aixplain.modules import Agent, Model @@ -552,6 +553,29 @@ def test_run_success(): assert response["url"] == ref_response["data"] +def test_run_async_includes_user_info_datetime(): + agent = Agent( + "123", + "Test Agent(-)", + "Sample Description", + instructions="Test Agent Instructions", + ) + run_url = urljoin(config.BACKEND_URL, f"sdk/agents/{agent.id}/run") + agent.url = run_url + + with requests_mock.Mocker() as mock: + headers = {"x-api-key": config.AIXPLAIN_API_KEY, "Content-Type": "application/json"} + mock.post(run_url, headers=headers, json={"data": "www.aixplain.com", "status": "IN_PROGRESS"}) + + agent.run_async(data={"query": "Hello, how are you?"}) + + sent = mock.last_request.json() + assert "userInfo" in sent + assert "datetime" in sent["userInfo"] + assert isinstance(sent["userInfo"]["datetime"], str) + datetime.fromisoformat(sent["userInfo"]["datetime"]) + + def test_run_variable_missing(): """Test that agent runs successfully even when variables are missing from data/parameters.""" agent = Agent( From ac297246ea5c6875e0b32c5ece4e526e3490e6af Mon Sep 17 00:00:00 2001 From: "zaina.abushaban" Date: Wed, 1 Apr 2026 12:15:33 +0300 Subject: [PATCH 2/2] added user region and country --- aixplain/utils/user_info_utils.py | 41 ++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/aixplain/utils/user_info_utils.py b/aixplain/utils/user_info_utils.py index 0cc6cde3..9238e724 100644 --- a/aixplain/utils/user_info_utils.py +++ b/aixplain/utils/user_info_utils.py @@ -15,9 +15,46 @@ limitations under the License. """ +import logging from datetime import datetime +from functools import lru_cache from typing import Dict +import requests + + +logger = logging.getLogger(__name__) + +_IPINFO_URL = "https://ipinfo.io/json" +_IPINFO_TIMEOUT = 2.0 + + +@lru_cache(maxsize=1) +def get_user_location() -> Dict[str, str]: + """Fetch and cache the user's region and country from ipinfo.io.""" + try: + response = requests.get(_IPINFO_URL, timeout=_IPINFO_TIMEOUT) + response.raise_for_status() + payload = response.json() + except (requests.RequestException, ValueError) as exc: + logger.debug("Failed to fetch user location from ipinfo.io: %s", exc) + return {} + + if not isinstance(payload, dict): + return {} + + location: Dict[str, str] = {} + + region = payload.get("region") + if isinstance(region, str) and region.strip(): + location["region"] = region.strip() + + country = payload.get("country") + if isinstance(country, str) and country.strip(): + location["country"] = country.strip() + + return location + def build_user_info() -> Dict[str, str]: """Build user metadata for execution payloads. @@ -25,4 +62,6 @@ def build_user_info() -> Dict[str, str]: Returns: Dict[str, str]: User metadata derived from the local client system. """ - return {"datetime": datetime.now().astimezone().isoformat()} + user_info = {"datetime": datetime.now().astimezone().isoformat()} + user_info.update(get_user_location()) + return user_info