Skip to content

Commit 131d8fa

Browse files
committed
fix: Create a new claim
breaking: Event URL is now changed to the phone number
1 parent f50f0dd commit 131d8fa

File tree

1 file changed

+47
-33
lines changed

1 file changed

+47
-33
lines changed

main.py

+47-33
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import openai_multi_tool_use_parallel_patch
33

44
# General imports
5+
from typing import List, Optional, Tuple
56
from azure.communication.callautomation import (
67
CallAutomationClient,
78
CallConnectionClient,
@@ -33,6 +34,7 @@
3334
from models.synthesis import SynthesisModel
3435
from persistence.cosmos import CosmosStore
3536
from persistence.sqlite import SqliteStore
37+
import html
3638
import re
3739
import asyncio
3840
from models.message import (
@@ -207,23 +209,23 @@ async def call_inbound_post(request: Request):
207209

208210

209211
@api.post(
210-
"/call/event/{call_id}",
212+
"/call/event/{phone_number}",
211213
description="Handle callbacks from Azure Communication Services.",
212214
status_code=status.HTTP_204_NO_CONTENT,
213215
)
214216
# TODO: Secure this endpoint with a secret
215217
# See: https://github.com/MicrosoftDocs/azure-docs/blob/main/articles/communication-services/how-tos/call-automation/secure-webhook-endpoint.md
216218
async def call_event_post(
217-
request: Request, background_tasks: BackgroundTasks, call_id: UUID
219+
request: Request, background_tasks: BackgroundTasks, phone_number: str
218220
) -> None:
219221
for event_dict in await request.json():
220-
background_tasks.add_task(communication_evnt_worker, event_dict, call_id)
222+
background_tasks.add_task(communication_event_worker, event_dict, phone_number)
221223

222224

223-
async def communication_evnt_worker(event_dict: dict, call_id: UUID) -> None:
224-
call = await db.call_aget(call_id)
225+
async def communication_event_worker(event_dict: dict, phone_number: str) -> None:
226+
call = await db.call_asearch_one(phone_number)
225227
if not call:
226-
_logger.warn(f"Call {call_id} not found")
228+
_logger.warn(f"Call with {phone_number} not found")
227229
return
228230

229231
event = CloudEvent.from_dict(event_dict)
@@ -249,7 +251,7 @@ async def communication_evnt_worker(event_dict: dict, call_id: UUID) -> None:
249251
)
250252
)
251253

252-
if not call.messages: # First call
254+
if len(call.messages) == 1: # First call
253255
await handle_recognize_text(
254256
call=call,
255257
client=client,
@@ -262,7 +264,7 @@ async def communication_evnt_worker(event_dict: dict, call_id: UUID) -> None:
262264
client=client,
263265
text=TTSPrompt.WELCOME_BACK,
264266
)
265-
await intelligence(call, client)
267+
call = await intelligence(call, client)
266268

267269
elif event_type == "Microsoft.Communication.CallDisconnected": # Call hung up
268270
_logger.info(f"Call disconnected ({call.call_id})")
@@ -279,7 +281,7 @@ async def communication_evnt_worker(event_dict: dict, call_id: UUID) -> None:
279281
call.messages.append(
280282
MessageModel(content=speech_text, persona=MessagePersona.HUMAN)
281283
)
282-
await intelligence(call, client)
284+
call = await intelligence(call, client)
283285

284286
elif (
285287
event_type == "Microsoft.Communication.RecognizeFailed"
@@ -366,7 +368,7 @@ async def communication_evnt_worker(event_dict: dict, call_id: UUID) -> None:
366368
await db.call_aset(call)
367369

368370

369-
async def intelligence(call: CallModel, client: CallConnectionClient) -> None:
371+
async def intelligence(call: CallModel, client: CallConnectionClient) -> CallModel:
370372
"""
371373
Handle the intelligence of the call, including: GPT chat, GPT completion, TTS, and media play.
372374
@@ -380,7 +382,7 @@ async def intelligence(call: CallModel, client: CallConnectionClient) -> None:
380382
hard_timeout_task = asyncio.create_task(
381383
asyncio.sleep(CONFIG.workflow.intelligence_hard_timeout_sec)
382384
)
383-
chat_res = None
385+
chat_action = None
384386

385387
try:
386388
while True:
@@ -397,7 +399,7 @@ async def intelligence(call: CallModel, client: CallConnectionClient) -> None:
397399
soft_timeout_task.cancel()
398400
hard_timeout_task.cancel()
399401
# Answer with chat result
400-
chat_res = chat_task.result()
402+
call, chat_action = chat_task.result()
401403
break
402404
# Break when hard timeout is reached
403405
if hard_timeout_task.done():
@@ -425,55 +427,57 @@ async def intelligence(call: CallModel, client: CallConnectionClient) -> None:
425427
_logger.warn(f"Error loading intelligence ({call.call_id})", exc_info=True)
426428

427429
# For any error reason, answer with error
428-
if not chat_res:
430+
if not chat_action:
429431
_logger.debug(
430432
f"Error loading intelligence ({call.call_id}), answering with default error"
431433
)
432-
chat_res = ActionModel(content=TTSPrompt.ERROR, intent=IndentAction.CONTINUE)
434+
chat_action = ActionModel(content=TTSPrompt.ERROR, intent=IndentAction.CONTINUE)
433435

434-
_logger.info(f"Chat ({call.call_id}): {chat_res}")
436+
_logger.info(f"Chat ({call.call_id}): {chat_action}")
435437

436-
if chat_res.intent == IndentAction.TALK_TO_HUMAN:
438+
if chat_action.intent == IndentAction.TALK_TO_HUMAN:
437439
await handle_play(
438440
call=call,
439441
client=client,
440442
context=Context.CONNECT_AGENT,
441443
text=TTSPrompt.END_CALL_TO_CONNECT_AGENT,
442444
)
443445

444-
elif chat_res.intent == IndentAction.END_CALL:
446+
elif chat_action.intent == IndentAction.END_CALL:
445447
await handle_play(
446448
call=call,
447449
client=client,
448450
context=Context.GOODBYE,
449451
text=TTSPrompt.GOODBYE,
450452
)
451453

452-
elif chat_res.intent in (
454+
elif chat_action.intent in (
453455
IndentAction.NEW_CLAIM,
454456
IndentAction.UPDATED_CLAIM,
455457
IndentAction.NEW_OR_UPDATED_REMINDER,
456458
):
457-
# Save in DB allowing demos to be more "real-time"
459+
# Save in DB for new claims and allowing demos to be more "real-time"
458460
await db.call_aset(call)
459461
# Answer with intermediate response
460462
await handle_play(
461463
call=call,
462464
client=client,
463465
store=False,
464-
text=chat_res.content,
466+
text=chat_action.content,
465467
)
466468
# Recursively call intelligence to continue the conversation
467-
await intelligence(call, client)
469+
call = await intelligence(call, client)
468470

469471
else:
470472
await handle_recognize_text(
471473
call=call,
472474
client=client,
473475
store=False,
474-
text=chat_res.content,
476+
text=chat_action.content,
475477
)
476478

479+
return call
480+
477481

478482
async def handle_play(
479483
client: CallConnectionClient,
@@ -555,7 +559,7 @@ async def gpt_completion(system: LLMPrompt, call: CallModel) -> str:
555559
return content or ""
556560

557561

558-
async def gpt_chat(call: CallModel) -> ActionModel:
562+
async def gpt_chat(call: CallModel) -> Tuple[CallModel, ActionModel]:
559563
_logger.debug(f"Running GPT chat ({call.call_id})")
560564

561565
messages = [
@@ -647,7 +651,7 @@ async def gpt_chat(call: CallModel) -> ActionModel:
647651
{
648652
"type": "function",
649653
"function": {
650-
"description": "Use this if the user wants to create a new claim. This will reset the claim and reminder data. Old is stored but not accessible anymore. Approval from the customer must be explicitely given. Example: 'I want to create a new claim'.",
654+
"description": "Use this if the user wants to create a new claim. This will reset the claim and reminder data. Old is stored but not accessible anymore. Approval from the customer must be explicitely given. Do not use this action twice in a row. Example: 'I want to create a new claim'.",
651655
"name": IndentAction.NEW_CLAIM.value,
652656
"parameters": {
653657
"properties": {
@@ -747,7 +751,9 @@ async def gpt_chat(call: CallModel) -> ActionModel:
747751

748752
content = res.choices[0].message.content or ""
749753
content = re.sub(
750-
rf"^(?:{'|'.join([action.value for action in MessageAction])}):", "", content
754+
rf"^(?:{'|'.join([action.value for action in MessageAction])}):",
755+
"",
756+
content,
751757
).strip() # Remove action from content, AI often adds it by mistake event if explicitly asked not to
752758
tool_calls = res.choices[0].message.tool_calls
753759

@@ -806,9 +812,11 @@ async def gpt_chat(call: CallModel) -> ActionModel:
806812
else:
807813
content += parameters[customer_response_prop] + " "
808814

809-
call.claim = ClaimModel()
810-
call.reminders = []
811-
model.content = "Claim and reminders created reset."
815+
# Add context of the last message, if not, LLM messed up and loop on this action
816+
last_message = call.messages[-1]
817+
call = CallModel(phone_number=call.phone_number)
818+
call.messages.append(last_message)
819+
model.content = "Claim, reminders and messages reset."
812820

813821
elif name == IndentAction.NEW_OR_UPDATED_REMINDER.value:
814822
intent = IndentAction.NEW_OR_UPDATED_REMINDER
@@ -854,15 +862,21 @@ async def gpt_chat(call: CallModel) -> ActionModel:
854862
)
855863
)
856864

857-
return ActionModel(
858-
content=content,
859-
intent=intent,
865+
return (
866+
call,
867+
ActionModel(
868+
content=content,
869+
intent=intent,
870+
),
860871
)
861872

862873
except Exception:
863874
_logger.warn(f"OpenAI API call error", exc_info=True)
864875

865-
return ActionModel(content=TTSPrompt.ERROR, intent=IndentAction.CONTINUE)
876+
return (
877+
call,
878+
ActionModel(content=TTSPrompt.ERROR, intent=IndentAction.CONTINUE),
879+
)
866880

867881

868882
async def handle_recognize_text(
@@ -1015,7 +1029,7 @@ async def callback_url(caller_id: str) -> str:
10151029
if not call:
10161030
call = CallModel(phone_number=caller_id)
10171031
await db.call_aset(call)
1018-
return f"{CALL_EVENT_URL}/{call.call_id}"
1032+
return f"{CALL_EVENT_URL}/{html.escape(call.phone_number)}"
10191033

10201034

10211035
async def post_call_synthesis(call: CallModel) -> None:

0 commit comments

Comments
 (0)