chore: refactor status processor (#465)

* chore: refactor status processor

* fix: fix billing duration when billsec is None for Cloudonix
This commit is contained in:
Abhishek 2026-06-24 22:07:35 +05:30 committed by GitHub
parent d817d50056
commit 29c5be298c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 910 additions and 809 deletions

View file

@ -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