### Documentation: Call Logging
#### 1. Overview
The "Dive In" feature is designed to manage telephone call logs starting with a basic logging object initialized when a call is received. This object is enriched with conversation updates, dialogue details, WAV file uploads (with SAS tokens), call finalization (including summarization and sentiment analysis), business events, and statistical logging. The system uses the updated `KalimeraConversation` model for persistent storage and aligns with data from the API endpoints (e.g., `RegisterCall`, `RegisterCDR`, etc.). Additional tables (`Statistics_Transactions`, `Statistics_DB_YTD`, `Statistics_DB_MONTH`, `Statistics_DB_DAY`, `BILLING_TRANSACTION`, and `BILLING_DB`) are introduced to gather and aggregate statistical and billing data.
- **Objective**: Create a JSON structure and define methods to handle the full lifecycle of a telephone call log, including statistical and billing calculations.
- **Key Components**:
- Basic logging object with core call details.
- Methods to update conversation, dialogue, WAV files (with SAS tokens), and finalize calls.
- Business event logging, statistics generation, and billing tracking.
---
#### DataTable
```python
class CallStatusEnum(db.Enum):
dropcall = "dropcall"
refused_call = "refused_call"
success = "success"
converted = "converted"
couldnotfind = "couldnotfind"
numberdoesnotexist = "numberdoesnotexist"
class CallTypeEnum(db.Enum):
answer = "answer"
notanswer = "notanswer"
wrongnumber = "wrongnumber"
cannotreach = "cannotreach"
class VoiceTypeEnum(db.Enum):
man = "man"
woman = "woman"
class StatStatusEnum(db.Enum):
answer = "answer"
noanswer = "noanswer"
longwait = "longwait"
averagewait = "averagewait"
reject = "reject"
comments = "comments"
recall = "recall"
complain = "complain"
class DayPartEnum(db.Enum):
sunday = 0
monday = 1
tuesday = 2
wednesday = 3
thursday = 4
friday = 5
saturday = 6
class CallTypeStatEnum(db.Enum):
inbound = "inbound"
outbound = "outbound"
# Main Call Logging Table
class KalimeraConversation(db.Model):
__tablename__ = "kalimera_conversations"
__table_args__ = (
db.PrimaryKeyConstraint("id", name="kalimera_conversation_pkey"),
db.Index("kalimera_conversation_idx", "agent_id", "tenant_id", "campaign_id", "dialplan_id"),
)
# Core Identifiers
id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()"))
campaign_id = db.Column(StringUUID, nullable=True)
dialplan_id = db.Column(StringUUID, nullable=True)
customer_id = db.Column(Integer, nullable=True)
voice_id = db.Column(StringUUID, nullable=True)
voice_name = db.Column(db.String(255), nullable=True)
voice_type = db.Column(VoiceTypeEnum, nullable=True)
# Call Details
from_number = db.Column(db.String(255), nullable=True)
to_number = db.Column(db.String(255), nullable=True)
sip_number = db.Column(db.String(255), nullable=True)
sip_id = db.Column(db.String(255), nullable=True)
call_type = db.Column(CallTypeEnum, nullable=True)
calltime = db.Column(DateTime, nullable=False, server_default=func.current_timestamp())
day = db.Column(Integer, nullable=True)
month = db.Column(Integer, nullable=True)
year = db.Column(Integer, nullable=True)
# Dialogue Details
dialog_start_time = db.Column(DateTime, nullable=True)
dialog_end_time = db.Column(DateTime, nullable=True)
dialog_duration = db.Column(Integer, nullable=True)
# Status and Comments
status = db.Column(CallStatusEnum, nullable=True)
auditor_comments = db.Column(db.Text, nullable=True)
comments = db.Column(db.Text, nullable=True)
# Agent and Version
agent_id = db.Column(StringUUID, nullable=True)
version_number = db.Column(StringUUID, nullable=True)
# Billing
charge_minutes = db.Column(Integer, nullable=True)
charge_timestamp = db.Column(DateTime, nullable=True)
charge_packet_id = db.Column(db.String(255), nullable=True)
# Azure Blob Storage URLs and SAS Tokens
voice_input_only_url = db.Column(db.String, nullable=True)
voice_output_only_url = db.Column(db.String, nullable=True)
voice_dialog_url = db.Column(db.String, nullable=True)
# Additional Metadata
language = db.Column(db.String(50), nullable=True)
metadata = db.Column(JSON, nullable=True)
customer_data = db.Column(JSON, nullable=True)
updated_at = db.Column(DateTime, nullable=False, server_default=func.current_timestamp(), onupdate=func.current_timestamp())
# Statistics Transaction Table
class Statistics_Transactions(db.Model):
__tablename__ = "statistics_transactions"
id = db.Column(Integer, primary_key=True, autoincrement=True)
type = db.Column(CallTypeStatEnum, nullable=False)
time = db.Column(db.String(8), nullable=False)
day = db.Column(db.String(10), nullable=False)
duration = db.Column(Integer, nullable=False)
sentiment_positive = db.Column(Float, nullable=True)
sentiment_negative = db.Column(Float, nullable=True)
sentiment_neutral = db.Column(Float, nullable=True)
status = db.Column(StatStatusEnum, nullable=False)
campaign_id = db.Column(StringUUID, nullable=True)
agent_id = db.Column(StringUUID, nullable=True)
included_in_db_ytd = db.Column(Boolean, nullable=False, default=False)
included_in_db_month = db.Column(Boolean, nullable=False, default=False)
included_in_db_day = db.Column(Boolean, nullable=False, default=False)
# Statistics Year-to-Date Table
class Statistics_DB_YTD(db.Model):
__tablename__ = "statistics_db_ytd"
id = db.Column(Integer, primary_key=True, autoincrement=True)
year = db.Column(SmallInteger, nullable=False)
type = db.Column(CallTypeStatEnum, nullable=False)
talk_time = db.Column(Integer, nullable=False)
avg_talk_time = db.Column(Integer, nullable=True)
number_of_calls = db.Column(Integer, nullable=False)
number_of_answered = db.Column(Integer, nullable=True)
number_of_not_answered = db.Column(Integer, nullable=True)
number_of_no_line = db.Column(Integer, nullable=True)
number_of_no_busy = db.Column(Integer, nullable=True)
number_of_no_lineissue = db.Column(Integer, nullable=True)
number_of_no_reject = db.Column(Integer, nullable=True)
number_of_no_approve = db.Column(Integer, nullable=True)
number_of_no_complain = db.Column(Integer, nullable=True)
# Statistics Month Table
class Statistics_DB_MONTH(db.Model):
__tablename__ = "statistics_db_month"
id = db.Column(Integer, primary_key=True, autoincrement=True)
year = db.Column(SmallInteger, nullable=False)
month = db.Column(SmallInteger, nullable=False)
type = db.Column(CallTypeStatEnum, nullable=False)
talk_time = db.Column(Integer, nullable=False)
avg_talk_time = db.Column(Integer, nullable=True)
number_of_calls = db.Column(Integer, nullable=False)
number_of_answered = db.Column(Integer, nullable=True)
number_of_not_answered = db.Column(Integer, nullable=True)
number_of_no_line = db.Column(Integer, nullable=True)
number_of_no_busy = db.Column(Integer, nullable=True)
number_of_no_lineissue = db.Column(Integer, nullable=True)
number_of_no_reject = db.Column(Integer, nullable=True)
number_of_no_approve = db.Column(Integer, nullable=True)
number_of_no_complain = db.Column(Integer, nullable=True)
# Statistics Day Table
class Statistics_DB_DAY(db.Model):
__tablename__ = "statistics_db_day"
id = db.Column(Integer, primary_key=True, autoincrement=True)
year = db.Column(SmallInteger, nullable=False)
month = db.Column(SmallInteger, nullable=False)
datenum = db.Column(SmallInteger, nullable=False)
datepart = db.Column(DayPartEnum, nullable=False)
type = db.Column(CallTypeStatEnum, nullable=False)
talk_time = db.Column(Integer, nullable=False)
avg_talk_time = db.Column(Integer, nullable=True)
number_of_calls = db.Column(Integer, nullable=False)
number_of_answered = db.Column(Integer, nullable=True)
number_of_not_answered = db.Column(Integer, nullable=True)
number_of_no_line = db.Column(Integer, nullable=True)
number_of_no_busy = db.Column(Integer, nullable=True)
number_of_no_lineissue = db.Column(Integer, nullable=True)
number_of_no_reject = db.Column(Integer, nullable=True)
number_of_no_approve = db.Column(Integer, nullable=True)
number_of_no_complain = db.Column(Integer, nullable=True)
# Billing Transaction Table
class BILLING_TRANSACTION(db.Model):
__tablename__ = "billing_transaction"
id = db.Column(Integer, primary_key=True, autoincrement=True)
year = db.Column(SmallInteger, nullable=False)
month = db.Column(SmallInteger, nullable=False)
datenum = db.Column(SmallInteger, nullable=False)
datepart = db.Column(DayPartEnum, nullable=False)
unit_charge = db.Column(Float, nullable=False)
type = db.Column(CallTypeStatEnum, nullable=False)
included_in_billing_db = db.Column(Boolean, nullable=False, default=False)
# Billing Database Table
class BILLING_DB(db.Model):
__tablename__ = "billing_db"
id = db.Column(Integer, primary_key=True, autoincrement=True)
year = db.Column(SmallInteger, nullable=False)
month = db.Column(SmallInteger, nullable=False)
datenum = db.Column(SmallInteger, nullable=False)
datepart = db.Column(DayPartEnum, nullable=False)
total_units = db.Column(Float, nullable=False)
type = db.Column(CallTypeStatEnum, nullable=False)
```
##### Column Explanation
- **KalimeraConversation**: (Unchanged from previous, refer to prior documentation for details.)
- **Statistics_Transactions**:
- **id**: Auto-incrementing primary key.
- **type**: Call type (inbound/outbound).
- **time**: Time of the call (HH:MM:SS).
- **day**: Date of the call (DD/MM/YYYY).
- **duration**: Duration in seconds.
- **sentiment_positive, sentiment_negative, sentiment_neutral**: Sentiment scores.
- **status**: Call status (answer, noanswer, longwait, averagewait, reject, comments, recall, complain).
- **campaign_id**: Campaign identifier.
- **agent_id**: Agent identifier.
- **included_in_db_ytd, included_in_db_month, included_in_db_day**: Flags for database inclusion.
- **Statistics_DB_YTD**:
- **id**: Auto-incrementing primary key.
- **year**: Year of the data.
- **type**: Call type (inbound/outbound).
- **talk_time**: Total talk time in seconds.
- **avg_talk_time**: Average talk time in seconds.
- **number_of_calls**: Total number of calls.
- **number_of_answered, number_of_not_answered, etc.**: Counts of various call outcomes.
- **Statistics_DB_MONTH**: Similar to `Statistics_DB_YTD` with added `month`.
- **Statistics_DB_DAY**: Similar to `Statistics_DB_MONTH` with added `datenum` and `datepart`.
- **BILLING_TRANSACTION**:
- **id**: Auto-incrementing primary key.
- **year, month, datenum, datepart**: Date details.
- **unit_charge**: Charge per unit.
- **type**: Call type (inbound/outbound).
- **included_in_billing_db**: Flag for billing database inclusion.
- **BILLING_DB**:
- **id**: Auto-incrementing primary key.
- **year, month, datenum, datepart**: Date details.
- **total_units**: Total units charged.
- **type**: Call type (inbound/outbound).
##### Mapping to Requirements
- **Basic Login Object**: `conversationId` (id), `callerTrackId` (sip_number), `customerId` (customer_id), `campaignName` (campaign_id), `callType` (call_type), `callTime` (calltime).
- **Conversation Updates**: Stored in `metadata` (customerSiteInfo, systemAnswers).
- **Dialogue Details**: `dialog_start_time`, `dialog_end_time`, `dialog_duration` in `KalimeraConversation`; feeds `Statistics_Transactions`.
- **WAV File Handling**: Uses `voice_*_url` and `voice_*_sas_token`.
- **Call Finalization**: `summary` and `sentiment` in `metadata`; sentiment feeds `Statistics_Transactions`.
- **Business Events**: `events` in `metadata`; status feeds `Statistics_Transactions`.
- **Logging and Statistics**: Aggregated in `Statistics_Transactions`, `Statistics_DB_YTD`, `Statistics_DB_MONTH`, `Statistics_DB_DAY`.
- **Billing**: Tracked in `BILLING_TRANSACTION` and `BILLING_DB` using `charge_minutes` and `charge_timestamp`.
##### Example Metadata
```json
{
"customerSiteInfo": {
"siteId": "site-001",
"location": "New York"
},
"systemAnswers": ["Welcome to support", "How can I assist?"],
"dialogue": [
{
"startTime": "2025-04-10T02:00:00Z",
"endTime": "2025-04-10T02:02:00Z",
"durationSecs": 120
}
],
"summary": "Customer requested support and received assistance",
"sentiment": {
"positive": 0.6,
"neutral": 0.3,
"negative": 0.1
},
"events": [
{
"type": "refused_call",
"timestamp": "2025-04-10T02:02:00Z",
"comments": "Customer refused"
}
]
}
```
##### Example Customer Data
```json
{
"name": "John Doe",
"contact": "john.doe@example.com",
"preferences": {
"language": "English",
"timezone": "UTC"
},
"status": "active"
}
```
---
#### 2. JSON Structure and Methods
##### 2.1 Basic Object for Login
- **JSON Structure**: Initialized when a call is received.
```json
{
"conversationId": "550e8400-e29b-41d4-a716-446655440000",
"callerTrackId": "trunk-001",
"customerId": 12345,
"campaignName": "SupportCampaign",
"callType": "answer",
"callTime": "2025-04-10T02:00:00Z"
}
```
- **Method**: `POST /api/kalimera/init`
- **Purpose**: Generate the basic logging object.
- **Request Data** (JSON Body): Data from `RegisterCall` with `passport`.
- **Response Data** (JSON):
```json
{
"conversationId": "550e8400-e29b-41d4-a716-446655440000",
"message": "Call log initialized",
"callTime": "2025-04-10T02:00:00Z"
}
```
- **Implementation**:
```python
@app.route('/api/kalimera/init', methods=['POST'])
def initialize_call():
data = request.get_json()
passport = data.get('passport')
if not passport:
return jsonify({"error": "Passport required"}), 400
conversation = KalimeraConversation(
id=UUID(data['CallUniqueID']),
sip_number=data.get('trunkID', 'unknown'),
customer_id=data.get('customerID'),
title=data.get('queueName') if not data.get('campaign_id') else None,
campaign_id=UUID(data.get('campaign_id')) if data.get('campaign_id') else None,
call_type=CallTypeEnum(data.get('campaignType', 'answer').lower()) if data.get('campaignType') else CallTypeEnum.answer,
calltime=datetime.utcnow(),
day=datetime.utcnow().day,
month=datetime.utcnow().month,
year=datetime.utcnow().year
)
db.session.add(conversation)
db.session.commit()
return jsonify({
"conversationId": str(conversation.id),
"message": "Call log initialized",
"callTime": conversation.calltime.isoformat() + "Z"
}), 201
```
##### 2.2 Conversation Updates
- **JSON Structure Update**:
```json
{
"conversationId": "550e8400-e29b-41d4-a716-446655440000",
"customerSiteInfo": {
"siteId": "site-001",
"location": "New York"
},
"systemAnswers": ["Welcome to support"]
}
```
- **Method**: `POST /api/kalimera/update-conversation`
- **Purpose**: Update with site info and system answers.
- **Request Data** (JSON Body): Includes `conversationId`, `customerSiteInfo`, `systemAnswers`, `passport`.
- **Response Data** (JSON):
```json
{
"conversationId": "550e8400-e29b-41d4-a716-446655440000",
"message": "Conversation updated",
"updated_at": "2025-04-10T02:05:00Z"
}
```
- **Implementation**:
```python
@app.route('/api/kalimera/update-conversation', methods=['POST'])
def update_conversation():
data = request.get_json()
passport = data.get('passport')
if not passport:
return jsonify({"error": "Passport required"}), 400
conversation = KalimeraConversation.query.get_or_404(UUID(data['conversationId']))
conversation.metadata = conversation.metadata or {}
conversation.metadata.update({
"customerSiteInfo": data.get('customerSiteInfo', {}),
"systemAnswers": data.get('systemAnswers', [])
})
db.session.commit()
return jsonify({
"conversationId": str(conversation.id),
"message": "Conversation updated",
"updated_at": conversation.updated_at.isoformat() + "Z"
})
```
##### 2.3 Dialogue Details
- **JSON Structure Update**:
```json
{
"conversationId": "550e8400-e29b-41d4-a716-446655440000",
"dialog_start_time": "2025-04-10T02:00:00Z",
"dialog_end_time": "2025-04-10T02:02:00Z",
"dialog_duration": 120
}
```
- **Method**: `POST /api/kalimera/update-dialogue`
- **Purpose**: Update dialogue details and populate `Statistics_Transactions`.
- **Request Data** (JSON Body): Data from `RegisterCDR` with `passport`.
- **Response Data** (JSON):
```json
{
"conversationId": "550e8400-e29b-41d4-a716-446655440000",
"message": "Dialogue updated",
"updated_at": "2025-04-10T02:06:00Z"
}
```
- **Implementation**:
```python
@app.route('/api/kalimera/update-dialogue', methods=['POST'])
def update_dialogue():
data = request.get_json()
passport = data.get('passport')
if not passport:
return jsonify({"error": "Passport required"}), 400
conversation = KalimeraConversation.query.get_or_404(UUID(data['CallUniqueID']))
conversation.dialog_start_time = datetime.strptime(data['timeStart'], '%Y-%m-%dT%H:%M:%SZ')
conversation.dialog_end_time = datetime.strptime(data['timeEnd'], '%Y-%m-%dT%H:%M:%SZ')
conversation.dialog_duration = data['durationSecs']
# Populate Statistics_Transactions
stat_transaction = Statistics_Transactions(
type=CallTypeStatEnum(conversation.call_type.name.lower() if conversation.call_type else 'inbound'),
time=conversation.dialog_start_time.strftime('%H:%M:%S'),
day=conversation.calltime.strftime('%d/%m/%Y'),
duration=conversation.dialog_duration,
sentiment_positive=conversation.metadata.get('sentiment', {}).get('positive', 0.0) if conversation.metadata else 0.0,
sentiment_negative=conversation.metadata.get('sentiment', {}).get('negative', 0.0) if conversation.metadata else 0.0,
sentiment_neutral=conversation.metadata.get('sentiment', {}).get('neutral', 0.0) if conversation.metadata else 0.0,
status=StatStatusEnum(conversation.status.name.lower() if conversation.status else 'answer'),
campaign_id=conversation.campaign_id,
agent_id=conversation.agent_id
)
db.session.add(stat_transaction)
db.session.commit()
return jsonify({
"conversationId": str(conversation.id),
"message": "Dialogue updated",
"updated_at": conversation.updated_at.isoformat() + "Z"
})
```
##### 2.4 WAV File Handling
- **JSON Structure Update**:
```json
{
"conversationId": "550e8400-e29b-41d4-a716-446655440000",
"voice_input_only_url": "https://storage.blob.core.windows.net/input/audio1.wav",
"voice_output_only_url": "https://storage.blob.core.windows.net/output/audio1_response.wav",
"voice_dialog_url": "https://storage.blob.core.windows.net/conversation/audio1_full.wav",
"voice_input_only_sas_token": "sas_token_input",
"voice_output_only_sas_token": "sas_token_output",
"voice_dialog_sas_token": "sas_token_dialog"
}
```
- **Method**: `POST /api/kalimera/upload-wav`
- **Purpose**: Upload WAV files with SAS tokens.
- **Request Data**: Multipart form data with `conversationId`, WAV files, SAS tokens, and `passport`.
- **Response Data** (JSON):
```json
{
"conversationId": "550e8400-e29b-41d4-a716-446655440000",
"voice_input_only_url": "https://storage.blob.core.windows.net/input/audio1.wav",
"voice_output_only_url": "https://storage.blob.core.windows.net/output/audio1_response.wav",
"voice_dialog_url": "https://storage.blob.core.windows.net/conversation/audio1_full.wav",
"voice_input_only_sas_token": "sas_token_input",
"voice_output_only_sas_token": "sas_token_output",
"voice_dialog_sas_token": "sas_token_dialog",
"message": "WAV files uploaded",
"updated_at": "2025-04-10T02:07:00Z"
}
```
- **Implementation**:
```python
@app.route('/api/kalimera/upload-wav', methods=['POST'])
def upload_wav():
data = request.form.to_dict()
passport = data.get('passport')
if not passport:
return jsonify({"error": "Passport required"}), 400
conversation_id = data.get('conversationId')
conversation = KalimeraConversation.query.get_or_404(UUID(conversation_id))
blob_service = BlobServiceClient.from_connection_string("your_connection_string")
container_name = "audio"
for file_type in ['inputFile', 'outputFile', 'wholeFile']:
file = request.files.get(file_type)
if file:
blob_client = blob_service.get_blob_client(container=container_name, blob=f"{conversation_id}_{file_type}.wav")
blob_client.upload_blob(file)
url = f"https://{blob_service.account_name}.blob.core.windows.net/{container_name}/{conversation_id}_{file_type}.wav"
sas_token = blob_client.generate_shared_access_signature()
setattr(conversation, f"voice_{file_type.replace('File', '')}_url", url)
setattr(conversation, f"voice_{file_type.replace('File', '')}_sas_token", sas_token)
db.session.commit()
return jsonify({
"conversationId": str(conversation.id),
"voice_input_only_url": conversation.voice_input_only_url,
"voice_output_only_url": conversation.voice_output_only_url,
"voice_dialog_url": conversation.voice_dialog_url,
"voice_input_only_sas_token": conversation.voice_input_only_sas_token,
"voice_output_only_sas_token": conversation.voice_output_only_sas_token,
"voice_dialog_sas_token": conversation.voice_dialog_sas_token,
"message": "WAV files uploaded",
"updated_at": conversation.updated_at.isoformat() + "Z"
})
```
##### 2.5 Call Finalization
- **JSON Structure Update**:
```json
{
"conversationId": "550e8400-e29b-41d4-a716-446655440000",
"summary": "Customer requested support",
"sentiment": {
"positive": 0.6,
"neutral": 0.3,
"negative": 0.1
},
"status": "success"
}
```
- **Method**: `POST /api/kalimera/finalize`
- **Purpose**: Finalize the call and update statistics.
- **Request Data** (JSON Body): Includes `conversationId`, `summary`, `sentiment`, `status`, `passport`.
- **Response Data** (JSON):
```json
{
"conversationId": "550e8400-e29b-41d4-a716-446655440000",
"message": "Call finalized",
"updated_at": "2025-04-10T02:08:00Z"
}
```
- **Implementation**:
```python
@app.route('/api/kalimera/finalize', methods=['POST'])
def finalize_call():
data = request.get_json()
passport = data.get('passport')
if not passport:
return jsonify({"error": "Passport required"}), 400
conversation = KalimeraConversation.query.get_or_404(UUID(data['conversationId']))
conversation.status = CallStatusEnum(data.get('status', 'success'))
conversation.metadata = conversation.metadata or {}
conversation.metadata.update({
"summary": data.get('summary', 'Call completed'),
"sentiment": data.get('sentiment', {"positive": 0.5, "neutral": 0.3, "negative": 0.2})
})
# Update Statistics_Transactions if not already included
if not conversation.metadata.get('included_in_stats', False):
stat_transaction = Statistics_Transactions.query.filter_by(campaign_id=conversation.campaign_id, agent_id=conversation.agent_id).first()
if not stat_transaction:
stat_transaction = Statistics_Transactions(
type=CallTypeStatEnum(conversation.call_type.name.lower() if conversation.call_type else 'inbound'),
time=conversation.calltime.strftime('%H:%M:%S'),
day=conversation.calltime.strftime('%d/%m/%Y'),
duration=conversation.dialog_duration or 0,
sentiment_positive=conversation.metadata.get('sentiment', {}).get('positive', 0.0),
sentiment_negative=conversation.metadata.get('sentiment', {}).get('negative', 0.0),
sentiment_neutral=conversation.metadata.get('sentiment', {}).get('neutral', 0.0),
status=StatStatusEnum(conversation.status.name.lower() if conversation.status else 'answer'),
campaign_id=conversation.campaign_id,
agent_id=conversation.agent_id
)
db.session.add(stat_transaction)
conversation.metadata['included_in_stats'] = True
db.session.commit()
return jsonify({
"conversationId": str(conversation.id),
"message": "Call finalized",
"updated_at": conversation.updated_at.isoformat() + "Z"
})
```
##### 2.6 Business Events
- **JSON Structure Update**:
```json
{
"conversationId": "550e8400-e29b-41d4-a716-446655440000",
"events": [
{
"type": "refused_call",
"timestamp": "2025-04-10T02:02:00Z",
"comments": "Customer refused"
}
]
}
```
- **Method**: `POST /api/kalimera/log-event/<event_type>`
- **Purpose**: Log business events.
- **Request Data** (JSON Body): Varies by event type with `passport`.
- **Response Data** (JSON):
```json
{
"conversationId": "550e8400-e29b-41d4-a716-446655440000",
"message": "Event logged",
"updated_at": "2025-04-10T02:09:00Z"
}
```
- **Implementation**:
```python
@app.route('/api/kalimera/log-event/<string:event_type>', methods=['POST'])
def log_event(event_type):
data = request.get_json()
passport = data.get('passport')
if not passport:
return jsonify({"error": "Passport required"}), 400
conversation = KalimeraConversation.query.get_or_404(UUID(data['CallUniqueID']))
event = {
"type": event_type,
"timestamp": datetime.utcnow().isoformat() + "Z",
"comments": data.get('comments'),
"promiseDate": data.get('promiseDate'),
"promiseAmount": data.get('promiseAmount'),
"recallDateTime": data.get('recallDateTime')
}
conversation.metadata = conversation.metadata or {}
conversation.metadata['events'] = conversation.metadata.get('events', []) + [event]
conversation.status = CallStatusEnum(event_type)
db.session.commit()
return jsonify({
"conversationId": str(conversation.id),
"message": "Event logged",
"updated_at": conversation.updated_at.isoformat() + "Z"
})
```
##### 2.7 Logging and Statistics
- **JSON Structure for Stats**:
```json
{
"stats": {
"day": 10,
"month": 4,
"year": 2025,
"inboundCount": 50,
"outboundCount": 30,
"averageDialogDuration": 120
}
}
```
- **Method**: `GET /api/kalimera/statistics`
- **Purpose**: Generate and aggregate statistics.
- **Query Parameters**: `day`, `month`, `year`, `callType`, `passport`.
- **Response Data** (JSON): As above.
- **Implementation**:
```python
@app.route('/api/kalimera/statistics', methods=['GET'])
def get_statistics():
args = request.args
passport = args.get('passport')
if not passport:
return jsonify({"error": "Passport required"}), 400
day = args.get('day')
month = args.get('month')
year = args.get('year')
call_type = args.get('callType')
# Aggregate from Statistics_Transactions
query = Statistics_Transactions.query
if day:
query = query.filter(Statistics_Transactions.day.contains(f"/{day}/"))
if month:
query = query.filter(Statistics_Transactions.day.contains(f"/{month}/"))
if year:
query = query.filter(Statistics_Transactions.day.contains(f"/{year}"))
if call_type:
query = query.filter_by(type=CallTypeStatEnum(call_type.lower()))
transactions = query.all()
inbound_count = len([t for t in transactions if t.type == CallTypeStatEnum.inbound])
outbound_count = len([t for t in transactions if t.type == CallTypeStatEnum.outbound])
average_dialog_duration = sum(t.duration for t in transactions) / len(transactions) if transactions else 0
stats = {
"day": day or datetime.utcnow().day,
"month": month or datetime.utcnow().month,
"year": year or datetime.utcnow().year,
"inboundCount": inbound_count,
"outboundCount": outbound_count,
"averageDialogDuration": int(average_dialog_duration)
}
return jsonify({"stats": stats})
```
##### 2.8 Status and Additional Fields
- **JSON Structure Update**:
```json
{
"conversationId": "550e8400-e29b-41d4-a716-446655440000",
"status": "success",
"businessEvents": {
"refused_call": false,
"converted": true,
"comments": "Follow up required",
"auditor_comments": "Reviewed and approved"
}
}
```
- **Method**: Integrated into `log-event` and `finalize` methods.
---
#### 3. Integration with Existing Endpoints
- **POST** **RegisterCall**: Triggers `initialize_call`.
- ***Awaiting detailed documentation.***
- Will return a UUID.
- **GET** **GetConfiguration**: Feeds `update-conversation`.
- **Purpose**: Return the entire configuration for a new call instance to start.
- **Query Parameters**: `UUID` (the UUID returned by `RegisterCall` - *awaiting documentation*)
- **Response JSON**:
```json
{
"dialogVersion": "string",
"definitionVersion": "string",
"sttSectionConfig": {
"sttServiceToUse": "Azure",
"sttServiceConfig": {}
},
"ttsSectionConfig": {
"ttsServiceToUse": "Azure",
"voice": "string",
"ttsServiceConfig": {}
},
"callProcessingSectionConfig": {
"intervalMilliseconds": 0,
"minimumChunksForInterruptions": 0,
"backgroundAudioConfig": {
"enableBackgroundAudio": true,
"alwaysPlayBackgroundAudio": true,
"backgroundAudioVolume": 0
}
},
"dialogNodes": [
{
"nodeType": "Welcome",
"llmServiceToUse": "OpenAI",
"llmServiceConfig": {},
"temperature": 0,
"topP": 0,
"presencePenalty": 0,
"frequencyPenalty": 0,
"maxTokens": 0,
"provideDateTimeToLLM": true,
"currentDateTime": "2025-04-15T13:13:42.711Z",
"useFunctions": true,
"functionsToUse": [
"RegisterPromise"
],
"userPrompt": "string",
"systemPrompt": "string"
}
],
"outboundCustomerData": "string"
}
```
***outboundCustomerData is a nullable (generic) object that supports any schema.***
- The available **enums** are as follows (have to be passed as strings, not integers):
```json
{
"AvailableLLMFunctions": {
"enum": [
"RegisterPromise",
"RegisterNote",
"RegisterComplaint",
"RegisterRecall",
"RegisterRefusal",
"RegisterNonHumanNote"
],
"type": "string",
"description": "The available LLM functions"
},
"AvailableLLMNodeTypes": {
"enum": [
"Welcome",
"Dialog",
"Farewell"
],
"type": "string",
"description": "The available LLM node types"
},
"AvailableLLMServices": {
"enum": [
"OpenAI",
"AzureOpenAI",
"AzureAIInference",
"OpenAICompatible"
],
"type": "string",
"description": "The available LLM services"
},
"AvailableSTTServices": {
"enum": [
"Azure"
],
"type": "string",
"description": "The available STT services"
},
"AvailableTTSServices": {
"enum": [
"Azure",
"ElevenLabs"
],
"type": "string",
"description": "The available TTS services"
}
}
```
- **sttServiceConfig** can be **one** of the following depending on **sttServiceToUse**:
**Azure**:
```json
{
"apiKey": "string",
"region": "string",
"languageCode": "string",
"enableWordLevelTimestamps": true,
"enableAutomaticPunctuation": true,
"enableProfanityFilter": true,
"segmentationSilenceTimeoutMs": 0,
"initialSilenceTimeoutMs": 0,
"endSilenceTimeoutMs": 0,
"segmentationStrategy": "string"
}
```
- **ttsServiceConfig** can be **one** of the following depending on **ttsServiceToUse**:
**Azure**:
```json
{
"apiKey": "string",
"region": "string"
}
```
**ElevenLabs**:
```json
{
"apiKey": "string",
"modelId": "string",
"stability": 0,
"similarityBoost": 0,
"style": 0,
"useSpeakerBoost": true,
"speed": 0
}
```
- **llmServiceConfig** can be **one** of the following depending on **llmServiceToUse**:
**OpenAI**:
```json
{
"apiKey": "string",
"model": "string"
}
```
**AzureOpenAI**:
```json
{
"apiKey": "string",
"deploymentName": "string",
"endpoint": "string"
}
```
**AzureAIInference**:
```json
{
"apiKey": "string",
"endpoint": "string",
"model": "string"
}
```
**OpenAICompatible**:
```json
{
"apiKey": "string",
"baseUrl": "string",
"model": "string"
}
```
- **RegisterCallResult_***: Feeds `log-event`.
- **RegisterCDR**: Feeds `update-dialogue` and `Statistics_Transactions`.