Tuesday, April 8, 2025

Termination_Call_Detail: identifier of last row of an ACD call

The task:

From Termination_Call_Detail (TCD) we need to select the unique identifier of the final row for each ACD call, for the last 6 months.

The "6 months" clause: uses DateTime (indexed). 

The "ACD call" clause: we use RouterCallKeyDay > 0 AND RouterCallKey > 0 (both are indexed). 

The "unique identifier" part: let's not complicate things and just use RecoveryKey.  

The challenge:

The "final row" - we know there are multiple rows per each ACD call in TCD. 

Let's not complicate things and just pretend rows are ordered by RecoveryKey. 


Analysis:

We've got multiple options: 

  • GROUP BY 
  • RANK/DENSE_RANK/ROW_NUMBER 
  • LAST_VALUE 

 

The good old GROUP BY approach:

SELECT tcd.RouterCallKeyDay, tcd.RouterCallKey, MAX(tcd.RecoveryKey) AS maxTcdRecoveryKey 
FROM Termination_Call_Detail tcd 
WHERE tcd.DateTime > DATEADD(m,-6,CAST(GETDATE() AS DATE)) 
AND tcd.RouterCallKeyDay > 0 AND tcd.RouterCallKey > 0
GROUP BY tcd.RouterCallKeyDay, tcd.RouterCallKey

STATISTICS TIME:  CPU time = 4297 ms,  elapsed time = 17097 ms.

The execution plan is simple. Index seek 85%, Aggregate cost 15%:

 


The fancy ranking approach - using ROW_NUMBER:

;WITH xTcd AS (
SELECT tcd.RouterCallKeyDay, tcd.RouterCallKey, tcd.RecoveryKey, ROW_NUMBER() OVER (PARTITION BY tcd.RouterCallKeyDay, tcd.RouterCallKey ORDER BY tcd.RecoveryKey DESC) AS rowNumber 
FROM Termination_Call_Detail tcd 
WHERE tcd.DateTime > DATEADD(m,-6,CAST(GETDATE() AS DATE)) 
AND tcd.RouterCallKeyDay > 0 AND tcd.RouterCallKey > 0 )
SELECT xTcd.RouterCallKeyDay, xTcd.RouterCallKey, xTcd.RecoveryKey FROM xTcd xTcd WHERE xTcd.rowNumber = 1

Notice we're using a Common Table Expression (CTE) to "store" data temporarily so we can filter out the rows where ROW_NUMBER is 1. 

STATISTICS TIME: CPU time = 6986 ms,  elapsed time = 10153 ms.

Execution plan: yeah, the sort cost is quite high, 45%: 

Finally, the funky analytic approach - using DISTINCT LAST_VALUE:

SELECT DISTINCT tcd.RouterCallKeyDay, tcd.RouterCallKey, LAST_VALUE(tcd.RecoveryKey) OVER (PARTITION BY tcd.RouterCallKeyDay, tcd.RouterCallKey ORDER BY tcd.RecoveryKey DESC ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS RecoveryKey 
FROM Termination_Call_Detail tcd 
WHERE tcd.DateTime > DATEADD(m,-6,CAST(GETDATE() AS DATE)) 
AND tcd.RouterCallKeyDay > 0 AND tcd.RouterCallKey > 0

STATISTICS TIME: CPU time = 17375 ms,  elapsed time = 13584 ms.

The execution plan is even more complicated, Index seek: 40%, sort cost: 37%:

 

 

It's not worth using the analytic approach, because... why?!

Now, comparing GROUP BY and ROW_NUMBER: the first approach uses less CPU time (4297ms) but the total elapsed time is 17097ms which is higher than the total elapsed time of the second approach (10153ms) where the CPU time is higher: 6986ms. 


The winner is ROW_NUMBER. 🎉

 

Wednesday, April 2, 2025

Genesys: Creating TTS prompts via API using HTTP only, no libs

The Uploading Architect Prompts with API document shows how to create prompts using either the JavaScript or the Python library. If you're lazy like me and/or want to avoid using a library, here's how create - TTS - prompts using simple HTTP: 

0. Get the auth token. 

1. Create a blank Prompt object: POST an object as JSON with the following fields: 

- String name,

- (Optional) String description

at https://api.mypurecloud.com/api/v2/architect/prompts

Save the selfUri value from the object returned by the server. 

2. Add a resource to the Prompt created at step 1: POST an object: 

- String language - from the list of supported languages, eg. "en-us",

- String text - the text of the prompt,

- String ttsString - use the same value as for text

at https://api.mypurecloud.com${uri}/resources where uri is the value of selfUri from step 1. 

 

Example methods - Groovy + Java 11:

Step 1: 

final def createPrompt(String accessToken, String body) {
 HttpRequest httpRequest = HttpRequest.newBuilder()
  .uri(new URI("https://api.mypurecloud.com/api/v2/architect/prompts"))
  .headers("Content-Type",  "application/json")
  .headers("Authorization", "Bearer $accessToken")
  .timeout(Duration.of(60, ChronoUnit.SECONDS))
  .POST(HttpRequest.BodyPublishers.ofString(body))
  .build()
  HttpResponse<String> httpResponse = HttpClient.newHttpClient().send(httpRequest, HttpResponse.BodyHandlers.ofString()) 
  def statusCode = httpResponse.statusCode()
  println("statusCode:\t$statusCode")
  println("httpResponse:\t${httpResponse.body()}")
 def jsonSlurper = new JsonSlurper()
 def object = jsonSlurper.parseText(httpResponse.body())
 println object
 return object
}

Where accessToken is the access token, and the body is

def o = [name: promptName, description: promptDescription]
def body = groovy.json.JsonOutput.toJson(o)

Step 2: 


final def addTTStoPrompt(String accessToken, String uri, body) {
 HttpRequest httpRequest = HttpRequest.newBuilder()
  .uri(new URI("https://api.mypurecloud.com${uri}/resources"))
  .headers("Content-Type",  "application/json")
  .headers("Authorization", "Bearer $accessToken")
  .timeout(Duration.of(60, ChronoUnit.SECONDS))
  .POST(HttpRequest.BodyPublishers.ofString(body))
  .build()
  HttpResponse<String> httpResponse = HttpClient.newHttpClient().send(httpRequest, HttpResponse.BodyHandlers.ofString()) 
  def statusCode = httpResponse.statusCode()
  println("statusCode:\t$statusCode")
  println("httpResponse:\t${httpResponse.body()}")
 def jsonSlurper = new JsonSlurper()
 def object = jsonSlurper.parseText(httpResponse.body())
 println object
 return object
}

uri is the selfUri value from step 1 and the body:

def o = [language: "en-us", text: ttsString, ttsString: ttsString ]
def body = groovy.json.JsonOutput.toJson(o)



Tuesday, February 28, 2023

UCCX scripts can haz String templates

" (double quotes) are _usually_ a problem within a String that must be enclosed by "'s.

Now, pretend you need to construct a JSON string in a UCCX/IP IVR AEF script - which needs you to use double quotes around Strings - but it also has to be dynamic: there must be a way to add values that may come from an external source.

String concatenation is sooo 80's and ugly anti pattern and not elegant. 

Let's implement String interpolation instead - with templates. It's simple, it's reusable and easy to test.

Of course, the UCCX scripting toolbox has got everything we need:

  • the u prefix* which allows escaping double qoutes using \" (without this prefix it just won't accept \") and
  • the String.format( String template, Object[] arguments ) static method.

First, the template:

String template = u"{\"fruit\":\"%1s\"}"

This just translates to a simple JSON object, a Map or a list of key-value pairs if you insist, with a one key: fruit.
%1s
is placeholder, meaning "take the first argument and format it as a String". In this example, template is just a UCCX String variable: 


Pretend, at some point we collect the value we need in order to construct our JSON string. Perhaps from a database. This value is stored in a String variable externalValue. In this example, externalValue = "banana".

Let's put template and externalValue together in order to create the value of completeJsonString.

Set completeJsonString = String.format(template, new Object[] {externalValue} )

What just happened? 

String.format is an elegant, yet underrated Java method. UCCX scripting is Java scripting, but instead of varargs it uses an array of objects.

The first argument is the template. The template value has a placeholder. Placeholders start with the percent sign, followed by the position (1, 2, n) and a format - in our case, s is just an instruction to pass it along (~ format the incoming String as a String).

Format what: the second argument's values. This needs to be an array of Objects (Object[]) which we construct and initialize on the fly, this is why new Object[] { externalValue }.

Let's test it (easiest: by using the debug methods available in CCX Editor):

And hey presto, we receive a nice JSON string:  U"{\"fruit\":\"banana\"}" which is of course {"fruit":"banana"}.


* Thanks @dmacias for the tip with the u prefix.

Wednesday, April 6, 2022

Groovy one-liner to check passwords stored in ICM Person table

Passwords are stored as MD5 hashes in the UCCE/PCCE/ICM Person table*. If you ever need to check for weak passwords, use the following one-liner:
assert passwordEncryptedInDb.replace("{enc:1}","").decodeBase64()
==
passwordPlain.digest('md5').decodeHex()
And of course, we get the passwordEncryptedInDb value using
SELECT per.Password FROM Person per 
JOIN Agent ag ON ag.PersonID = per.PersonID 
WHERE ag.PeripheralNumber = @loginId
* non-SSO

Saturday, February 12, 2022

WAV file / Youtube video → GStreamer → RTP/UDP stream → CiscoTerminal

GStreamer makes streaming "from" a G.711 audio file easy.

6001.wav is already encoded as G.711uLaw:


$ gst-launch-1.0 -v filesrc location = /home/greg/jars/audio/6001.wav ! wavparse ! rtppcmupay ! udpsink host=127.0.0.1 port=19001

 

And with ffmpeg and yt-dlp:


gst-launch-1.0 -v souphttpsrc location=$(yt-dlp -x -g https://www.youtube.com/watch?v=ljnxZypkjn0) ! decodebin ! audioconvert ! audioresample ! mulawenc ! rtppcmupay ! udpsink host=127.0.0.1 port=19001



Wednesday, June 16, 2021

Grails: inject bean from a config file (via grailsApplication) in resources.groovy directly without BootStrap.groovy

I used to do this: resources.groovy:

beans = {
        envSettings(ConcurrentHashMap) {bean ->
        /* k: String, v: String */
    }
}
And then in BootStrap.groovy:

String label = grailsApplication.config.getProperty('envSettings.label', String, 'N/A')
Boolean nonProd = grailsApplication.config.getProperty('envSettings.nonProd', Boolean, true)
envSettings["label"] = label
envSettings["nonProd"] = nonProd
It looks like we can do that without BootStrap.groovy. It's sooo simple. resources.groovy:

beans = {
	prefixDflt String, grailsApplication.config.getProperty("prefix.dflt", String, "")
}

Saturday, January 9, 2021

Transact-SQL percentage

DECLARE @t TABLE (id INT PRIMARY KEY, name VARCHAR(10), amount INT)
INSERT INTO @t VALUES (1,'apple',10)
INSERT INTO @t VALUES (2,'pear',20)
INSERT INTO @t VALUES (3,'plum',50)
SELECT 
t.id,
t.name,
t.amount,
CAST(t.amount AS FLOAT)/SUM(amount) OVER () AS [percent]
FROM @t t

Tuesday, May 22, 2018

VMWare PowerCLI backup of ESXi 5.5 hosts.

Set-ExecutionPolicy RemoteSigned
Add-PSSnapin VMware.VimAutomation.Core
$server="10.123.45.67"
$user="user"
$pass="1superSecretP@ssword!!!"
Connect-VIServer -Server $server -User $user -Password $pass
$now=Get-Date -format "yyyy-MM-dd_HH-mm-ss"
$newdir="b:\vmware-backups\$now"
new-item $newdir -itemtype directory
get-vmhost | get-vmhostfirmware -BackupConfiguration -DestinationPath $newdir
Disconnect-VIServer -Server $server -Confirm:$false

Tuesday, April 24, 2018

Apache POI 3.17 with Groovy/Grapes kept reporting javax.xml.stream.FactoryConfigurationError: Provider com.bea.xml.stream.EventFactory not found

Like this:
Caught: javax.xml.stream.FactoryConfigurationError: Provider com.bea.xml.stream.EventFactory not found
javax.xml.stream.FactoryConfigurationError: Provider com.bea.xml.stream.EventFactory not found
 at org.apache.poi.openxml4j.opc.internal.marshallers.PackagePropertiesMarshaller.(PackagePropertiesMarshaller.java:41)
 at org.apache.poi.openxml4j.opc.OPCPackage.init(OPCPackage.java:161)
 at org.apache.poi.openxml4j.opc.OPCPackage.(OPCPackage.java:141)
 at org.apache.poi.openxml4j.opc.ZipPackage.(ZipPackage.java:97)
 at org.apache.poi.openxml4j.opc.OPCPackage.open(OPCPackage.java:324)
 at org.apache.poi.ss.usermodel.WorkbookFactory.create(WorkbookFactory.java:184)
 at org.apache.poi.ss.usermodel.WorkbookFactory.create(WorkbookFactory.java:149)
 at org.apache.poi.ss.usermodel.WorkbookFactory$create.call(Unknown Source)
 at poitest.run(poitest.groovy:14)
 at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
 at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
Solution here. This works:
@GrabConfig(systemClassLoader=true)
@Grapes([
   @Grab(group='org.apache.poi', module='poi', version='3.17'),
   @Grab(group='org.apache.poi', module='poi-ooxml', version='3.17'),
   @Grab(group='org.apache.poi', module='ooxml-schemas', version='1.3'),
   @Grab(group='com.fasterxml', module='aalto-xml', version='1.1.0') 
])

import org.apache.poi.ss.usermodel.*
import org.apache.poi.ss.usermodel.WorkbookFactory

System.setProperty("javax.xml.stream.XMLInputFactory", "com.fasterxml.aalto.stax.InputFactoryImpl")
System.setProperty("javax.xml.stream.XMLOutputFactory", "com.fasterxml.aalto.stax.OutputFactoryImpl")
System.setProperty("javax.xml.stream.XMLEventFactory", "com.fasterxml.aalto.stax.EventFactoryImpl")

Workbook wb = WorkbookFactory.create(new FileInputStream("./blah.xlsx"))
println wb.dump()