Skip to content

Commit 5f988a8

Browse files
Merge pull request #102 from aprilwebster/master
integration of tone and conversation example code
2 parents c2f244d + 8338959 commit 5f988a8

File tree

5 files changed

+364
-0
lines changed

5 files changed

+364
-0
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# see README.md for details
2+
3+
CONVERSATION_USERNAME=<Conversation Username>
4+
CONVERSATION_PASSWORD=<Conversation Password>
5+
WORKSPACE_ID=<Workspace Id>
6+
7+
TONE_ANALYZER_USERNAME=<Tone Analyzer Username>
8+
TONE_ANALYZER_PASSWORD=<Tone Analyzer Password>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Conversation and Tone Analyzer Integration Example
2+
3+
This example provides sample code for integrating [Tone Analyzer][tone_analyzer] and [Conversation][conversation].
4+
5+
* [tone_detection.py][tone_conversation_integration_example_tone_detection] - sample code to initialize a user object in the conversation payload's context (initUser), to call Tone Analyzer to retrieve tone for a user's input (invokeToneAsync), and to update tone in the user object in the conversation payload's context (updateUserTone).
6+
7+
* [tone_conversation_integration.v1.py][tone_conversation_integration_example] - sample code to use tone_detection.py to get and add tone to the payload and send a request to the Conversation Service's message endpoint both in a synchronous and asynchronous manner.
8+
9+
10+
Requirements to run the sample code
11+
12+
* [Tone Analyzer Service credentials][bluemix_tone_analyzer_service]
13+
* [Conversation Service credentials][bluemix_conversation_service]
14+
* [Conversation Workspace ID][conversation_simple_workspace]
15+
16+
Credentials & the Workspace ID can be set in environment properties, a .env file, or directly in the code.
17+
18+
Dependencies provided in
19+
`init.py`
20+
21+
Command to run the sample code
22+
23+
`python tone_conversation_integration.v1.py`
24+
25+
[conversation]: https://www.ibm.com/watson/developercloud/conversation.html
26+
[tone_analyzer]: http://www.ibm.com/watson/developercloud/tone-analyzer.html
27+
[bluemix_conversation_service]: https://console.ng.bluemix.net/catalog/services/conversation/
28+
[bluemix_tone_analyzer_service]: https://console.ng.bluemix.net/catalog/services/tone-analyzer/
29+
[conversation_simple_workspace]: https://github.com/watson-developer-cloud/conversation-simple#workspace
30+
[tone_conversation_integration_example]: https://github.com/watson-developer-cloud/python-sdk/tree/master/examples/tone_conversation_integration.v1.py
31+
[tone_conversation_integration_example_tone_detection]: https://github.com/watson-developer-cloud/python-sdk/tree/master/examples/conversation_addons/tone_detection.py
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Copyright 2016 IBM All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from .watson_developer_cloud_service import WatsonDeveloperCloudService
16+
from .watson_developer_cloud_service import WatsonException
17+
from .watson_developer_cloud_service import WatsonInvalidArgument
18+
from .conversation_v1 import ConversationV1
19+
from .tone_analyzer_v3 import ToneAnalyzerV3
20+
21+
22+
from .version import __version__
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import json
2+
import os
3+
from dotenv import load_dotenv, find_dotenv
4+
import asyncio
5+
6+
from watson_developer_cloud import ConversationV1
7+
from watson_developer_cloud import ToneAnalyzerV3
8+
9+
# import tone detection
10+
import tone_detection
11+
12+
# load the .env file containing your environment variables for the required services (conversation and tone)
13+
load_dotenv(find_dotenv())
14+
15+
# replace with your own conversation credentials or put them in a .env file
16+
conversation = ConversationV1(
17+
username=os.environ.get('CONVERSATION_USERNAME') or 'YOUR SERVICE NAME',
18+
password=os.environ.get('CONVERSATION_PASSWORD') or 'YOUR PASSWORD',
19+
version='2016-07-11')
20+
21+
# replace with your own tone analyzer credentials
22+
tone_analyzer = ToneAnalyzerV3(
23+
username=os.environ.get('TONE_ANALYZER_USERNAME') or 'YOUR SERVICE NAME',
24+
password=os.environ.get('TONE_ANALYZER_PASSWORD') or 'YOUR SERVICE NAME',
25+
version='2016-02-11')
26+
27+
# replace with your own workspace_id
28+
workspace_id = os.environ.get('WORKSPACE_ID') or 'YOUR WORKSPACE ID'
29+
30+
# This example stores tone for each user utterance in conversation context.
31+
# Change this to false, if you do not want to maintain history
32+
maintainToneHistoryInContext = True
33+
34+
# Payload for the Watson Conversation Service
35+
# user input text required - replace "I am happy" with user input text.
36+
payload = {
37+
'workspace_id':workspace_id,
38+
'input': {
39+
'text': "I am happy"
40+
}
41+
}
42+
43+
def invokeToneConversation (payload, maintainToneHistoryInContext):
44+
'''
45+
invokeToneConversation calls the the Tone Analyzer service to get the tone information for the user's
46+
input text (input['text'] in the payload json object), adds/updates the user's tone in the payload's context,
47+
and sends the payload to the conversation service to get a response which is printed to screen.
48+
:param payload: a json object containing the basic information needed to converse with the Conversation Service's message endpoint.
49+
:param maintainHistoryInContext:
50+
51+
52+
Note: as indicated below, the console.log statements can be replaced with application-specific code to process the err or data object returned by the Conversation Service.
53+
'''
54+
tone = tone_analyzer.tone(text=payload['input']['text'])
55+
conversation_payload = tone_detection.updateUserTone(payload, tone, maintainToneHistoryInContext)
56+
response = conversation.message(workspace_id=workspace_id, message_input=conversation_payload['input'], context=conversation_payload['context'])
57+
print(json.dumps(response, indent=2))
58+
59+
async def invokeToneConversationAsync (payload, maintainToneHistoryInContext):
60+
tone = await tone_detection.invokeToneAsync(payload,tone_analyzer)
61+
conversation_payload = tone_detection.updateUserTone(payload, tone, maintainToneHistoryInContext)
62+
response = conversation.message(workspace_id=workspace_id, message_input=conversation_payload['input'], context=conversation_payload['context'])
63+
print(json.dumps(response, indent=2))
64+
65+
66+
# invoke tone aware calls to conversation - either synchronously or asynchronously
67+
68+
# synchronous call to conversation with tone included in the context
69+
invokeToneConversation(payload,maintainToneHistoryInContext)
70+
71+
# asynchronous call to conversation with tone included in the context
72+
loop = asyncio.get_event_loop()
73+
loop.run_until_complete(invokeToneConversationAsync(payload,maintainToneHistoryInContext))
74+
loop.close()
75+
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
"""
2+
* Copyright 2015 IBM Corp. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
"""
16+
17+
"""
18+
* Thresholds for identifying meaningful tones returned by the Watson Tone Analyzer. Current values are
19+
* based on the recommendations made by the Watson Tone Analyzer at
20+
* https://www.ibm.com/watson/developercloud/doc/tone-analyzer/understanding-tone.shtml
21+
* These thresholds can be adjusted to client/domain requirements.
22+
"""
23+
24+
import json
25+
import asyncio
26+
27+
PRIMARY_EMOTION_SCORE_THRESHOLD = 0.5
28+
WRITING_HIGH_SCORE_THRESHOLD = 0.75
29+
WRITING_NO_SCORE_THRESHOLD = 0.0
30+
SOCIAL_HIGH_SCORE_THRESHOLD = 0.75
31+
SOCIAL_LOW_SCORE_THRESHOLD = 0.25
32+
33+
# Labels for the tone categories returned by the Watson Tone Analyzer
34+
EMOTION_TONE_LABEL = 'emotion_tone'
35+
WRITING_TONE_LABEL = 'writing_tone'
36+
SOCIAL_TONE_LABEL = 'social_tone'
37+
38+
'''
39+
* updateUserTone processes the Tone Analyzer payload to pull out the emotion, writing and social
40+
* tones, and identify the meaningful tones (i.e., those tones that meet the specified thresholds).
41+
* The conversationPayload json object is updated to include these tones.
42+
* @param conversationPayload json object returned by the Watson Conversation Service
43+
* @param toneAnalyzerPayload json object returned by the Watson Tone Analyzer Service
44+
* @returns conversationPayload where the user object has been updated with tone information from the toneAnalyzerPayload
45+
'''
46+
def updateUserTone (conversationPayload, toneAnalyzerPayload, maintainHistory):
47+
48+
emotionTone = None
49+
writingTone = None
50+
socialTone = None
51+
52+
# if there is no context in a
53+
if 'context' not in conversationPayload:
54+
conversationPayload['context'] = {};
55+
56+
if 'user' not in conversationPayload['context']:
57+
conversationPayload['context'] = initUser()
58+
59+
# For convenience sake, define a variable for the user object
60+
user = conversationPayload['context']['user'];
61+
62+
# Extract the tones - emotion, writing and social
63+
if toneAnalyzerPayload and toneAnalyzerPayload['document_tone']:
64+
for toneCategory in toneAnalyzerPayload['document_tone']['tone_categories']:
65+
if toneCategory['category_id'] == EMOTION_TONE_LABEL:
66+
emotionTone = toneCategory
67+
if toneCategory['category_id'] == WRITING_TONE_LABEL:
68+
writingTone = toneCategory
69+
if toneCategory['category_id'] == SOCIAL_TONE_LABEL:
70+
socialTone = toneCategory
71+
72+
updateEmotionTone(user, emotionTone, maintainHistory)
73+
updateWritingTone(user, writingTone, maintainHistory)
74+
updateSocialTone(user, socialTone, maintainHistory)
75+
76+
conversationPayload['context']['user'] = user
77+
78+
return conversationPayload;
79+
80+
'''
81+
initToneContext initializes a user object containing tone data (from the Watson Tone Analyzer)
82+
@returns user json object with the emotion, writing and social tones. The current
83+
tone identifies the tone for a specific conversation turn, and the history provides the conversation for
84+
all tones up to the current tone for a conversation instance with a user.
85+
'''
86+
def initUser():
87+
return {
88+
'user': {
89+
'tone': {
90+
'emotion': {
91+
'current': None
92+
},
93+
'writing': {
94+
'current': None
95+
},
96+
'social': {
97+
'current': None
98+
}
99+
}
100+
}
101+
}
102+
103+
104+
105+
106+
'''
107+
invokeToneAsync is an asynchronous function that calls the Tone Analyzer service
108+
@param conversationPayload json object returned by the Watson Conversation Service
109+
@param tone_analyzer an instance of the Watson Tone Analyzer service
110+
@returns the result of calling the tone_analyzer with the conversationPayload
111+
(which contains the user's input text)
112+
'''
113+
async def invokeToneAsync(conversationPayload, tone_analyzer):
114+
return tone_analyzer.tone(text=conversationPayload['input']['text'])
115+
116+
117+
'''
118+
updateEmotionTone updates the user emotion tone with the primary emotion - the emotion tone that has
119+
a score greater than or equal to the EMOTION_SCORE_THRESHOLD; otherwise primary emotion will be 'neutral'
120+
@param user a json object representing user information (tone) to be used in conversing with the Conversation Service
121+
@param emotionTone a json object containing the emotion tones in the payload returned by the Tone Analyzer
122+
'''
123+
def updateEmotionTone(user, emotionTone, maintainHistory):
124+
125+
maxScore = 0.0
126+
primaryEmotion = None
127+
primaryEmotionScore = None
128+
129+
for tone in emotionTone['tones']:
130+
if tone['score'] > maxScore:
131+
maxScore = tone['score']
132+
primaryEmotion = tone['tone_name'].lower()
133+
primaryEmotionScore = tone['score']
134+
135+
if maxScore <= PRIMARY_EMOTION_SCORE_THRESHOLD:
136+
primaryEmotion = 'neutral'
137+
primaryEmotionScore = None
138+
139+
# update user emotion tone
140+
user['tone']['emotion']['current'] = primaryEmotion;
141+
142+
if maintainHistory:
143+
if 'history' not in user['tone']['emotion']:
144+
user['tone']['emotion']['history'] = []
145+
user['tone']['emotion']['history'].append({
146+
'tone_name': primaryEmotion,
147+
'score': primaryEmotionScore
148+
})
149+
150+
'''
151+
updateWritingTone updates the user with the writing tones interpreted based on the specified thresholds
152+
@param: user a json object representing user information (tone) to be used in conversing with the Conversation Service
153+
@param: writingTone a json object containing the writing tones in the payload returned by the Tone Analyzer
154+
'''
155+
def updateWritingTone (user, writingTone, maintainHistory):
156+
157+
currentWriting = [];
158+
currentWritingObject = [];
159+
160+
# Process each writing tone and determine if it is high or low
161+
for tone in writingTone['tones']:
162+
if tone['score'] >= WRITING_HIGH_SCORE_THRESHOLD:
163+
currentWriting.append(tone['tone_name'].lower() + '_high')
164+
currentWritingObject.append({
165+
'tone_name': tone['tone_name'].lower(),
166+
'score': tone['score'],
167+
'interpretation': 'likely high'
168+
})
169+
elif tone['score'] <= WRITING_NO_SCORE_THRESHOLD:
170+
currentWritingObject.append({
171+
'tone_name': tone['tone_name'].lower(),
172+
'score': tone['score'],
173+
'interpretation': 'no evidence'
174+
})
175+
else:
176+
currentWritingObject.append({
177+
'tone_name': tone['tone_name'].lower(),
178+
'score': tone['score'],
179+
'interpretation': 'likely medium'
180+
})
181+
182+
# update user writing tone
183+
user['tone']['writing']['current'] = currentWriting
184+
if maintainHistory:
185+
if 'history' not in user['tone']['writing']:
186+
user['tone']['writing']['history'] = []
187+
user['tone']['writing']['history'].append(currentWritingObject) #TODO - is this the correct location??? AW
188+
189+
'''
190+
updateSocialTone updates the user with the social tones interpreted based on the specified thresholds
191+
@param user a json object representing user information (tone) to be used in conversing with the Conversation Service
192+
@param socialTone a json object containing the social tones in the payload returned by the Tone Analyzer
193+
'''
194+
def updateSocialTone (user, socialTone, maintainHistory):
195+
196+
currentSocial = []
197+
currentSocialObject = []
198+
199+
# Process each social tone and determine if it is high or low
200+
for tone in socialTone['tones']:
201+
if tone['score'] >= SOCIAL_HIGH_SCORE_THRESHOLD:
202+
currentSocial.append(tone['tone_name'].lower() + '_high')
203+
currentSocialObject.append({
204+
'tone_name': tone['tone_name'].lower(),
205+
'score': tone['score'],
206+
'interpretation': 'likely high'
207+
})
208+
elif tone['score'] <= SOCIAL_LOW_SCORE_THRESHOLD:
209+
currentSocial.append(tone['tone_name'].lower() + '_low');
210+
currentSocialObject.append({
211+
'tone_name': tone['tone_name'].lower(),
212+
'score': tone['score'],
213+
'interpretation': 'likely low'
214+
})
215+
else:
216+
currentSocialObject.append({
217+
'tone_name': tone['tone_name'].lower(),
218+
'score': tone['score'],
219+
'interpretation': 'likely medium'
220+
})
221+
222+
# update user social tone
223+
user['tone']['social']['current'] = currentSocial
224+
if maintainHistory:
225+
if not user['tone']['social']['current']:
226+
user['tone']['social']['current'] = [];
227+
user['tone']['social']['current'].append(currentSocialObject);
228+

0 commit comments

Comments
 (0)