Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
173 changes: 171 additions & 2 deletions app/agents/voice/automatic/tools/juspay/analytics.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import functools
import json
from datetime import datetime
from datetime import datetime, timezone

import httpx
import pytz
Expand All @@ -13,7 +13,11 @@
ApiSuccess,
GeniusApiResponse,
)
from app.core.config import EULER_DASHBOARD_API_URL, GENIUS_API_URL
from app.core.config import (
EULER_DASHBOARD_API_URL,
GENIUS_API_URL,
GENIUS_LIST_ORDERS_API,
)
from app.core.logger import logger
from app.core.transport.http_client import create_http_client

Expand Down Expand Up @@ -155,6 +159,142 @@ async def _make_genius_api_request(
)


async def _make_list_orders_api_request(
params: FunctionCallParams, payload_details: dict
) -> GeniusApiResponse:
"""
Generic helper to make requests to the Juspay List Orders API.
Returns a GeniusApiResponse object.
"""
if not euler_token:
logger.error(
"Tool Error: [list_orders_request] Juspay tool called without required euler_token."
)
return ApiFailure(
error={"Tool Error": "[list_orders_request] Juspay tool is not configured."}
)

start_time_str = params.arguments.get("startTime")
end_time_str = params.arguments.get("endTime")

try:
ist = pytz.timezone("Asia/Kolkata")
utc = pytz.utc
if not start_time_str:
now_ist = datetime.now(ist)
start_time_ist = now_ist.replace(hour=0, minute=0, second=0, microsecond=0)
else:
start_time_ist = ist.localize(
datetime.strptime(start_time_str, "%Y-%m-%d %H:%M:%S")
)
start_time_utc = start_time_ist.astimezone(utc)

if end_time_str:
end_time_ist = ist.localize(
datetime.strptime(end_time_str, "%Y-%m-%d %H:%M:%S")
)
else:
end_time_ist = datetime.now(ist)
end_time_utc = end_time_ist.astimezone(utc)

except Exception as e:
logger.error(
f"Tool Error: [list_orders_request] Error converting time for Juspay API: {e}"
)
return ApiFailure(
error={
"Tool Error": f" [list_orders_request] Invalid time format provided. Please use 'YYYY-MM-DD HH:MM:SS' in IST. Error: {e}"
}
)

time_field = (
"order_created_at"
if payload_details.get("domain") == "ordersELS"
else "date_created"
)

end_time_str = end_time_utc.strftime("%Y-%m-%dT%H:%M:%SZ")
start_time_str = start_time_utc.strftime("%Y-%m-%dT%H:%M:%SZ")

end_time_dt = end_time_utc.fromisoformat(end_time_str.replace("Z", "+00:00"))
start_time_dt = start_time_utc.fromisoformat(start_time_str.replace("Z", "+00:00"))

if end_time_dt.tzinfo is None:
end_time_dt = end_time_dt.replace(tzinfo=timezone.utc)
if start_time_dt.tzinfo is None:
start_time_dt = start_time_dt.replace(tzinfo=timezone.utc)

date_from_ts = int(start_time_dt.timestamp())
date_to_ts = int(end_time_dt.timestamp())

qFilters = {
"and": {
"right": {
"field": time_field,
"condition": "LessThanEqual",
"val": str(date_to_ts),
},
"left": {
"field": time_field,
"condition": "GreaterThanEqual",
"val": str(date_from_ts),
},
}
}

full_payload = {
**payload_details,
"filters": {"dateCreated": {"lte": end_time_str, "gte": start_time_str}},
"qFilters": qFilters,
}
headers = {
"Content-Type": "application/json",
"x-web-logintoken": euler_token,
}

logger.info(
f"Requesting Juspay List Orders API with payload: {json.dumps(full_payload)}"
)

try:
async with create_http_client(timeout=10.0) as client:
response = await client.post(
GENIUS_LIST_ORDERS_API, json=full_payload, headers=headers
)
response.raise_for_status()
response_text = response.text
logger.info(f"Received Raw Juspay API text response: {response_text}")
return ApiSuccess(data=response_text)
except httpx.TimeoutException:
logger.error(
"Tool Error: [list_orders_api_request] Juspay API request timed out after 10 seconds."
)
return ApiFailure(
error={
"Tool Error": "[list_orders_api_request] It is taking too much time to process. Please try again."
}
)
except httpx.HTTPStatusError as e:
logger.error(
f"Tool Error: [list_orders_api_request] HTTP error calling Juspay API: {e.response.status_code} - {e.response.text}"
)
return ApiFailure(
error={
"Tool Error": f" [list_orders_api_request] Juspay API error: {e.response.status_code}",
"details": e.response.text,
}
)
except Exception as e:
logger.error(
f"Tool Error: [list_orders_api_request] Unexpected error calling Juspay API: {e}"
)
return ApiFailure(
error={
"Tool Error": f" [list_orders_api_request] An unexpected error occurred: {e}"
}
)

Comment on lines 162 to 264
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

List Orders helper: tighten time handling, error typing, and logging/redaction.

  • Replace string round‑trip with direct microsecond truncation; add httpx.RequestError catch; avoid logging full responses (PII risk); unify error tags; add UA header.

Apply this diff:

@@
-    time_field = (
+    time_field = (
         "order_created_at"
         if payload_details.get("domain") == "ordersELS"
         else "date_created"
     )
 
-    end_time_str = end_time_utc.strftime("%Y-%m-%dT%H:%M:%SZ")
-    start_time_str = start_time_utc.strftime("%Y-%m-%dT%H:%M:%SZ")
-
-    end_time_dt = end_time_utc.fromisoformat(end_time_str.replace("Z", "+00:00"))
-    start_time_dt = start_time_utc.fromisoformat(start_time_str.replace("Z", "+00:00"))
-
-    if end_time_dt.tzinfo is None:
-        end_time_dt = end_time_dt.replace(tzinfo=timezone.utc)
-    if start_time_dt.tzinfo is None:
-        start_time_dt = start_time_dt.replace(tzinfo=timezone.utc)
+    end_time_str = end_time_utc.strftime("%Y-%m-%dT%H:%M:%SZ")
+    start_time_str = start_time_utc.strftime("%Y-%m-%dT%H:%M:%SZ")
+    # Drop microseconds without reparsing strings
+    end_time_dt = end_time_utc.replace(microsecond=0)
+    start_time_dt = start_time_utc.replace(microsecond=0)
@@
     full_payload = {
         **payload_details,
         "filters": {"dateCreated": {"lte": end_time_str, "gte": start_time_str}},
         "qFilters": qFilters,
     }
     headers = {
         "Content-Type": "application/json",
         "x-web-logintoken": euler_token,
+        "user-agent": "ClairvoyanceApp/1.0",
     }
@@
-            response_text = response.text
-            logger.info(f"Received Raw Juspay API text response: {response_text}")
-            return ApiSuccess(data=response_text)
+            response_text = response.text
+            # Avoid logging full order payloads (may contain PII)
+            logger.debug(f"Received Juspay List Orders response (len={len(response_text)})")
+            return ApiSuccess(data=response_text)
     except httpx.TimeoutException:
-        logger.error(
-            "Tool Error: [list_orders_api_request] Juspay API request timed out after 10 seconds."
-        )
+        logger.error("Tool Error: [list_orders_request] Juspay API request timed out after 10 seconds.")
         return ApiFailure(
             error={
-                "Tool Error": "[list_orders_api_request] It is taking too much time to process. Please try again."
+                "Tool Error": "[list_orders_request] It is taking too much time to process. Please try again."
             }
         )
+    except httpx.RequestError as e:
+        logger.error(f"Tool Error: [list_orders_request] Network error calling Juspay API: {e}")
+        return ApiFailure(
+            error={
+                "Tool Error": "[list_orders_request] Network error while contacting Juspay API.",
+                "details": str(e),
+            }
+        )
     except httpx.HTTPStatusError as e:
-        logger.error(
-            f"Tool Error: [list_orders_api_request] HTTP error calling Juspay API: {e.response.status_code} - {e.response.text}"
-        )
+        logger.error(f"Tool Error: [list_orders_request] HTTP error calling Juspay API: {e.response.status_code} - {e.response.text}")
         return ApiFailure(
             error={
-                "Tool Error": f" [list_orders_api_request] Juspay API error: {e.response.status_code}",
+                "Tool Error": f" [list_orders_request] Juspay API error: {e.response.status_code}",
                 "details": e.response.text,
             }
         )
     except Exception as e:
-        logger.error(
-            f"Tool Error: [list_orders_api_request] Unexpected error calling Juspay API: {e}"
-        )
+        logger.error(f"Tool Error: [list_orders_request] Unexpected error calling Juspay API: {e}", exc_info=True)
         return ApiFailure(
             error={
-                "Tool Error": f" [list_orders_api_request] An unexpected error occurred: {e}"
+                "Tool Error": f" [list_orders_request] An unexpected error occurred: {e}"
             }
         )

Note: Consider confirming the necessity/contract of both "filters.dateCreated" (camelCase) and "qFilters.date_created" for ec/v4/orders; keep only the supported one to reduce ambiguity.


🏁 Script executed:

#!/bin/bash
file="app/agents/voice/automatic/tools/juspay/analytics.py"
sed -n '1,50p' "$file"

Length of output: 1353


🏁 Script executed:

#!/bin/bash
file="app/agents/voice/automatic/tools/juspay/analytics.py"
grep -n "async def _make_list_orders_api_request" "$file"
sed -n '150,330p' "$file"

Length of output: 6229


Refactor List Orders helper: microsecond truncation, error handling, logging, and headers

  • Replace the round-trip ISO parse with end_time_dt = end_time_utc.replace(microsecond=0) (and likewise for start)
  • Catch httpx.HTTPStatusError immediately after TimeoutException, then httpx.RequestError to avoid swallowing HTTP status errors
  • Downgrade full response logging to logger.debug with length only to redact PII
  • Use a single tag [list_orders_request] in all log messages and error payloads (remove extra spaces)
  • Add a User-Agent header (e.g. "User-Agent": "ClairvoyanceApp/1.0")
  • Verify whether both filters.dateCreated (camelCase) and qFilters are needed—remove the unsupported one to avoid ambiguity
🧰 Tools
🪛 Ruff (0.13.1)

200-200: Do not catch blind exception: Exception

(BLE001)


287-287: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
In app/agents/voice/automatic/tools/juspay/analytics.py around lines 162-296,
fix the list orders helper by: 1) replace the round-trip ISO parsing with direct
microsecond-truncated datetimes (use start_time_utc =
start_time_utc.replace(microsecond=0) and same for end_time_utc) and compute
timestamps from those; 2) reorder exception handling to catch
httpx.TimeoutException first, then httpx.HTTPStatusError immediately after, and
then httpx.RequestError/Exception to avoid swallowing status errors; 3) change
full response logging to logger.debug and log only the response length (not full
body) to avoid PII; 4) normalize all log messages and returned error payload
keys to use the single tag "[list_orders_request]" and remove extra spaces in
messages; 5) add a User-Agent header like "User-Agent": "ClairvoyanceApp/1.0" to
headers; 6) remove the unsupported/ambiguous filters.dateCreated field from
full_payload (or confirm and keep only the supported one, likely qFilters) so
only one filter mechanism is sent.


def handle_genius_response(func):
"""
A decorator that takes a tool function, executes it, and handles the
Expand Down Expand Up @@ -427,6 +567,20 @@ def get_success_transactional_data_by_time(
return _make_genius_api_request(params, payload_details)


@handle_genius_response
def get_last_n_orders(params: FunctionCallParams):
logger.info(f"Fetching last placed order with params: {params.arguments}")

analytics_payload = {
"domain": "txnsELS",
"offset": 0,
"sortDimension": "order_created_at",
"limit": params.arguments.get("limit", 1),
}

return _make_list_orders_api_request(params, analytics_payload)

Comment on lines 541 to 556
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Fix sort field for txnsELS and validate limit.

  • sortDimension uses order_created_at but domain=txnsELS implies date_created; this likely breaks sorting.
  • Sanitize limit to int and clamp to a safe range.
  • Log message says “last placed order” (singular).

Apply this diff:

 @handle_genius_response
 def get_last_n_orders(params: FunctionCallParams):
-    logger.info(f"Fetching last placed order with params: {params.arguments}")
+    logger.info(f"Fetching last N orders with params: {params.arguments}")
 
-    analytics_payload = {
-        "domain": "txnsELS",
-        "offset": 0,
-        "sortDimension": "order_created_at",
-        "limit": params.arguments.get("limit", 1),
-    }
+    # Validate and clamp limit
+    limit_arg = params.arguments.get("limit", 1)
+    try:
+        limit = max(1, min(100, int(limit_arg)))
+    except (TypeError, ValueError):
+        limit = 1
+
+    analytics_payload = {
+        "domain": "txnsELS",
+        "offset": 0,
+        "sortDimension": "date_created",  # txnsELS uses date_created
+        "limit": limit,
+    }
 
     return _make_list_orders_api_request(params, analytics_payload)

If ec/v4/orders supports specifying sort order explicitly, add it (e.g., descending) per API contract.


🏁 Script executed:

#!/bin/bash
# Search for txnsELS usage in analytics.py
rg -nP "'domain':\s*\"txnsELS\"" -C3 app/agents/voice/automatic/tools/juspay/analytics.py

# Search for all sortDimension values in analytics.py
rg -nP "'sortDimension':" -C3 app/agents/voice/automatic/tools/juspay/analytics.py

# Check for date_created occurrences
rg -nP "date_created" -C3 app/agents/voice/automatic/tools/juspay/analytics.py

# Check for order_created_at occurrences
rg -nP "order_created_at" -C3 app/agents/voice/automatic/tools/juspay/analytics.py

Length of output: 612


Fix sorting field and sanitize limit in get_last_n_orders

  • Update log to “Fetching last N orders…”
  • Replace "sortDimension": "order_created_at" with "date_created" for txnsELS
  • Cast limit to int and clamp between 1–100 (default 1)
  • Optionally add explicit sort order (e.g. "sortOrder": "desc") if supported

app/agents/voice/automatic/tools/juspay/analytics.py lines 571–582

🤖 Prompt for AI Agents
In app/agents/voice/automatic/tools/juspay/analytics.py around lines 571 to 582,
update the function to log "Fetching last N orders…" instead of the current
message; change the analytics_payload sortDimension from "order_created_at" to
"date_created" for the txnsELS domain; read params.arguments.get("limit") and
cast it to int, defaulting to 1, then clamp it to the range 1–100 before setting
analytics_payload["limit"]; and add an explicit "sortOrder": "desc" entry to
analytics_payload if the API supports it to ensure newest orders are returned
first.


async def get_gmv_order_value_payment_method_wise_by_time(params: FunctionCallParams):
logger.info(f"Fetching real-time GMV with params: {params.arguments}")
payload_details = {
Expand Down Expand Up @@ -1666,6 +1820,19 @@ async def update_euler_offer(params: FunctionCallParams):
required=time_input_schema["required"],
)

get_last_n_orders_function = FunctionSchema(
name="get_last_n_orders",
description="Retrieves the most recently placed orders within a specified time range, sorted by creation time in descending order.",
properties={
**time_input_schema["properties"],
"limit": {
"type": "number",
"description": "Maximum number of recent orders to retrieve. Defaults to 1 to get the single most recent order. Increase this value to get multiple recent orders (e.g., 5 for last 5 orders, 10 for last 10 orders).",
},
},
required=time_input_schema["required"],
)

gmv_order_value_payment_method_wise_function = FunctionSchema(
name="get_gmv_order_value_payment_method_wise_by_time",
description="Get the total Gross Merchandise Value (GMV) for each payment method within a specified time range. The results can be summed to calculate the total payment method GMV/sales. Use this to understand the revenue contribution of each payment method and the overall sales performance. Default to today if no timeframe specified.",
Expand Down Expand Up @@ -1905,6 +2072,7 @@ async def update_euler_offer(params: FunctionCallParams):
list_offers_by_filter_function,
delete_euler_offer_function,
update_euler_offer_function,
get_last_n_orders_function,
]
)

Expand All @@ -1921,4 +2089,5 @@ async def update_euler_offer(params: FunctionCallParams):
"list_offers_by_filter": list_offers_by_filter,
"delete_euler_offer": delete_euler_offer,
"update_euler_offer": update_euler_offer,
"get_last_n_orders": get_last_n_orders,
}
1 change: 1 addition & 0 deletions app/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ def get_required_env(var_name: str) -> str:

# Juspay API configuration
GENIUS_API_URL = "https://portal.juspay.in/api/q/query?api-type=genius-query"
GENIUS_LIST_ORDERS_API = "https://portal.juspay.in/ec/v4/orders"
EULER_DASHBOARD_API_URL = os.environ.get(
"EULER_DASHBOARD_API_URL", "https://portal.juspay.in"
)
Expand Down