Unable to Call the Analytics Conversations Details Query with Dimension and Metric filters via Python SDK

We are working on a nightly script that gets a list of conversation Id's for the previous day, we are able to filter on dimensions, or metrics, however we cannot filter on both of them at the same time.

Here's a snippet of the code in question:

api_instance = PureCloudPlatformClientV2.AnalyticsApi(authToken)

query = PureCloudPlatformClientV2.ConversationQuery()

query.interval = "2023-07-03T04:00:00.000Z/2023-07-07T04:00:00.000Z"
query.paging = PureCloudPlatformClientV2.PagingSpec()

query.paging.page_number = '1'
query.paging.page_size = '2'

query.conversation_filters = [PureCloudPlatformClientV2.ConversationDetailQueryFilter()]
query.conversation_filters[0].type = "and"

query.conversation_filters[0].clauses = [PureCloudPlatformClientV2.ConversationDetailQueryClause()]
query.conversation_filters[0].clauses[0].type = "and"
query.conversation_filters[0].clauses[0].predicates = [PureCloudPlatformClientV2.ConversationDetailQueryPredicate()] * 5

query.conversation_filters[0].clauses[0].predicates[0].type = "dimension"
query.conversation_filters[0].clauses[0].predicates[0].dimension = "divisionId"
query.conversation_filters[0].clauses[0].predicates[0].operator = "matches"
query.conversation_filters[0].clauses[0].predicates[0].value = "5338ce2b-8a04-4b0e-bea2-cb8b24ad76aa"

query.conversation_filters[0].clauses[0].predicates[1].type = "dimension"
query.conversation_filters[0].clauses[0].predicates[1].dimension = "originatingDirection"
query.conversation_filters[0].clauses[0].predicates[1].operator = "matches"
query.conversation_filters[0].clauses[0].predicates[1].value = "inbound"
query.conversation_filters[0].clauses[0].predicates[2].type = "metric"
query.conversation_filters[0].clauses[0].predicates[2].metric = "tTalk"
query.conversation_filters[0].clauses[0].predicates[2].range = PureCloudPlatformClientV2.NumericRange()
query.conversation_filters[0].clauses[0].predicates[2].range.gt = "60000"

query.conversation_filters[0].clauses[0].predicates[3].type = "metric"
query.conversation_filters[0].clauses[0].predicates[3].metric = "nBlindTransferred"
query.conversation_filters[0].clauses[0].predicates[3].operator = "notExists"
query.conversation_filters[0].clauses[0].predicates[3].value = None
query.conversation_filters[0].clauses[0].predicates[3].range = None

query.conversation_filters[0].clauses[0].predicates[4].type = "metric"
query.conversation_filters[0].clauses[0].predicates[4].metric = "nConsultTransferred"
query.conversation_filters[0].clauses[0].predicates[4].operator = "notExists"
query.conversation_filters[0].clauses[0].predicates[4].value = None
query.conversation_filters[0].clauses[0].predicates[4].range = None

When running these filters we get the error:

Traceback (most recent call last):
File "c:\Users...\gcConversationSearch.py", line 41, in
query.conversation_filters[0].clauses[0].predicates[2].dimension = None
File "C:\Users...\AppData\Roaming\Python\Python310\site-packages\PureCloudPlatformClientV2\models\conversation_detail_query_predicate.py", line 115, in dimension
if dimension.lower() not in map(str.lower, allowed_values):
AttributeError: 'NoneType' object has no attribute 'lower'
PS C:\Users\maxwed2> & "C:/Program Files/Python310/python.exe" "c:/Users/maxwed2/OneDrive - Nationwide/Desktop/gcConversationSearch.py"
Traceback (most recent call last):
File "c:\Users...\Desktop\gcConversationSearch.py", line 98, in
api_response = api_instance.post_analytics_conversations_details_query(query)
File "C:\Users...\AppData\Roaming\Python\Python310\site-packages\PureCloudPlatformClientV2\apis\analytics_api.py", line 2542, in post_analytics_conversations_details_query
response = self.api_client.call_api(resource_path, 'POST',
File "C:\Users...\AppData\Roaming\Python\Python310\site-packages\PureCloudPlatformClientV2\api_client.py", line 530, in call_api
return self.__call_api(resource_path, method,
File "C:\Users...\AppData\Roaming\Python\Python310\site-packages\PureCloudPlatformClientV2\api_client.py", line 343, in __call_api
response_data = self.request(method, url, query_params=query_params,
File "C:\Users...\AppData\Roaming\Python\Python310\site-packages\PureCloudPlatformClientV2\api_client.py", line 565, in request
return self.rest_client.POST(url,
File "C:\Users...\AppData\Roaming\Python\Python310\site-packages\PureCloudPlatformClientV2\rest.py", line 225, in POST
return self.request("POST", url,
File "C:\Users...\AppData\Roaming\Python\Python310\site-packages\PureCloudPlatformClientV2\rest.py", line 197, in request
raise ApiException(http_resp=r)
PureCloudPlatformClientV2.rest.ApiException: (400)
Reason: Bad Request
HTTP response headers: HTTPHeaderDict({'Content-Type': 'application/json', 'Content-Length': '195', 'Connection': 'keep-alive', 'Date': 'Mon, 10 Jul 2023 18:25:30 GMT', 'ININ-Correlation-Id': '3de06140-aac7-4c06-82f6-3ced2a917ecd', 'Strict-Transport-Security':
'max-age=600; includeSubDomains', 'Cache-Control': 'no-cache, no-store, must-revalidate', 'X-Cache': 'Error from cloudfront', 'Via': '1.1 8c2dc914428d194718c8f636a78a5f74.cloudfront.net (CloudFront)', 'X-Amz-Cf-Pop': 'DFW57-P3', 'X-Amz-Cf-Id': 'NoXTrlsOH05nps4qL0Bs_8eaGjDs89BgGbf5dURLZoyx5oqupI_Ygg=='})
HTTP response body: {"message":"dimension must be null when using a metric predicate","code":"bad.request","status":400,"messageParams":{},"contextId":"3de06140-aac7-4c06-82f6-3ced2a917ecd","details":[],"errors":[]}

The part that stands out is:

HTTP response body: {"message":"dimension must be null when using a metric predicate","code":"bad.request","status":400,"messageParams":{},"contextId":"3de06140-aac7-4c06-82f6-3ced2a917ecd","details":[],"errors":[]}

Based on this error "dimension must be null when using a metric predicate" we tried updating our code to have dimension = None like below for all of our metric types but hit another error (example just shows 1 of 3 places we updated it):

query.conversation_filters[0].clauses[0].predicates[3].type = "metric"
query.conversation_filters[0].clauses[0].predicates[3].metric = "nBlindTransferred"
query.conversation_filters[0].clauses[0].predicates[3].operator = "notExists"
query.conversation_filters[0].clauses[0].predicates[3].value = None
query.conversation_filters[0].clauses[0].predicates[3].range = None
query.conversation_filters[0].clauses[0].predicates[3].dimension = None

It gives this error:

When adding Dimension = None we get NoneType has not object .lower()

We also originally weren't using "clauses" so if you remove any reference to "clauses" the exact same thing applies as I said above, we were just grasping at ideas to get this to work.

In the definition for dimensions you have this:

allowed_values = ["conversationEnd", "conversationId", "conversationInitiator", "conversationStart", >"customerParticipation", "divisionId", "externalTag", "mediaStatsMinConversationMos", "originatingDirection"]
if dimension.lower() not in map(str.lower, allowed_values):
# print("Invalid value for dimension -> " + dimension)
self._dimension = "outdated_sdk_version"

So it's not handling None/Null's and it seems like this is where the issue is coming from, however admittedly I've found the documentation on consuming this via python pretty lacking so I'm expecting we're doing something silly somewhere and hoping someone can help identify what's going on here. Like I said, if we only do Dimension filters, or only do Metric filters it works perfectly fine, only when we try to do both do things go haywire.

I don't see a question in your post and it looks like you got a query working. But here are some resources relevant to analytics queries:

The question is why am I getting the error, I tried doing as you described and here's our API Explorer Body we've been writing this based on

{
"conversationFilters": [
{
"type": "and",
"predicates": [
{
"type": "dimension",
"dimension": "divisionId",
"value": "5338ce2b-8a04-4b0e-bea2-cb8b24ad76aa"
},
{
"type": "metric",
"metric": "tTalk",
"range": {
"gt": 60000
}
},
{
"type": "dimension",
"dimension": "originatingDirection",
"operator": "matches",
"value": "inbound"
},
{
"type": "metric",
"metric": "nBlindTransferred",
"operator": "notExists"
},
{
"type": "metric",
"metric": "nConsultTransferred",
"operator": "notExists"
}
]
}
],
"interval": "2023-07-03T04:00:00.000Z/2023-07-07T04:00:00.000Z",
"paging": {
"pageSize": 100,
"pageNumber": 1
}
}

And here's the actual query it's creating based on our code (without the clauses section since that matches our existing working query):

{'aggregations': None,
'conversation_filters': [{'clauses': None,
'predicates': [{'dimension': 'originatingDirection',
'metric': 'nConsultTransferred',
'operator': 'notExists',
'range': None,
'type': 'metric',
'value': None},
{'dimension': 'originatingDirection',
'metric': 'nConsultTransferred',
'operator': 'notExists',
'range': None,
'type': 'metric',
'value': None},
{'dimension': 'originatingDirection',
'metric': 'nConsultTransferred',
'operator': 'notExists',
'range': None,
'type': 'metric',
'value': None},
{'dimension': 'originatingDirection',
'metric': 'nConsultTransferred',
'operator': 'notExists',
'range': None,
'type': 'metric',
'value': None},
{'dimension': 'originatingDirection',
'metric': 'nConsultTransferred',
'operator': 'notExists',
'range': None,
'type': 'metric',
'value': None}],
'type': 'and'}],
'evaluation_filters': None,
'interval': '2023-07-03T04:00:00.000Z/2023-07-07T04:00:00.000Z',
'order': None,
'order_by': None,
'paging': {'page_number': '1', 'page_size': '2'},
'resolution_filters': None,
'segment_filters': None,
'survey_filters': None}

Now to figure out why it's overwriting all of the existing data each time we set an additional predicate rather than appending each one to the list.

query.conversation_filters[0].predicates[0].type = "dimension"
query.conversation_filters[0].predicates[0].dimension = "divisionId"
query.conversation_filters[0].predicates[0].operator = "matches"
query.conversation_filters[0].predicates[0].value = "5338ce2b-8a04-4b0e-bea2-cb8b24ad76aa"

query.conversation_filters[0].predicates[1].type = "dimension"
query.conversation_filters[0].predicates[1].dimension = "originatingDirection"
query.conversation_filters[0].predicates[1].operator = "matches"
query.conversation_filters[0].predicates[1].value = "inbound"

query.conversation_filters[0].predicates[2].type = "metric"
query.conversation_filters[0].predicates[2].metric = "tTalk"
query.conversation_filters[0].predicates[2].range = PureCloudPlatformClientV2.NumericRange()
query.conversation_filters[0].predicates[2].range.gt = "60000"

query.conversation_filters[0].predicates[3].type = "metric"
query.conversation_filters[0].predicates[3].metric = "nBlindTransferred"
query.conversation_filters[0].predicates[3].operator = "notExists"
query.conversation_filters[0].predicates[3].value = None
query.conversation_filters[0].predicates[3].range = None

query.conversation_filters[0].predicates[4].type = "metric"
query.conversation_filters[0].predicates[4].metric = "nConsultTransferred"
query.conversation_filters[0].predicates[4].operator = "notExists"
query.conversation_filters[0].predicates[4].value = None
query.conversation_filters[0].predicates[4].range = None

Admittedly this is my first time working with something like this, and I'm much more familiar w/ Node JS, however this solution is required to be in python so I'm probably doing something stupid as originally mentioned. We're going to update our code to try using append instead of what we're doing above and see if that improves things.

@Davis_Walker can you do this and share the result? The second block you shared above is a dump of a python object, not serialization to JSON using the SDK's logic.

The problem you're experiencing seems like every array element has the same object reference. I'm not a python developer either, but multiplying an array by 5 and expecting that to result in instantiating 5 separate objects seems suspect. But python's syntax is generally pretty suspect, so maybe that's ok. What happens if you instantiate each one manually like query.conversation_filters[0].clauses[0].predicates = [PureCloudPlatformClientV2.ConversationDetailQueryPredicate(), PureCloudPlatformClientV2.ConversationDetailQueryPredicate(), PureCloudPlatformClientV2.ConversationDetailQueryPredicate(), PureCloudPlatformClientV2.ConversationDetailQueryPredicate(), PureCloudPlatformClientV2.ConversationDetailQueryPredicate()] (I assume that's valid syntax)? Or append to the array in each predicate block like query.conversation_filters[0].clauses[0].predicates.append(PureCloudPlatformClientV2.ConversationDetailQueryPredicate()).

Yup, after updating the code to use append and redefining the value each time things are now working as expected, here's the updated part of the code in case someone comes across this while googling similar issues

conversation_filter = PureCloudPlatformClientV2.ConversationDetailQueryFilter()
conversation_filter.type = "and"
conversation_filter.predicates = []

predicate = PureCloudPlatformClientV2.ConversationDetailQueryPredicate()
predicate.type = "dimension"
predicate.dimension = "divisionId"
predicate.operator = "matches"
predicate.value = "5338ce2b-8a04-4b0e-bea2-cb8b24ad76aa"

conversation_filter.predicates.append(predicate)

predicate = PureCloudPlatformClientV2.ConversationDetailQueryPredicate()
predicate.type = "dimension"
predicate.dimension = "originatingDirection"
predicate.operator = "matches"
predicate.value = "inbound"

conversation_filter.predicates.append(predicate)

predicate = PureCloudPlatformClientV2.ConversationDetailQueryPredicate()
predicate.type = "metric"
predicate.metric = "tTalk"
range_ = PureCloudPlatformClientV2.NumericRange()
range_.gt = 60000
predicate.range = range_

conversation_filter.predicates.append(predicate)

predicate = PureCloudPlatformClientV2.ConversationDetailQueryPredicate()
predicate.type = "metric"
predicate.metric = "nBlindTransferred"
predicate.operator = "notExists"

conversation_filter.predicates.append(predicate)

predicate = PureCloudPlatformClientV2.ConversationDetailQueryPredicate()
predicate.type = "metric"
predicate.metric = "nConsultTransferred"
predicate.operator = "notExists"

conversation_filter.predicates.append(predicate)

query.conversation_filters.append(conversation_filter)

Edit: I like your idea of putting them each in the initial array, instead of what I posted above, for now this is working so I'm not going to spend much more time on it right now, but may have to give that a try later.