Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions aixplain/utils/user_info_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"""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.
"""

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."""
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@xainaz for on prem deployment this will not work. Assume there is no internet connection there. Lets add this as a defensive check if no internet connection keep it empty and do not shre in user info.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the user doesn't have internet connection they won't be able to run agents anyway right?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, they run locally. this is for on prem setup

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aix-ahmet Will the except here not be enough?
It returns an empty dict if there is any issues in getting the request.

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.

Returns:
Dict[str, str]: User metadata derived from the local client system.
"""
user_info = {"datetime": datetime.now().astimezone().isoformat()}
user_info.update(get_user_location())
return user_info
3 changes: 3 additions & 0 deletions aixplain/v1/modules/agent/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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),
Expand Down
3 changes: 3 additions & 0 deletions aixplain/v1/modules/team_agent/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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),
Expand Down
2 changes: 2 additions & 0 deletions aixplain/v2/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
24 changes: 24 additions & 0 deletions tests/unit/agent/agent_test.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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(
Expand Down