

翻訳は機械翻訳により提供されています。提供された翻訳内容と英語版の間で齟齬、不一致または矛盾がある場合、英語版が優先します。

# Amazon Connect のコンタクトレコードを使用して会議と転送を特定する
<a name="identify-conferences-transfers"></a>

コンタクトレコードは、コンタクトセンターにあるコンタクトに関連付けられているイベントをキャプチャします。新しいコンタクトごとに、Amazon Connect はコンタクトレコードを作成し、一意のコンタクト ID をコンタクトに割り当てます。

エージェントが別のエージェント (Amazon Connect の内部または外部、通話料無料または直通ダイヤル番号を使用) と相談するたびに、Amazon Connect はコンサルトレッグコンタクトレコードを作成し、このレッグの新しいコンタクト ID を発行します。

メインコンタクトレコードとそれ以降のコンサルトレッグコンタクトレコードは、初回のコンタクト ID、次のコンタクト ID、前のコンタクト ID など、複数のコンタクト ID フィールドでリンクできます。

このトピックでは、これらのフィールドを使用して、コンタクトレコードで会議と転送を区別する方法について説明します。また、コンサルト通話、会議、転送のコンサルティング操作のタイプを確立するロジックも提供します。

**Topics**
+ [用語](#consultative-terms)
+ [コンサルティング通話のコンタクトレコード](#ctr-structure-consultative-calls)
+ [コンサルティング通話を特定する方法](#logic-consultative-calls)
+ [コードスニペット](#codesnippets-consultative-calls)

## 用語
<a name="consultative-terms"></a>

このトピックでは、次の用語を使用します。

**コンサルティング通話**  
次の 3 人の参加者が関与する通話です。  

1. イニシエータ (顧客など)

1. 受信者 (エージェントなど)

1. 相談を受ける参加者 (スーパーバイザー、外部のサードパーティー翻訳者など)
コンサルティング通話は、コンサルト通話、転送通話、または会議通話になる可能性があります。

**コンサルト通話**  
イニシエータが保留されているときに、受信者エージェントが別の参加者 (同じ Amazon Connect インスタンスまたは外部エンティティのエージェントなど) と相談する通話です。  
通話が切断されると、Amazon Connect はエージェントをアフターコールワーク (ACW) 状態にします。コンタクトレコードは、この状態になったときのタイムスタンプで更新されます。コンサルト通話の場合、相談を受ける参加者は顧客より早く切断されます。  
コンタクトレコードには、エージェントが `AfterContactWorkStartTimestamp` で ACW 状態になったときのタイムスタンプが記録されます。

**転送通話**  
受信者は、相談を受ける参加者にイニシエータを転送します。この場合、受信者エージェントは相談されたエージェントよりも前に ACW 状態になります。

**会議通話**  
受信者は、相談を受ける参加者にイニシエータを会議に招待します (三者間通話）。  
Amazon Connect では、3 人より多くの参加者が会議に参加できます。内部通話の場合、相談を受ける参加者は、コンサルト状況と会議状況の両方で受信者よりも先に ACW 状態になります。ただし、会議状況では相談を受ける参加者が顧客と話せるようになり、コンサルト状況では顧客が受信者によって保留される点が異なります。

以下のセクションでは、コンタクトレコードでこれらの各タイプの通話を識別する方法について説明します。

## コンサルティング通話のコンタクトレコード
<a name="ctr-structure-consultative-calls"></a>

顧客が Agent1 を呼び出したとします。エージェントは転送も、他のユーザーと相談もしません。通話が切断されると、コンタクトレコードは次の例のようになります (関連するフィールドのみが表示されます)。

```
{
    "AWSAccountId": "account-id",
    "Agent": {
        "ARN": "agent-arn",
        "AfterContactWorkStartTimestamp": "2024-08-02T17:50:53Z",
        .
        .
        "Username": "Agent1"
    },
    "ContactId": "497f04ca-6de1-408f-9b8a-ec57bcc99b31",
    .
    .
    "InitialContactId": null,
    "NextContactId": null,
    "PreviousContactId": null,
    .
    .
}
```

Agent1 が別のエージェント (Agent2) とのコンサルティング通話を開始した場合、コンサルト、転送、会議のいずれかになります。

 次のコンタクトレコードのサンプルは、これが開始エージェント (Agent1) と受信者エージェント (Agent2) でどのようになるかを示しています。
+ 開始エージェント (Agent1)

  ```
  {
      "Agent": {
          "ARN": "agent-arn"
          "AfterContactWorkStartTimestamp": "2024-08-02T17:50:53Z",
          .
          .
          "Username": "Agent1"
      },
      "ContactId": "497f04ca-6de1-408f-9b8a-ec57bcc99b31",
      "InitialContactId": null,
      "NextContactId": "6aa058d3-e771-4544-8e93-f5ce9c9003b3",
      .
      .
  }
  ```
+ 受信者エージェント (Agent2)

  ```
  {
      "Agent": {
          "ARN": "agent-arn",
          "AfterContactWorkStartTimestamp": "2024-08-02T17:51:07Z",
          .
          .
          "Username": "Agent2"
      },
      "ContactId": "6aa058d3-e771-4544-8e93-f5ce9c9003b3",
      "InitialContactId": "497f04ca-6de1-408f-9b8a-ec57bcc99b31",
      "NextContactId": null,
      "PreviousContactId": "497f04ca-6de1-408f-9b8a-ec57bcc99b31",
      .
      .
  }
  ```

  コンタクトレコードの 2 つの部分間の関係を次の図に示します。  
![\[コンサルティング通話中の Agent1 と Agent2 の関係。\]](http://docs.aws.amazon.com/ja_jp/connect/latest/adminguide/images/consultative-call.png)

  Agent1 (A1) と Agent2 (A2) は以下によってリンクされます。
  + N = 次のコンタクト ID。このフィールドは、最初のレッグのコンタクトレコードに表示されます。これは、このエージェントが相談した最後のエージェントのコンタクト ID です (この場合、最後のエージェントは A2)。
  + P = 前のコンタクト ID。このフィールドは、コンサルトレッグのコンタクトレコードに表示されます。これは、このレッグを呼び出したレッグのコンタクト ID です。この場合、A1 です。

  以下は図には示されていません。
  + 初回のコンタクト ID: Agent1 (A1) と顧客 (C) の間の最初のやり取りのコンタクト ID です。
  + コンタクト ID: これは、特定のやり取りの一意の識別子です。

  コンタクト ID、初回のコンタクト ID、前のコンタクト ID はシステム属性です。それぞれの詳細については、「[システム属性](connect-attrib-list.md#attribs-system-table)」を参照してください。

このモデルは、複数のエージェントが関与するコンサルト通話に拡張できます。以下は、拡張可能な方法のユースケースの例です。
+ **ユースケース 1**: Agent1 が Agent2 を招待し、Agent2 が Agent3 を招待し、Agent3 が Agent4 を招待します。前のコンタクト ID は常に前のエージェントです。次の図表は、このユースケースを示しています。  
![\[A1 が A2 を招待し、A2 が A3 を招待し、A3 が A4 を招待します。前のコンタクト ID は常に以前のエージェントです。\]](http://docs.aws.amazon.com/ja_jp/connect/latest/adminguide/images/consultative-call-example1.png)
+ **ユースケース 2**: Agent1 が Agent2 を招待し、Agent1 が Agent3 を招待し、Agent1 が Agent4 を招待します。前のコンタクト ID は常に Agent1 です。次の図表は、このユースケースを示しています。  
![\[A1 が Agent2 を招待し、A1 が A3 を招待し、A1 が A4 を招待します。前のコンタクト ID は常に A1 です。\]](http://docs.aws.amazon.com/ja_jp/connect/latest/adminguide/images/consultative-call-example2.png)
+ **ユースケース 3**: Agent1 が Agent2 を招待し、Agent2 が Agent4 と Agent5 を招待し、Agent1 が Agent3 を招待します。Agent2 および Agent3 の前のコンタクト ID は Agent1 です。Agent4 および Agent5 の前のコンタクト ID は Agent2 です。次の図表は、このユースケースを示しています。  
![\[A1 が A2 を招待し、A2 が A4 と A5 を招待し、A1 が A3 を招待します。\]](http://docs.aws.amazon.com/ja_jp/connect/latest/adminguide/images/consultative-call-example3.png)

## コンサルティング通話を特定する方法
<a name="logic-consultative-calls"></a>

1. [ステップ 1: メインコンタクトに関連付けられているすべてのレッグをグループ化する](#step1-consultative-calls)

1. [ステップ 2: コンタクト ID フィールドを使用して各ペア間の関係を特定する](#step2-consultative-calls) (前のコンタクト ID、次のコンタクト ID、初回のコンタクト ID、コンタクト ID)。コンタクトレコードの追加フィールドを調べて、コンサルト、転送、会議のコンサルティング操作のタイプを特定します。

### ステップ 1: メインコンタクトに関連付けられているすべてのレッグをグループ化する
<a name="step1-consultative-calls"></a>

このステップは、特定のイニシエータ/発信者によって開始されたすべての通話をグループ化するのに役立ちます。対象となるフィールドは、コンタクト ID、前のコンタクト ID、次のコンタクト ID、初回のコンタクト ID、およびコンタクト ID です。これは、通話の解決にかかったレッグの数を理解するのにも役立ちます。このワークフローは次のとおりです。

1. イニシエータを確立する: これは、`InitialContactId` フィールドが `NULL` であるコンタクトレコードです。また、このレコードの `PreviousContactId` も `NULL` です。

1. `InitialContactId` フィールドがイニシエータコンタクトレコードの `ContactId` と等しいすべてのコンタクトレコードは、このコンタクトレコードに関連しています。

### ステップ 2: コンタクト ID フィールドを使用して各ペア間の関係を特定する
<a name="step2-consultative-calls"></a>

次のロジックを使用して、コンサルト、転送、会議を特定できます。ロジックは、コンタクトレコードに記録されたタイムスタンプフィールドを使用します。関連するすべてのフィールドが `code` としてマークされています。

#### コンサルト通話
<a name="consult-c"></a>

イニシエータは、DID または通話料無料番号を使用して、同じ Amazon Connect インスタンス内 (内部) またはそのインスタンスの外部 (外部) で別の当事者と相談します。
+ 内部コンサルトの特性:
  + 相談を受けるエージェントはイニシエータエージェントの前に ACW 状態になります。
  + 相談を受けるエージェントが顧客と話すことはありません。これは、顧客がイニシエータによって保留されているためです。したがって、相談を受けるエージェントの `AgentInteractionDuration` フィールドはゼロです。
+ 外部コンサルトの特性:
  + イニシエータの顧客保留期間は、外部当事者のやり取りの時間 (`ExternalThirdPartyInteractionDuration`) より長くなります。

#### 会議通話
<a name="conference-c"></a>

イニシエータは、DID または通話料無料番号を使用して、同じ Amazon Connect インスタンス内 (内部) またはそのインスタンスの外部 (外部) で別の参加者と会議します。
+ 内部コンサルトの特性:
  + 相談を受けるエージェントはイニシエータエージェントの前に ACW 状態になります。
  + 相談を受けるエージェントは顧客と話します。`AgentInteractionDuration` はゼロではありません。
+ 外部コンサルトの特性:
  + イニシエータの顧客保留期間は、外部当事者のやり取りの時間 (`ExternalThirdPartyInteractionDuration`) よりも短くなります。つまり、顧客は一時的に保留状態になり、すべての参加者が通話に参加しました。

#### 転送通話
<a name="transfer-c"></a>

イニシエータは、DID または通話料無料番号を使用して、同じ Amazon Connect インスタンス内 (内部) またはそのインスタンスの外部 (外部) で別の当事者と相談します。
+ 内部コンサルトの特性: 
  + 相談を受けるエージェントは、イニシエータエージェントの後に ACW 状態になります。
  + イニシエータエージェントのフィールド `TransferCompletedTimestamp` は ZERO ではありません。
+ 外部コンサルトの特性:
  + 外部レッグが切断される (`DisconnectTimestamp`) 前にイニシエータが ACW 状態 (`AfterContactWorkStartTimestamp`) になります。
  + イニシエータエージェントのフィールド `TransferCompletedTimestamp` は ZERO ではありません。

## コードスニペット
<a name="codesnippets-consultative-calls"></a>

 次のコードスニペット (SQL、Java Script、Python) の例は、前のセクションで説明したロジックを活用して、会議、転送、コンサルティング通話を識別する方法を示しています。これらのスニペットは例として提供されており、本番稼働用ではありません。

### SQL コード
<a name="sql-consultative-calls"></a>

```
-- Conference transfer query DO NOT EDIT --
SELECT current_cr.contact_id,
    current_cr.initial_contact_id,
    current_cr.previous_contact_id,
    current_cr.next_contact_id,
    previous_cr.agent_username as initiator_agent_username,
    COALESCE (
        current_cr.agent_username,
        current_cr.customer_endpoint_address
    ) as recipient_agent_username,
    current_cr.agent_connected_to_agent_timestamp,
    current_cr.agent_after_contact_work_start_timestamp,
    current_cr.transfer_completed_timestamp,
    CASE
        WHEN previous_cr.agent_after_contact_work_start_timestamp < current_cr.agent_after_contact_work_start_timestamp
            AND previous_cr.transfer_completed_timestamp IS NOT NULL THEN 'TRANSFER'
        WHEN previous_cr.agent_after_contact_work_start_timestamp > current_cr.agent_after_contact_work_start_timestamp
            AND current_cr.agent_interaction_duration_ms <= 2000 THEN 'CONSULT'
        WHEN previous_cr.agent_after_contact_work_start_timestamp > current_cr.agent_after_contact_work_start_timestamp
            AND current_cr.agent_interaction_duration_ms > 2000 THEN 'CONFERENCE'
        WHEN current_cr.agent_username is NULL
            AND current_cr.initiation_method = 'EXTERNAL_OUTBOUND'
            AND previous_cr.agent_after_contact_work_start_timestamp > current_cr.disconnect_timestamp 
            AND previous_cr.agent_customer_hold_duration_ms > current_cr.external_third_party_interaction_duration_ms THEN 'EXTERNAL_CONSULT'
        WHEN current_cr.agent_username is NULL
            AND current_cr.initiation_method = 'EXTERNAL_OUTBOUND'
            AND previous_cr.agent_after_contact_work_start_timestamp > current_cr.disconnect_timestamp 
            AND previous_cr.agent_customer_hold_duration_ms < current_cr.external_third_party_interaction_duration_ms THEN 'EXTERNAL_CONFERENCE'
        WHEN current_cr.agent_username is NULL
            AND current_cr.initiation_method = 'EXTERNAL_OUTBOUND'
            AND current_cr.disconnect_timestamp > previous_cr.transfer_completed_timestamp THEN 'EXTERNAL_TRANSFER' ELSE 'START'
    END AS TYPE
FROM contact_record_link current_cr
    LEFT JOIN contact_record_link previous_cr ON previous_cr.contact_id = current_cr.previous_contact_id
WHERE (
        -- INPUT CONTACT ID --
        current_cr.initial_contact_id = 'A CONTACT ID'
        or current_cr.contact_id = 'SAME CONTACT ID AS ABOVE'
    )
order by current_cr.agent_connected_to_agent_timestamp asc
```

### Python コード
<a name="python-consultative-calls"></a>

```
"""Module Compare CTR's and establish relation"""
###############################################################################
# Usage python ctr_processor.py [Initial Contact ID]
# Example: python CTR_Processor.py 497f04ca-6de1-408f-9b8a-ec57bcc99b31
#
# Have your CTR record JSON files in the same directory as this Python module
# and execute the module as noted above. The input parameter is the
# Initial Contact ID / the Contact ID of the first leg of the call.
#
####################################################################z###########

import json
import re
import os
import sys
from dateutil import parser

PATH_OF_FILES   = './'
JSON            = '.json'
ENCODING        = 'UTF-8'
INTERACTION_DURN_THRESHOLD = 2
TYPE_INITIAL        = 'STAND ALONE'
TYPE_CONSULT        = 'CONSULT'
TYPE_EXT_CONSULT    = 'EXT_CONSULT'
TYPE_EXT_CONF       = 'EXT_CONFERENCE'
TYPE_CONFERENCE     = 'CONFERENCE'
TYPE_TRANSFER       = 'TRANSFER'
TYPE_UNKNOWN        = 'UNKNOWN'
CONTACT_STATE_INT   = 'INTERMEDIATE'
CONTACT_STATE_FINAL = 'FINAL'
CONTACT_STATE_START = 'START'
PRINT_INDENT        = 4

def process_ctr_records(ctr_array):
    """ Function to process CTR Records"""
    relation = {}
    output_list = []
    if ctr_array is None : return None
    for i, a_record in enumerate(ctr_array):
        if (prev_cid := a_record.get('PreviousContactId', None)) is not None:
            if (parent_ctr := get_parent_node(ctr_array, a_record['ContactId'], prev_cid)) is not None:
                relation = establish_relation(parent_ctr, a_record)
        else:
            relation = establish_parent(a_record)
        if relation is not None:
            output_list.append(relation)
    return output_list
           
def establish_parent(a_ctr):
    """ Establish the first record - the one that doesn't have a Previous Contact ID"""
    if a_ctr.get('Agent', None) is not None:
        return {
                'Agent': a_ctr['Agent']['Username']
                ,'ConnectedToAgentTimestamp': a_ctr['Agent']['ConnectedToAgentTimestamp']
                ,'Root Contact ID': a_ctr['ContactId']
                ,'Type': TYPE_INITIAL
                ,'Contact State': CONTACT_STATE_START
            }
   
def establish_relation(parent, child):
    """ Establish Conf / Transfer / Consult relation between two Agents"""
    if is_external_call(child):
        return establish_external_relation(parent, child)
    else:
        return establish_internal_relation(parent, child)

def establish_external_relation(parent, child):
    """ Establish Conf / Transfer / Consult relation between two Agents - External call"""
    ret = {
        'Parties': parent['Agent']['Username'] + ' <-> External:' + child['CustomerEndpoint']['Address']
        ,'Contact State': parent.get('Contact State', CONTACT_STATE_INT)
        ,'ConnectedToAgentTimestamp': child['ConnectedToSystemTimestamp']
    }

    parent_acw_start_ts = parser.parse(parent['Agent']['AfterContactWorkStartTimestamp'])
    child_disconnect_ts  = parser.parse(child['DisconnectTimestamp'])
    if (parent_acw_start_ts - child_disconnect_ts).total_seconds() > 0: # Parent ended after child: Consult or conference
        ret['Type'] = TYPE_EXT_CONSULT if (parent['Agent']['CustomerHoldDuration'] - child['ExternalThirdParty']['ExternalThirdPartyInteractionDuration']) > INTERACTION_DURN_THRESHOLD else TYPE_EXT_CONF
    elif ((transfer_completed_ts := parser.parse(parent.get('TransferCompletedTimestamp', None))) is not None) and \
         ((child_disconnect_ts - transfer_completed_ts).total_seconds() > 0): # ACW started after transfer was completed
        ret['Type'] = TYPE_TRANSFER
    return ret
    
def establish_internal_relation(parent, child):
    """ Establish Conf / Transfer / Consult relation between two Agents - Internal call"""        
    ret = {
        'Parties': parent['Agent']['Username'] + ' <-> ' + child['Agent']['Username']
        ,'Contact State': parent.get('Contact State', CONTACT_STATE_INT)
        ,'Child Contact ID': child.get('ContactId', 'NOTHING')
        ,'ConnectedToAgentTimestamp': child['Agent']['ConnectedToAgentTimestamp']
    }

    parent_acw_start_ts = parser.parse(parent['Agent']['AfterContactWorkStartTimestamp'])
    child_acw_start_ts  = parser.parse(child['Agent']['AfterContactWorkStartTimestamp'])
 
    if (parent_acw_start_ts - child_acw_start_ts).total_seconds() > 0: # Parent ended after child: Consult or conference
        ret['Type'] = TYPE_CONSULT if child['Agent']['AgentInteractionDuration'] < INTERACTION_DURN_THRESHOLD else TYPE_CONFERENCE
    elif ((transfer_completed_ts := parser.parse(parent.get('TransferCompletedTimestamp', None))) is not None) and \
         ((child_acw_start_ts - transfer_completed_ts).total_seconds() > 0): # ACW started after transfer was completed
        ret['Type'] = TYPE_TRANSFER
    return ret

def is_external_call(a_record):
    """Is this an external call """
    if (a_record.get('Agent', None) is None and
        a_record.get('InitiationMethod', None) == 'EXTERNAL_OUTBOUND'):
        return True
    return False

def get_parent_node(ctr_array, child_cid, child_prev_cid):
    """ Get the parent node when we have a Previous Contact ID"""    
    for i, a_record in enumerate(ctr_array):
        if (parent_cid := a_record.get('ContactId', None)) is not None:
            if compare_strings(parent_cid, child_prev_cid):
                if (parent_next_cid := a_record.get('NextContactId', None)) is not None:
                    if compare_strings(parent_next_cid, child_cid):
                        return a_record |  {'Contact State': CONTACT_STATE_FINAL}
                    else:
                        return a_record
                else:
                    return a_record |  {'Contact State': CONTACT_STATE_INT}

def compare_strings(s1, s2):
    """ Compare two Contact IDs"""    
    if s1 is None or s2 is None : return False 
    return re.search(re.compile(s2), s1)

def read_all_ctr_records(a_cid):
    """ Read all the CTR records for a given Initial Contact ID. Modify for S3 read"""
    ctr_array = []
    for file_name in [file for file in os.listdir(PATH_OF_FILES) if file.endswith(JSON)]:
        with open(PATH_OF_FILES + file_name, encoding=ENCODING) as json_file:
            try:
                a_ctr = json.load(json_file)
            except ValueError:
                print('Error in parsing JSON. File name:[', file_name, ']')
            
            if a_ctr is not None:
                c_id = a_ctr['ContactId']
                init_cid = a_ctr.get('InitialContactId', None)
                if compare_strings(a_cid, c_id):
                    ctr_array.append(a_ctr)
                elif compare_strings(a_cid, init_cid):
                    ctr_array.append(a_ctr)
            
    return ctr_array

def main():
    """ Entry point"""
    if len(sys.argv) < 2:
        print('Incorrect number of arguments (', len(sys.argv), ') --> python ctr_processor.py [Initial Contact ID]')
        return
    else:
        output_list = process_ctr_records(read_all_ctr_records(sys.argv[1]))
        if output_list is not None and len(output_list) > 0:
            output_list.sort(key=lambda x: x['ConnectedToAgentTimestamp'])
            for i, an_entry in enumerate(output_list):
                print(json.dumps(an_entry, indent=PRINT_INDENT))
        else:
            print('Unable to find Contact ID:[', sys.argv[1], '] in the input CTR Records. Please check the files and try again.')

if __name__ == "__main__":
    main()
```

### JS コード
<a name="js-consultative-calls"></a>

```
// Has a dependency on the following Node.js modules: - date-fns, fs, path
//sample input: node index.js 497f04ca-6de1-408f-9b8a-ec57bcc99b31

const fs = require('fs');
const path = require('path');
const { parseISO } = require('date-fns');

const PATH_OF_FILES = './';
const JSON_EXT = '.json';
const ENCODING = 'UTF-8';
const INTERACTION_DURATION_THRESHOLD = 2;
const CONTACT_TYPES = {
    INITIAL: 'STAND ALONE',
    CONSULT: 'CONSULT',
    EXTERNAL_CONSULT: 'EXT_CONSULT',
    EXTERNAL_CONFERENCE: 'EXT_CONFERENCE',
    CONFERENCE: 'CONFERENCE',
    TRANSFER: 'TRANSFER',
    EXTERNAL_TRANSFER: 'EXT_TRANSFER',
};
const CONTACT_STATES = {
    INTERMEDIATE: 'INTERMEDIATE',
    FINAL: 'FINAL',
    START: 'START',
};
const PRINT_INDENT = 4;

function processCtrRecords(ctrArray) {
    if (!ctrArray) return null;
    const outputList = [];

    ctrArray.forEach(record => {
        let relation = null;
        const prevCid = record.PreviousContactId;
        if (prevCid) {
            const parentRecord = findParentRecord(ctrArray, record.ContactId, prevCid);
            if (parentRecord) {
                relation = establishRelation(parentRecord, record);
            }
        } else {
            relation = establishInitialRecord(record);
        }
        if (relation) {
            outputList.push(relation);
        }
    });

    return outputList;
}

function establishInitialRecord(record) {
    if (record.Agent) {
        return {
            'Agent': record.Agent.Username,
            'ConnectedToAgentTimestamp': record.Agent.ConnectedToAgentTimestamp,
            'Root Contact ID': record.ContactId,
            'Type': CONTACT_TYPES.INITIAL,
            'Contact State': CONTACT_STATES.START,
        };
    }
}

function establishRelation(parent, child) {
    return isExternalCall(child)
        ? establishExternalRelation(parent, child)
        : establishInternalRelation(parent, child);
}

function establishExternalRelation(parent, child) {
    const parentAcwStartTs = parent.Agent?.AfterContactWorkStartTimestamp
        ? parseISO(parent.Agent.AfterContactWorkStartTimestamp)
        : null;
    const childDisconnectTs = child.DisconnectTimestamp
        ? parseISO(child.DisconnectTimestamp)
        : null;

    const relation = {
        'Parties': `${parent.Agent.Username} <-> External:${child.CustomerEndpoint.Address}`,
        'Contact State': parent['Contact State'] || CONTACT_STATES.INTERMEDIATE,
        'ConnectedToAgentTimestamp': child.ConnectedToSystemTimestamp,
    };

    if (parentAcwStartTs && childDisconnectTs && (parentAcwStartTs - childDisconnectTs) > 0) {
        if (parent.Agent.CustomerHoldDuration - child.ExternalThirdParty.ExternalThirdPartyInteractionDuration > INTERACTION_DURATION_THRESHOLD) {
            relation['Type'] = CONTACT_TYPES.EXTERNAL_CONSULT;
        } else {
            relation['Type'] = CONTACT_TYPES.EXTERNAL_CONFERENCE;
        }
    } else if (parent.TransferCompletedTimestamp) {
        const transferCompletedTs = parseISO(parent.TransferCompletedTimestamp);
        if (transferCompletedTs && childDisconnectTs && (childDisconnectTs - transferCompletedTs) > 0) {
            relation['Type'] = CONTACT_TYPES.EXTERNAL_TRANSFER;
        }
    }

    return relation;
}

function establishInternalRelation(parent, child) {
    const parentAcwStartTs = parent.Agent?.AfterContactWorkStartTimestamp
        ? parseISO(parent.Agent.AfterContactWorkStartTimestamp)
        : null;
    const childAcwStartTs = child.Agent?.AfterContactWorkStartTimestamp
        ? parseISO(child.Agent.AfterContactWorkStartTimestamp)
        : null;

    const relation = {
        'Parties': `${parent.Agent.Username} <-> ${child.Agent.Username}`,
        'Contact State': parent['Contact State'] || CONTACT_STATES.INTERMEDIATE,
        'Child Contact ID': child.ContactId || 'NOTHING',
        'ConnectedToAgentTimestamp': child.Agent.ConnectedToAgentTimestamp,
    };

    if (parentAcwStartTs && childAcwStartTs && (parentAcwStartTs - childAcwStartTs) > 0) {
        relation['Type'] = child.Agent.AgentInteractionDuration < INTERACTION_DURATION_THRESHOLD
            ? CONTACT_TYPES.CONSULT
            : CONTACT_TYPES.CONFERENCE;
    } else if (parent.TransferCompletedTimestamp) {
        const transferCompletedTs = parseISO(parent.TransferCompletedTimestamp);
        if (transferCompletedTs && childAcwStartTs && (childAcwStartTs - transferCompletedTs) > 0) {
            relation['Type'] = CONTACT_TYPES.TRANSFER;
        }
    }

    return relation;
}

function isExternalCall(record) {
    return !record.Agent && record.InitiationMethod === 'EXTERNAL_OUTBOUND';
}

function findParentRecord(ctrArray, childCid, childPrevCid) {
    for (const record of ctrArray) {
        const parentCid = record.ContactId;
        if (compareStrings(parentCid, childPrevCid)) {
            const parentNextCid = record.NextContactId;
            if (parentNextCid && compareStrings(parentNextCid, childCid)) {
                return { ...record, 'Contact State': CONTACT_STATES.FINAL };
            } else {
                return { ...record, 'Contact State': CONTACT_STATES.INTERMEDIATE };
            }
        }
    }
    return null;
}

function compareStrings(s1, s2) {
    return s1 && s2 && s1.includes(s2);
}

function readAllCtrRecords(contactId) {
    return fs.readdirSync(PATH_OF_FILES)
        .filter(file => file.endsWith(JSON_EXT))
        .map(fileName => JSON.parse(fs.readFileSync(path.join(PATH_OF_FILES, fileName), ENCODING)))
        .filter(record => compareStrings(contactId, record.ContactId) || compareStrings(contactId, record.InitialContactId));
}

function main() {
    const [initialContactId] = process.argv.slice(2);
    if (!initialContactId) {
        console.log('Usage: node index.js [Initial Contact ID]');
        return;
    }

    const outputList = processCtrRecords(readAllCtrRecords(initialContactId));
    if (outputList.length) {
        outputList.sort((a, b) => new Date(a.ConnectedToAgentTimestamp) - new Date(b.ConnectedToAgentTimestamp));
        outputList.forEach(entry => console.log(JSON.stringify(entry, null, PRINT_INDENT)));
    } else {
        console.log(`Unable to find Contact ID: [${initialContactId}]. Please check and try again.`);
    }
}

if (require.main === module) {
    main();
}
```