primeira versão customizada de VAD- Voice Activity Detection
This commit is contained in:
343
tests/test_deep_agent.py
Normal file
343
tests/test_deep_agent.py
Normal file
@@ -0,0 +1,343 @@
|
||||
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
from voice_assistant.agent.adapter import AgentAdapter
|
||||
|
||||
|
||||
def test_deep_agent_implements_port():
|
||||
from voice_assistant.agent.deep_agent import DeepAgentsAdapter
|
||||
|
||||
assert issubclass(DeepAgentsAdapter, AgentAdapter)
|
||||
assert callable(getattr(DeepAgentsAdapter, "stream", None))
|
||||
assert callable(getattr(DeepAgentsAdapter, "aclose", None))
|
||||
|
||||
|
||||
def test_deep_agent_session_config():
|
||||
from voice_assistant.agent.deep_agent import DeepAgentsAdapter
|
||||
|
||||
adapter = object.__new__(DeepAgentsAdapter)
|
||||
|
||||
assert adapter._config("session-a") == {
|
||||
"configurable": {
|
||||
"thread_id": "session-a",
|
||||
}
|
||||
}
|
||||
|
||||
assert adapter._config("session-b") == {
|
||||
"configurable": {
|
||||
"thread_id": "session-b",
|
||||
}
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
adapter._config("")
|
||||
|
||||
|
||||
|
||||
def test_deep_agent_cleanup_is_idempotent():
|
||||
import asyncio
|
||||
|
||||
from voice_assistant.agent.deep_agent import DeepAgentsAdapter
|
||||
|
||||
adapter = object.__new__(DeepAgentsAdapter)
|
||||
adapter.model = MagicMock()
|
||||
adapter.model.aclose = AsyncMock()
|
||||
adapter._closed = False
|
||||
|
||||
async def scenario():
|
||||
await adapter.aclose()
|
||||
await adapter.aclose()
|
||||
|
||||
asyncio.run(scenario())
|
||||
|
||||
adapter.model.aclose.assert_awaited_once()
|
||||
assert adapter._closed is True
|
||||
|
||||
|
||||
|
||||
def test_deep_agent_stream_can_be_cancelled():
|
||||
import asyncio
|
||||
|
||||
from voice_assistant.agent.deep_agent import DeepAgentsAdapter
|
||||
|
||||
async def scenario():
|
||||
adapter = object.__new__(DeepAgentsAdapter)
|
||||
adapter._closed = False
|
||||
|
||||
started = asyncio.Event()
|
||||
cancelled = asyncio.Event()
|
||||
|
||||
class FakeAgent:
|
||||
async def astream(self, *args, **kwargs):
|
||||
started.set()
|
||||
|
||||
try:
|
||||
yield (
|
||||
FakeMessage("Primeiro fragmento"),
|
||||
{"langgraph_node": "model"},
|
||||
)
|
||||
|
||||
await asyncio.Event().wait()
|
||||
|
||||
except asyncio.CancelledError:
|
||||
cancelled.set()
|
||||
raise
|
||||
|
||||
class FakeMessage:
|
||||
def __init__(self, content):
|
||||
self.content = content
|
||||
self.tool_call_chunks = []
|
||||
|
||||
adapter.agent = FakeAgent()
|
||||
|
||||
async def consume():
|
||||
async for _ in adapter.stream(
|
||||
session_id="session-cancel",
|
||||
turn_id=1,
|
||||
text="Teste de cancelamento",
|
||||
):
|
||||
pass
|
||||
|
||||
task = asyncio.create_task(consume())
|
||||
|
||||
await asyncio.wait_for(started.wait(), timeout=2.0)
|
||||
|
||||
task.cancel()
|
||||
|
||||
try:
|
||||
await task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
assert cancelled.is_set()
|
||||
assert task.done()
|
||||
|
||||
asyncio.run(scenario())
|
||||
|
||||
|
||||
def test_deep_agent_isolates_session_history():
|
||||
import asyncio
|
||||
|
||||
from deepagents import create_deep_agent
|
||||
from langchain_core.messages import HumanMessage
|
||||
from langchain_core.language_models import FakeListChatModel
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
|
||||
async def scenario():
|
||||
checkpointer = MemorySaver()
|
||||
|
||||
class ToolCompatibleFakeChatModel(FakeListChatModel):
|
||||
def bind_tools(self, tools, *, tool_choice=None, **kwargs):
|
||||
return self
|
||||
|
||||
|
||||
model = ToolCompatibleFakeChatModel(
|
||||
responses=[
|
||||
"Resposta A1",
|
||||
"Resposta B1",
|
||||
"Resposta A2",
|
||||
]
|
||||
)
|
||||
|
||||
agent = create_deep_agent(
|
||||
model=model,
|
||||
tools=[],
|
||||
system_prompt="Responda de forma breve.",
|
||||
checkpointer=checkpointer,
|
||||
)
|
||||
|
||||
async def ask(session_id, text):
|
||||
return await agent.ainvoke(
|
||||
{
|
||||
"messages": [
|
||||
HumanMessage(content=text)
|
||||
]
|
||||
},
|
||||
config={
|
||||
"configurable": {
|
||||
"thread_id": session_id,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
await ask("session-a", "Destino: Japão")
|
||||
await ask("session-b", "Destino: Canadá")
|
||||
await ask("session-a", "Qual era meu destino?")
|
||||
|
||||
def history(session_id):
|
||||
snapshot = agent.get_state({
|
||||
"configurable": {
|
||||
"thread_id": session_id,
|
||||
}
|
||||
})
|
||||
|
||||
return snapshot.values.get("messages", [])
|
||||
|
||||
history_a = history("session-a")
|
||||
history_b = history("session-b")
|
||||
|
||||
texts_a = [
|
||||
message.content
|
||||
for message in history_a
|
||||
if isinstance(message, HumanMessage)
|
||||
]
|
||||
|
||||
texts_b = [
|
||||
message.content
|
||||
for message in history_b
|
||||
if isinstance(message, HumanMessage)
|
||||
]
|
||||
|
||||
assert texts_a == [
|
||||
"Destino: Japão",
|
||||
"Qual era meu destino?",
|
||||
]
|
||||
|
||||
assert texts_b == [
|
||||
"Destino: Canadá",
|
||||
]
|
||||
|
||||
assert all(
|
||||
"Canadá" not in text
|
||||
for text in texts_a
|
||||
)
|
||||
|
||||
assert all(
|
||||
"Japão" not in text
|
||||
for text in texts_b
|
||||
)
|
||||
|
||||
asyncio.run(scenario())
|
||||
|
||||
|
||||
def test_deep_agent_checkpoint_survives_cancellation():
|
||||
import asyncio
|
||||
|
||||
from deepagents import create_deep_agent
|
||||
from langchain_core.language_models import FakeListChatModel
|
||||
from langchain_core.messages import HumanMessage
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
|
||||
class CancellableFakeModel(FakeListChatModel):
|
||||
def bind_tools(self, tools, *, tool_choice=None, **kwargs):
|
||||
return self
|
||||
|
||||
async def _agenerate(
|
||||
self,
|
||||
messages,
|
||||
stop=None,
|
||||
run_manager=None,
|
||||
**kwargs,
|
||||
):
|
||||
last_user_message = next(
|
||||
(
|
||||
message
|
||||
for message in reversed(messages)
|
||||
if isinstance(message, HumanMessage)
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
if (
|
||||
last_user_message is not None
|
||||
and last_user_message.content == "Interromper esta resposta"
|
||||
):
|
||||
started.set()
|
||||
await asyncio.Event().wait()
|
||||
|
||||
return await super()._agenerate(
|
||||
messages,
|
||||
stop=stop,
|
||||
run_manager=run_manager,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
async def scenario():
|
||||
nonlocal started
|
||||
|
||||
started = asyncio.Event()
|
||||
|
||||
model = CancellableFakeModel(
|
||||
responses=[
|
||||
"Contexto inicial confirmado.",
|
||||
"Contexto preservado.",
|
||||
]
|
||||
)
|
||||
|
||||
agent = create_deep_agent(
|
||||
model=model,
|
||||
tools=[],
|
||||
system_prompt="Responda brevemente.",
|
||||
checkpointer=MemorySaver(),
|
||||
)
|
||||
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "session-cancellation-test",
|
||||
}
|
||||
}
|
||||
|
||||
async def ask(text):
|
||||
return await agent.ainvoke(
|
||||
{
|
||||
"messages": [
|
||||
HumanMessage(content=text)
|
||||
]
|
||||
},
|
||||
config=config,
|
||||
)
|
||||
|
||||
# First turn completes successfully.
|
||||
await ask("Meu destino é Japão")
|
||||
|
||||
# Second turn begins but never completes.
|
||||
task = asyncio.create_task(
|
||||
ask("Interromper esta resposta")
|
||||
)
|
||||
|
||||
await asyncio.wait_for(
|
||||
started.wait(),
|
||||
timeout=2.0,
|
||||
)
|
||||
|
||||
task.cancel()
|
||||
|
||||
try:
|
||||
await task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
assert task.done()
|
||||
assert task.cancelled()
|
||||
|
||||
# The same thread must remain usable.
|
||||
await asyncio.wait_for(
|
||||
ask("Qual era meu destino?"),
|
||||
timeout=5.0,
|
||||
)
|
||||
|
||||
snapshot = await agent.aget_state(config)
|
||||
|
||||
messages = snapshot.values.get("messages", [])
|
||||
|
||||
human_messages = [
|
||||
message.content
|
||||
for message in messages
|
||||
if isinstance(message, HumanMessage)
|
||||
]
|
||||
|
||||
assert "Meu destino é Japão" in human_messages
|
||||
assert "Qual era meu destino?" in human_messages
|
||||
|
||||
# An interrupted execution must not be recorded
|
||||
# as a completed assistant response.
|
||||
assert not any(
|
||||
getattr(message, "content", None)
|
||||
== "Resposta interrompida concluída."
|
||||
for message in messages
|
||||
)
|
||||
|
||||
started = None
|
||||
asyncio.run(scenario())
|
||||
|
||||
Reference in New Issue
Block a user