mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-25 08:48:13 +02:00
chore: refactor status processor (#465)
* chore: refactor status processor * fix: fix billing duration when billsec is None for Cloudonix
This commit is contained in:
parent
d817d50056
commit
29c5be298c
29 changed files with 910 additions and 809 deletions
|
|
@ -268,28 +268,23 @@ class TestEndCallViaNodeTransition:
|
|||
new_callable=AsyncMock,
|
||||
return_value=1,
|
||||
):
|
||||
with patch(
|
||||
"api.services.workflow.pipecat_engine.apply_disposition_mapping",
|
||||
with patch.object(
|
||||
VariableExtractionManager,
|
||||
"_perform_extraction",
|
||||
new_callable=AsyncMock,
|
||||
return_value="completed",
|
||||
return_value={"user_intent": "end call"},
|
||||
):
|
||||
with patch.object(
|
||||
VariableExtractionManager,
|
||||
"_perform_extraction",
|
||||
new_callable=AsyncMock,
|
||||
return_value={"user_intent": "end call"},
|
||||
):
|
||||
|
||||
async def run_pipeline():
|
||||
await run_pipeline_worker(task)
|
||||
async def run_pipeline():
|
||||
await run_pipeline_worker(task)
|
||||
|
||||
async def initialize_engine():
|
||||
await asyncio.sleep(0.01)
|
||||
await engine.initialize()
|
||||
await engine.set_node(engine.workflow.start_node_id)
|
||||
await engine.llm.queue_frame(LLMContextFrame(engine.context))
|
||||
async def initialize_engine():
|
||||
await asyncio.sleep(0.01)
|
||||
await engine.initialize()
|
||||
await engine.set_node(engine.workflow.start_node_id)
|
||||
await engine.llm.queue_frame(LLMContextFrame(engine.context))
|
||||
|
||||
await asyncio.gather(run_pipeline(), initialize_engine())
|
||||
await asyncio.gather(run_pipeline(), initialize_engine())
|
||||
|
||||
# Verify end_call_with_reason was called
|
||||
assert len(test_helper.end_call_reasons) >= 1, (
|
||||
|
|
@ -371,28 +366,23 @@ class TestEndCallViaNodeTransition:
|
|||
new_callable=AsyncMock,
|
||||
return_value=1,
|
||||
):
|
||||
with patch(
|
||||
"api.services.workflow.pipecat_engine.apply_disposition_mapping",
|
||||
with patch.object(
|
||||
VariableExtractionManager,
|
||||
"_perform_extraction",
|
||||
new_callable=AsyncMock,
|
||||
return_value="completed",
|
||||
return_value={"greeting_type": "formal", "user_name": "John"},
|
||||
):
|
||||
with patch.object(
|
||||
VariableExtractionManager,
|
||||
"_perform_extraction",
|
||||
new_callable=AsyncMock,
|
||||
return_value={"greeting_type": "formal", "user_name": "John"},
|
||||
):
|
||||
|
||||
async def run_pipeline():
|
||||
await run_pipeline_worker(task)
|
||||
async def run_pipeline():
|
||||
await run_pipeline_worker(task)
|
||||
|
||||
async def initialize_engine():
|
||||
await asyncio.sleep(0.01)
|
||||
await engine.initialize()
|
||||
await engine.set_node(engine.workflow.start_node_id)
|
||||
await engine.llm.queue_frame(LLMContextFrame(engine.context))
|
||||
async def initialize_engine():
|
||||
await asyncio.sleep(0.01)
|
||||
await engine.initialize()
|
||||
await engine.set_node(engine.workflow.start_node_id)
|
||||
await engine.llm.queue_frame(LLMContextFrame(engine.context))
|
||||
|
||||
await asyncio.gather(run_pipeline(), initialize_engine())
|
||||
await asyncio.gather(run_pipeline(), initialize_engine())
|
||||
|
||||
# Should have 3 LLM generations
|
||||
assert llm.get_current_step() == 3
|
||||
|
|
@ -469,28 +459,23 @@ class TestEndCallViaCustomTool:
|
|||
new_callable=AsyncMock,
|
||||
return_value=1,
|
||||
):
|
||||
with patch(
|
||||
"api.services.workflow.pipecat_engine.apply_disposition_mapping",
|
||||
with patch.object(
|
||||
VariableExtractionManager,
|
||||
"_perform_extraction",
|
||||
new_callable=AsyncMock,
|
||||
return_value="end_call_tool",
|
||||
return_value={"user_intent": "end"},
|
||||
):
|
||||
with patch.object(
|
||||
VariableExtractionManager,
|
||||
"_perform_extraction",
|
||||
new_callable=AsyncMock,
|
||||
return_value={"user_intent": "end"},
|
||||
):
|
||||
|
||||
async def run_pipeline():
|
||||
await run_pipeline_worker(task)
|
||||
async def run_pipeline():
|
||||
await run_pipeline_worker(task)
|
||||
|
||||
async def initialize_engine():
|
||||
await asyncio.sleep(0.01)
|
||||
await engine.initialize()
|
||||
await engine.set_node(engine.workflow.start_node_id)
|
||||
await engine.llm.queue_frame(LLMContextFrame(engine.context))
|
||||
async def initialize_engine():
|
||||
await asyncio.sleep(0.01)
|
||||
await engine.initialize()
|
||||
await engine.set_node(engine.workflow.start_node_id)
|
||||
await engine.llm.queue_frame(LLMContextFrame(engine.context))
|
||||
|
||||
await asyncio.gather(run_pipeline(), initialize_engine())
|
||||
await asyncio.gather(run_pipeline(), initialize_engine())
|
||||
|
||||
# Verify end_call_with_reason was called with END_CALL_TOOL_REASON
|
||||
assert len(test_helper.end_call_reasons) >= 1, (
|
||||
|
|
@ -560,28 +545,23 @@ class TestEndCallViaCustomTool:
|
|||
new_callable=AsyncMock,
|
||||
return_value=1,
|
||||
):
|
||||
with patch(
|
||||
"api.services.workflow.pipecat_engine.apply_disposition_mapping",
|
||||
with patch.object(
|
||||
VariableExtractionManager,
|
||||
"_perform_extraction",
|
||||
new_callable=AsyncMock,
|
||||
return_value="end_call_tool",
|
||||
return_value={"user_intent": "end"},
|
||||
):
|
||||
with patch.object(
|
||||
VariableExtractionManager,
|
||||
"_perform_extraction",
|
||||
new_callable=AsyncMock,
|
||||
return_value={"user_intent": "end"},
|
||||
):
|
||||
|
||||
async def run_pipeline():
|
||||
await run_pipeline_worker(task)
|
||||
async def run_pipeline():
|
||||
await run_pipeline_worker(task)
|
||||
|
||||
async def initialize_engine():
|
||||
await asyncio.sleep(0.01)
|
||||
await engine.initialize()
|
||||
await engine.set_node(engine.workflow.start_node_id)
|
||||
await engine.llm.queue_frame(LLMContextFrame(engine.context))
|
||||
async def initialize_engine():
|
||||
await asyncio.sleep(0.01)
|
||||
await engine.initialize()
|
||||
await engine.set_node(engine.workflow.start_node_id)
|
||||
await engine.llm.queue_frame(LLMContextFrame(engine.context))
|
||||
|
||||
await asyncio.gather(run_pipeline(), initialize_engine())
|
||||
await asyncio.gather(run_pipeline(), initialize_engine())
|
||||
|
||||
# Verify end_call_with_reason was called
|
||||
assert len(test_helper.end_call_reasons) >= 1, (
|
||||
|
|
@ -637,37 +617,32 @@ class TestEndCallViaClientDisconnect:
|
|||
new_callable=AsyncMock,
|
||||
return_value=1,
|
||||
):
|
||||
with patch(
|
||||
"api.services.workflow.pipecat_engine.apply_disposition_mapping",
|
||||
with patch.object(
|
||||
VariableExtractionManager,
|
||||
"_perform_extraction",
|
||||
new_callable=AsyncMock,
|
||||
return_value="user_hangup",
|
||||
return_value={"user_intent": "disconnected"},
|
||||
):
|
||||
with patch.object(
|
||||
VariableExtractionManager,
|
||||
"_perform_extraction",
|
||||
new_callable=AsyncMock,
|
||||
return_value={"user_intent": "disconnected"},
|
||||
):
|
||||
|
||||
async def run_pipeline():
|
||||
await run_pipeline_worker(task)
|
||||
async def run_pipeline():
|
||||
await run_pipeline_worker(task)
|
||||
|
||||
async def initialize_and_disconnect():
|
||||
await asyncio.sleep(0.01)
|
||||
await engine.initialize()
|
||||
await engine.set_node(engine.workflow.start_node_id)
|
||||
await engine.llm.queue_frame(LLMContextFrame(engine.context))
|
||||
async def initialize_and_disconnect():
|
||||
await asyncio.sleep(0.01)
|
||||
await engine.initialize()
|
||||
await engine.set_node(engine.workflow.start_node_id)
|
||||
await engine.llm.queue_frame(LLMContextFrame(engine.context))
|
||||
|
||||
# Wait for initial generation to complete
|
||||
await asyncio.sleep(0.1)
|
||||
# Wait for initial generation to complete
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
# Simulate client disconnect by calling end_call_with_reason directly
|
||||
# This is what on_client_disconnected does
|
||||
await engine.end_call_with_reason(
|
||||
EndTaskReason.USER_HANGUP.value, abort_immediately=True
|
||||
)
|
||||
# Simulate client disconnect by calling end_call_with_reason directly
|
||||
# This is what on_client_disconnected does
|
||||
await engine.end_call_with_reason(
|
||||
EndTaskReason.USER_HANGUP.value, abort_immediately=True
|
||||
)
|
||||
|
||||
await asyncio.gather(run_pipeline(), initialize_and_disconnect())
|
||||
await asyncio.gather(run_pipeline(), initialize_and_disconnect())
|
||||
|
||||
# Verify end_call_with_reason was called with USER_HANGUP
|
||||
assert EndTaskReason.USER_HANGUP.value in test_helper.end_call_reasons, (
|
||||
|
|
@ -727,46 +702,41 @@ class TestEndCallRaceConditions:
|
|||
new_callable=AsyncMock,
|
||||
return_value=1,
|
||||
):
|
||||
with patch(
|
||||
"api.services.workflow.pipecat_engine.apply_disposition_mapping",
|
||||
with patch.object(
|
||||
VariableExtractionManager,
|
||||
"_perform_extraction",
|
||||
new_callable=AsyncMock,
|
||||
return_value="first_reason",
|
||||
return_value={"user_intent": "end"},
|
||||
):
|
||||
with patch.object(
|
||||
VariableExtractionManager,
|
||||
"_perform_extraction",
|
||||
new_callable=AsyncMock,
|
||||
return_value={"user_intent": "end"},
|
||||
):
|
||||
|
||||
async def run_pipeline():
|
||||
await run_pipeline_worker(task)
|
||||
async def run_pipeline():
|
||||
await run_pipeline_worker(task)
|
||||
|
||||
async def initialize_and_race():
|
||||
await asyncio.sleep(0.01)
|
||||
await engine.initialize()
|
||||
await engine.set_node(engine.workflow.start_node_id)
|
||||
await engine.llm.queue_frame(LLMContextFrame(engine.context))
|
||||
async def initialize_and_race():
|
||||
await asyncio.sleep(0.01)
|
||||
await engine.initialize()
|
||||
await engine.set_node(engine.workflow.start_node_id)
|
||||
await engine.llm.queue_frame(LLMContextFrame(engine.context))
|
||||
|
||||
# Wait for initial generation
|
||||
await asyncio.sleep(0.1)
|
||||
# Wait for initial generation
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
# Try to end call multiple times concurrently
|
||||
await asyncio.gather(
|
||||
engine.end_call_with_reason(
|
||||
EndTaskReason.USER_HANGUP.value, abort_immediately=True
|
||||
),
|
||||
engine.end_call_with_reason(
|
||||
EndTaskReason.END_CALL_TOOL_REASON.value,
|
||||
abort_immediately=True,
|
||||
),
|
||||
engine.end_call_with_reason(
|
||||
EndTaskReason.USER_QUALIFIED.value,
|
||||
abort_immediately=False,
|
||||
),
|
||||
)
|
||||
# Try to end call multiple times concurrently
|
||||
await asyncio.gather(
|
||||
engine.end_call_with_reason(
|
||||
EndTaskReason.USER_HANGUP.value, abort_immediately=True
|
||||
),
|
||||
engine.end_call_with_reason(
|
||||
EndTaskReason.END_CALL_TOOL_REASON.value,
|
||||
abort_immediately=True,
|
||||
),
|
||||
engine.end_call_with_reason(
|
||||
EndTaskReason.USER_QUALIFIED.value,
|
||||
abort_immediately=False,
|
||||
),
|
||||
)
|
||||
|
||||
await asyncio.gather(run_pipeline(), initialize_and_race())
|
||||
await asyncio.gather(run_pipeline(), initialize_and_race())
|
||||
|
||||
# Due to the _call_disposed guard, only one end_call should fully execute
|
||||
# The tracked end_call_reasons will show all attempted calls
|
||||
|
|
@ -838,41 +808,34 @@ class TestEndCallRaceConditions:
|
|||
new_callable=AsyncMock,
|
||||
return_value=1,
|
||||
):
|
||||
with patch(
|
||||
"api.services.workflow.pipecat_engine.apply_disposition_mapping",
|
||||
with patch.object(
|
||||
VariableExtractionManager,
|
||||
"_perform_extraction",
|
||||
new_callable=AsyncMock,
|
||||
return_value="end_reason",
|
||||
return_value={"user_intent": "end"},
|
||||
):
|
||||
with patch.object(
|
||||
VariableExtractionManager,
|
||||
"_perform_extraction",
|
||||
new_callable=AsyncMock,
|
||||
return_value={"user_intent": "end"},
|
||||
):
|
||||
|
||||
async def run_pipeline():
|
||||
await run_pipeline_worker(task)
|
||||
async def run_pipeline():
|
||||
await run_pipeline_worker(task)
|
||||
|
||||
async def initialize_and_race_disconnect():
|
||||
nonlocal disconnect_called
|
||||
await asyncio.sleep(0.01)
|
||||
await engine.initialize()
|
||||
await engine.set_node(engine.workflow.start_node_id)
|
||||
await engine.llm.queue_frame(LLMContextFrame(engine.context))
|
||||
async def initialize_and_race_disconnect():
|
||||
nonlocal disconnect_called
|
||||
await asyncio.sleep(0.01)
|
||||
await engine.initialize()
|
||||
await engine.set_node(engine.workflow.start_node_id)
|
||||
await engine.llm.queue_frame(LLMContextFrame(engine.context))
|
||||
|
||||
# Wait for the end_call tool to be called
|
||||
await asyncio.sleep(0.15)
|
||||
# Wait for the end_call tool to be called
|
||||
await asyncio.sleep(0.15)
|
||||
|
||||
# Simulate client disconnect racing with end_call tool
|
||||
disconnect_called = True
|
||||
await engine.end_call_with_reason(
|
||||
EndTaskReason.USER_HANGUP.value, abort_immediately=True
|
||||
)
|
||||
|
||||
await asyncio.gather(
|
||||
run_pipeline(), initialize_and_race_disconnect()
|
||||
# Simulate client disconnect racing with end_call tool
|
||||
disconnect_called = True
|
||||
await engine.end_call_with_reason(
|
||||
EndTaskReason.USER_HANGUP.value, abort_immediately=True
|
||||
)
|
||||
|
||||
await asyncio.gather(run_pipeline(), initialize_and_race_disconnect())
|
||||
|
||||
# Verify disconnect was attempted
|
||||
assert disconnect_called, "Disconnect should have been called"
|
||||
|
||||
|
|
@ -933,40 +896,35 @@ class TestEndCallExtractionBehavior:
|
|||
new_callable=AsyncMock,
|
||||
return_value=1,
|
||||
):
|
||||
with patch(
|
||||
"api.services.workflow.pipecat_engine.apply_disposition_mapping",
|
||||
new_callable=AsyncMock,
|
||||
return_value="completed",
|
||||
with patch.object(
|
||||
VariableExtractionManager,
|
||||
"_perform_extraction",
|
||||
side_effect=mock_extraction,
|
||||
):
|
||||
with patch.object(
|
||||
VariableExtractionManager,
|
||||
"_perform_extraction",
|
||||
side_effect=mock_extraction,
|
||||
):
|
||||
|
||||
async def run_pipeline():
|
||||
await run_pipeline_worker(task)
|
||||
async def run_pipeline():
|
||||
await run_pipeline_worker(task)
|
||||
|
||||
async def initialize_and_end():
|
||||
await asyncio.sleep(0.01)
|
||||
await engine.initialize()
|
||||
await engine.set_node(engine.workflow.start_node_id)
|
||||
await engine.llm.queue_frame(LLMContextFrame(engine.context))
|
||||
async def initialize_and_end():
|
||||
await asyncio.sleep(0.01)
|
||||
await engine.initialize()
|
||||
await engine.set_node(engine.workflow.start_node_id)
|
||||
await engine.llm.queue_frame(LLMContextFrame(engine.context))
|
||||
|
||||
# Wait for initial generation
|
||||
await asyncio.sleep(0.1)
|
||||
# Wait for initial generation
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
# End the call
|
||||
await engine.end_call_with_reason(
|
||||
EndTaskReason.USER_HANGUP.value, abort_immediately=True
|
||||
)
|
||||
# End the call
|
||||
await engine.end_call_with_reason(
|
||||
EndTaskReason.USER_HANGUP.value, abort_immediately=True
|
||||
)
|
||||
|
||||
# Verify extraction was awaited (synchronous)
|
||||
assert extraction_completed.is_set(), (
|
||||
"Extraction should have completed before end_call returned"
|
||||
)
|
||||
# Verify extraction was awaited (synchronous)
|
||||
assert extraction_completed.is_set(), (
|
||||
"Extraction should have completed before end_call returned"
|
||||
)
|
||||
|
||||
await asyncio.gather(run_pipeline(), initialize_and_end())
|
||||
await asyncio.gather(run_pipeline(), initialize_and_end())
|
||||
|
||||
# Verify synchronous extraction was used
|
||||
sync_extractions = [
|
||||
|
|
@ -1058,35 +1016,30 @@ class TestEndCallExtractionBehavior:
|
|||
new_callable=AsyncMock,
|
||||
return_value=1,
|
||||
):
|
||||
with patch(
|
||||
"api.services.workflow.pipecat_engine.apply_disposition_mapping",
|
||||
new_callable=AsyncMock,
|
||||
return_value="completed",
|
||||
with patch.object(
|
||||
VariableExtractionManager,
|
||||
"_perform_extraction",
|
||||
extraction_mock,
|
||||
):
|
||||
with patch.object(
|
||||
VariableExtractionManager,
|
||||
"_perform_extraction",
|
||||
extraction_mock,
|
||||
):
|
||||
|
||||
async def run_pipeline():
|
||||
await run_pipeline_worker(task)
|
||||
async def run_pipeline():
|
||||
await run_pipeline_worker(task)
|
||||
|
||||
async def initialize_and_end():
|
||||
await asyncio.sleep(0.01)
|
||||
await engine.initialize()
|
||||
await engine.set_node(engine.workflow.start_node_id)
|
||||
await engine.llm.queue_frame(LLMContextFrame(engine.context))
|
||||
async def initialize_and_end():
|
||||
await asyncio.sleep(0.01)
|
||||
await engine.initialize()
|
||||
await engine.set_node(engine.workflow.start_node_id)
|
||||
await engine.llm.queue_frame(LLMContextFrame(engine.context))
|
||||
|
||||
# Wait for initial generation
|
||||
await asyncio.sleep(0.1)
|
||||
# Wait for initial generation
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
# End the call
|
||||
await engine.end_call_with_reason(
|
||||
EndTaskReason.USER_HANGUP.value, abort_immediately=True
|
||||
)
|
||||
# End the call
|
||||
await engine.end_call_with_reason(
|
||||
EndTaskReason.USER_HANGUP.value, abort_immediately=True
|
||||
)
|
||||
|
||||
await asyncio.gather(run_pipeline(), initialize_and_end())
|
||||
await asyncio.gather(run_pipeline(), initialize_and_end())
|
||||
|
||||
# Extraction should have been called but the inner _perform_extraction
|
||||
# should not have been called because extraction_enabled=False
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue