Skip to content
Merged
18 changes: 18 additions & 0 deletions ai/generative-ai-service/cx-conversations-analysis/files/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,24 @@ Create a .config file with the following variables:

# About the App
Built using Streamlit and OCI Python SDK, it allows the user to upload a list of audio files (recordings of a call center) to be analysed.

```mermaid
flowchart TD
A[User Uploads Audio Files] --> B[Upload to OCI Object Storage]
B --> C[OCI Speech Service<br/>Transcription]
C --> D[Transcription Text]
D --> E[OCI Language Service<br/>Sentiment Analysis]
D --> F[OCI Generative AI<br/>Summary & Insights]
E --> G[Sentiment Score]
F --> H[Call Summary<br/>Reason, Issue Status, Info Requested]
G --> I[Per-Call View]
H --> I
H --> J[Batch Categorization]
J --> K[Batch Analytics Dashboard<br/>Metrics & Reports]
I --> L[Display Results]
K --> L
```

Process:
1. Files are sent to a bucket.
2. Speech processes them and gets a transcription in JSON format.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
TextContent,
UserMessage,
)
from oci.ai_language.models import (
BatchDetectLanguageSentimentsDetails,
TextDocument,
)


class SpeechPipeline:
Expand All @@ -33,7 +37,7 @@ def upload_file_to_object_storage(self, file_path, object_name):
object_name=object_name,
put_object_body=f,
)
print("Upload complete to bucket complete.")
# File uploaded to object storage successfully
return response

def get_transcription(
Expand Down Expand Up @@ -75,6 +79,8 @@ def get_transcription(
model_config.domain = "GENERIC"
model_config.language_code = "en-US"
else:
# Available Whisper models: WHISPER_MEDIUM, WHISPER_LARGE_V2 (requires service request)
# Using WHISPER_MEDIUM as default since WHISPER_LARGE_V2 requires service request
Comment thread
ysocarras-oracle marked this conversation as resolved.
model_config.model_type = "WHISPER_LARGE_V3T"
model_config.domain = "GENERIC"
model_config.language_code = "en"
Expand Down Expand Up @@ -107,18 +113,30 @@ def get_transcription(
transcription_job.data.lifecycle_state == "IN_PROGRESS"
or transcription_job.data.lifecycle_state == "ACCEPTED"
):
print(
f"Job {job_id} is IN_PROGRESS for {str(seconds)} seconds, progress: {transcription_job.data.percent_complete}"
)
# Job progress is handled by UI progress indicators
sleep(2)
seconds += 2
transcription_job = self.client.get_transcription_job(
transcription_job_id=job_id
)

print(f"Job {job_id} is in {transcription_job.data.lifecycle_state} state.")
if transcription_job.data.lifecycle_state == "FAILED":
return f"Transcription job {job_id} failed."
# Extract error details if available
error_message = f"Transcription job {job_id} failed."
error_details = []

# Check for lifecycle_details
if hasattr(transcription_job.data, 'lifecycle_details') and transcription_job.data.lifecycle_details:
error_details.append(f"Lifecycle details: {transcription_job.data.lifecycle_details}")

# Check for error_message
if hasattr(transcription_job.data, 'error_message') and transcription_job.data.error_message:
error_details.append(f"Error message: {transcription_job.data.error_message}")

if error_details:
error_message += " " + " | ".join(error_details)

return error_message

# Getting response from object storage
list_response = self.object_client.list_objects(
Expand Down Expand Up @@ -156,18 +174,50 @@ def get_chat_response(self, system_prompt, user_prompt, model_id):
user_msg = UserMessage(content=[TextContent(text=str(user_prompt))])
messages = [system_msg, user_msg]

# ----- 2. Configure the generic chat request to highlight model features -----
chat_request = GenericChatRequest(
api_format=BaseChatRequest.API_FORMAT_GENERIC,
messages=messages,
# generation controls
max_tokens=128000,
temperature=0.5,
top_p=0.8,
top_k=-1,
frequency_penalty=0.0,
presence_penalty=0.0,
)
# ----- 2. Configure the generic chat request with model-specific parameters -----
# Model-specific parameter configuration based on provider
is_grok_model = model_id.startswith("xai.")
is_meta_model = model_id.startswith("meta.")

# Base parameters for all models
chat_request_params = {
"api_format": BaseChatRequest.API_FORMAT_GENERIC,
"messages": messages,
}

# Model-specific parameter configuration
if is_grok_model:
# Grok-specific parameters (based on reference implementation)
# Grok doesn't support presence_penalty and frequency_penalty
chat_request_params.update({
"max_tokens": 128000,
"temperature": 1.0,
"top_p": 1.0,
"top_k": 0,
})
elif is_meta_model:
# Meta-specific parameters (based on Python reference implementation)
# IMPORTANT: Meta models have a maximum of 4096 tokens for max_tokens
chat_request_params.update({
"max_tokens": 4000, # Meta max is 4096, using 4000 for safety margin
"temperature": 1.0,
"top_p": 0.75,
"frequency_penalty": 0.0,
"presence_penalty": 0.0,
# Note: top_k is not used in Meta reference code
})
else:
# Default parameters for OpenAI and other models
chat_request_params.update({
"max_tokens": 128000,
"temperature": 0.5,
"top_p": 0.8,
"top_k": -1,
"frequency_penalty": 0.0,
"presence_penalty": 0.0,
})

chat_request = GenericChatRequest(**chat_request_params)

# ----- 3. Wrap in ChatDetails with on-demand serving mode -----
chat_detail = ChatDetails(
Expand All @@ -188,7 +238,146 @@ def get_chat_response(self, system_prompt, user_prompt, model_id):
return json_parsed


class SentimentAnalysisPipeline:
Comment thread
ysocarras-oracle marked this conversation as resolved.
"""
A class to analyze sentiment using OCI Language Service.
Converts OCI Language Service sentiment scores to a 1-10 scale.
"""

def __init__(self, config):
oci_config = oci.config.from_file(
config.CONFIG_FILE_PATH, profile_name=config.PROFILE_NAME
)
self.ai_language_client = oci.ai_language.AIServiceLanguageClient(oci_config)
self.config = config

def analyze_sentiment(self, text: str, level: str = "SENTENCE") -> dict:
"""
Analyze sentiment of text using OCI Language Service.

Args:
text: The text to analyze
level: "SENTENCE" or "ASPECT" (default: "SENTENCE")

Returns:
dict with sentiment_score (1-10) and sentiment details
"""
if not text or not text.strip():
return {
"sentiment_score": 5, # Neutral default
"sentiment": "neutral",
"confidence": 0.0,
}

if not level or not isinstance(level, str):
level = "SENTENCE"
level = level.upper().strip()
if level not in ["SENTENCE", "ASPECT"]:
level = "SENTENCE" # Default to SENTENCE if invalid

# Convert to list as required by the API
level_list = [level]

# Prepare document for analysis
document = TextDocument(key="doc1", text=text)
details = BatchDetectLanguageSentimentsDetails(
documents=[document]
)

# Call OCI Language Service
try:
response = self.ai_language_client.batch_detect_language_sentiments(
batch_detect_language_sentiments_details=details,
level=level_list,
)

if response.data and response.data.documents:
doc_result = response.data.documents[0]
document_sentiment = doc_result.document_sentiment.lower()
document_scores = doc_result.document_scores
normalized_scores = self._normalize_scores(document_scores)

# Convert OCI sentiment (positive/negative/neutral/mixed) to 1-10 scale
sentiment_score = self._convert_to_scale_1_10(
document_sentiment, normalized_scores
)

return {
"sentiment_score": sentiment_score,
"sentiment": document_sentiment,
"confidence": max(
normalized_scores.get("positive", 0),
normalized_scores.get("negative", 0),
normalized_scores.get("neutral", 0),
normalized_scores.get("mixed", 0),
),
"scores": normalized_scores,
}
except Exception as e:
print(f"Error in sentiment analysis: {e}")
# Return neutral default on error
return {
"sentiment_score": 5,
"sentiment": "neutral",
"confidence": 0.0,
}

def _normalize_scores(self, scores: dict) -> dict:
normalized_scores = {}
if isinstance(scores, dict):
for key, value in scores.items():
normalized_scores[str(key).lower()] = value
return normalized_scores

def _convert_to_scale_1_10(
self, sentiment: str, scores: dict
) -> int:
"""
Convert OCI Language Service sentiment to 1-10 scale.

OCI returns: positive, negative, neutral, mixed with confidence scores 0-1
We convert to: 1 (very negative) to 10 (very positive), 5 (neutral)

Formula:
- If positive: 5 + (positive_score * 5) -> range 5-10
- If negative: 5 - (negative_score * 4) -> range 1-5
- If neutral: 5
- If mixed: weighted average of positive and negative
"""
positive_score = scores.get("positive", 0.0) or 0.0
negative_score = scores.get("negative", 0.0) or 0.0
neutral_score = scores.get("neutral", 0.0) or 0.0
mixed_score = scores.get("mixed", 0.0) or 0.0

if sentiment == "positive":
# Map positive confidence (0-1) to 5-10 scale
return int(round(5 + (positive_score * 5)))
elif sentiment == "negative":
# Map negative confidence (0-1) to 1-5 scale
return int(round(5 - (negative_score * 4)))
elif sentiment == "neutral":
return 5
elif sentiment == "mixed":
# For mixed, calculate weighted average
# Positive contributes to higher scores, negative to lower
positive_contribution = positive_score * 5 # 0-5
negative_contribution = negative_score * 4 # 0-4
base_score = 5 + positive_contribution - negative_contribution
return int(round(max(1, min(10, base_score))))
else:
# Default to neutral
return 5


def post_process_trans(transcription_response, diarization=True):
# Handle error case: if transcription_response is a string (error message)
if isinstance(transcription_response, str):
raise ValueError(f"Transcription failed: {transcription_response}")

# Handle case where response doesn't have expected structure
if not hasattr(transcription_response, 'data') or not hasattr(transcription_response.data, 'content'):
raise ValueError(f"Invalid transcription response format: {type(transcription_response)}")

object_content_bytes = transcription_response.data.content # this is in bytes
object_content_str = object_content_bytes.decode("utf-8")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,15 @@
SPEECH_BUCKET_OUTPUT_PREFIX = "speech_output"

## Other config params
ORACLE_LOGO = "app_images/oracle_logo.png"
ORACLE_LOGO = "frontend/Oracle logo.png"
UPLOAD_PATH = "uploaded_files"
GENAI_MODELS = {
"OpenAI GPT-OSS 120b": "openai.gpt-oss-120b",
"OpenAI GPT-OSS 20b": "openai.gpt-oss-20b",

# Grok models (xAI)
"Grok Non Reasoning": "xai.grok-4-fast-non-reasoning",
# Meta models
"Meta Llama 3.3 70B": "meta.llama-3.3-70b-instruct",
# Add more models as needed - check OCI documentation for exact model IDs
}
LIST_GENAI_MODELS = list(GENAI_MODELS.keys())
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ def make_sidebar(config):
# --- Page selector (view) ---
st.markdown("#### Analysis view")
page = st.radio(
"",
"View",
("Per-call view", "Batch overview"),
index=0,
help="Switch between single-call inspection and batch-level insights.",
label_visibility="collapsed",
)

if not processed and page == "Batch overview":
Expand All @@ -31,12 +32,26 @@ def make_sidebar(config):
accept_multiple_files=True,
)

st.markdown("#### Model")
st.markdown("#### Models")
selected_speech_model = st.selectbox(
"Speech transcription model",
("Oracle", "Whisper"),
index=0,
help="Oracle: OCI's native speech model. Whisper: OpenAI Whisper Large V3.",
)

selected_model = st.selectbox(
"Oracle Generative AI model",
config.LIST_GENAI_MODELS,
)

sentiment_method = st.selectbox(
"Sentiment analysis method",
("GenAI", "OCI Language"),
index=0,
help="GenAI uses prompt-based custom criteria. OCI Language uses the managed sentiment service.",
)

run_button = st.button("Run")

# Sidebar info card using .sidebar-tips / .param-display styles
Expand Down Expand Up @@ -64,12 +79,27 @@ def make_sidebar(config):
<span class="param-value">{file_count}</span>
</div>
<div class="param-row">
<span class="param-label">Model</span>
<span class="param-label">Speech model</span>
<span class="param-value">{selected_speech_model}</span>
</div>
<div class="param-row">
<span class="param-label">GenAI model</span>
<span class="param-value">{selected_model}</span>
</div>
<div class="param-row">
<span class="param-label">Sentiment method</span>
<span class="param-value">{sentiment_method}</span>
</div>
</div>
""",
unsafe_allow_html=True,
)

return uploaded_files, run_button, selected_model, page
return (
uploaded_files,
run_button,
selected_model,
selected_speech_model,
sentiment_method,
page,
)
Loading