refactor: simplifica o adapter DeepAgents

This commit is contained in:
alex alves
2026-11-05 16:22:23 -03:00
parent fbbe47d90d
commit 85fc4c4d46
3 changed files with 56 additions and 52 deletions

View File

@@ -1,4 +1,3 @@
""" """
Deep Agents implementation of the AgentVoice Agent port. Deep Agents implementation of the AgentVoice Agent port.
@@ -24,6 +23,7 @@ from contextlib import aclosing
from deepagents import create_deep_agent from deepagents import create_deep_agent
from langchain_core.messages import HumanMessage from langchain_core.messages import HumanMessage
from langchain_core.runnables import RunnableConfig
from langchain_oci import ChatOCIGenAI from langchain_oci import ChatOCIGenAI
from langgraph.checkpoint.memory import MemorySaver from langgraph.checkpoint.memory import MemorySaver
@@ -95,12 +95,11 @@ class DeepAgentsAdapter(StreamingAgentAdapter):
self._stream_event_timeout = float(timeout_seconds) self._stream_event_timeout = float(timeout_seconds)
self.model = ChatOCIGenAI( self._model = ChatOCIGenAI(
model_id=model_id, model_id=model_id,
compartment_id=compartment_id, compartment_id=compartment_id,
service_endpoint=( service_endpoint=(
f"https://inference.generativeai." f"https://inference.generativeai.{region}.oci.oraclecloud.com"
f"{region}.oci.oraclecloud.com"
), ),
auth_type=auth_type, auth_type=auth_type,
auth_profile=auth_profile, auth_profile=auth_profile,
@@ -112,18 +111,19 @@ class DeepAgentsAdapter(StreamingAgentAdapter):
}, },
) )
self.checkpointer = MemorySaver() self._checkpointer = MemorySaver()
self.agent = create_deep_agent( self._graph = create_deep_agent(
model=self.model, model=self._model,
tools=[], tools=[],
system_prompt=system_prompt, system_prompt=system_prompt,
checkpointer=self.checkpointer, checkpointer=self._checkpointer,
) )
self._closed = False self._closed = False
def _config(self, session_id: str) -> dict: @staticmethod
def _session_config(session_id: str) -> RunnableConfig:
if not session_id: if not session_id:
raise ValueError("session_id is required.") raise ValueError("session_id is required.")
@@ -133,34 +133,6 @@ class DeepAgentsAdapter(StreamingAgentAdapter):
} }
} }
async def process(
self,
session_id: str,
turn_id: int,
text: str,
) -> str:
parts: list[str] = []
async with aclosing(
self.stream(
session_id=session_id,
turn_id=turn_id,
text=text,
)
) as chunks:
async for chunk in chunks:
parts.append(chunk)
response = "".join(parts).strip()
if not response:
raise RuntimeError(
"Deep Agent returned an empty response."
)
return response
async def stream( async def stream(
self, self,
session_id: str, session_id: str,
@@ -173,9 +145,9 @@ class DeepAgentsAdapter(StreamingAgentAdapter):
if not text.strip(): if not text.strip():
raise ValueError("Agent input text cannot be empty.") raise ValueError("Agent input text cannot be empty.")
config = self._config(session_id) config = self._session_config(session_id)
events = self.agent.astream( events = self._graph.astream(
{"messages": [HumanMessage(content=text)]}, {"messages": [HumanMessage(content=text)]},
config=config, config=config,
stream_mode="messages", stream_mode="messages",
@@ -200,10 +172,9 @@ class DeepAgentsAdapter(StreamingAgentAdapter):
for fragment in _text_fragments(message.content): for fragment in _text_fragments(message.content):
yield fragment yield fragment
async def aclose(self) -> None: async def aclose(self) -> None:
if self._closed: if self._closed:
return return
await self.model.aclose() await self._model.aclose()
self._closed = True self._closed = True

View File

@@ -18,20 +18,20 @@ def test_deep_agent_session_config():
adapter = object.__new__(DeepAgentsAdapter) adapter = object.__new__(DeepAgentsAdapter)
assert adapter._config("session-a") == { assert adapter._session_config("session-a") == {
"configurable": { "configurable": {
"thread_id": "session-a", "thread_id": "session-a",
} }
} }
assert adapter._config("session-b") == { assert adapter._session_config("session-b") == {
"configurable": { "configurable": {
"thread_id": "session-b", "thread_id": "session-b",
} }
} }
with pytest.raises(ValueError): with pytest.raises(ValueError):
adapter._config("") adapter._session_config("")
@@ -41,8 +41,8 @@ def test_deep_agent_cleanup_is_idempotent():
from voice_assistant.agent.deep_agent import DeepAgentsAdapter from voice_assistant.agent.deep_agent import DeepAgentsAdapter
adapter = object.__new__(DeepAgentsAdapter) adapter = object.__new__(DeepAgentsAdapter)
adapter.model = MagicMock() adapter._model = MagicMock()
adapter.model.aclose = AsyncMock() adapter._model.aclose = AsyncMock()
adapter._closed = False adapter._closed = False
async def scenario(): async def scenario():
@@ -51,7 +51,7 @@ def test_deep_agent_cleanup_is_idempotent():
asyncio.run(scenario()) asyncio.run(scenario())
adapter.model.aclose.assert_awaited_once() adapter._model.aclose.assert_awaited_once()
assert adapter._closed is True assert adapter._closed is True
@@ -90,7 +90,7 @@ def test_deep_agent_stream_can_be_cancelled():
self.content = content self.content = content
self.tool_call_chunks = [] self.tool_call_chunks = []
adapter.agent = FakeAgent() adapter._graph = FakeAgent()
async def consume(): async def consume():
async for _ in adapter.stream( async for _ in adapter.stream(
@@ -389,6 +389,6 @@ def test_deep_agent_factory_configures_authentication(monkeypatch, options, expe
model_factory.assert_called_once() model_factory.assert_called_once()
kwargs = model_factory.call_args.kwargs kwargs = model_factory.call_args.kwargs
assert (kwargs["auth_type"], kwargs["auth_profile"], kwargs["auth_file_location"]) == expected_auth assert (kwargs["auth_type"], kwargs["auth_profile"], kwargs["auth_file_location"]) == expected_auth
assert adapter.model is model_factory.return_value assert adapter._model is model_factory.return_value
assert agent_factory.call_args.kwargs["model"] is adapter.model assert agent_factory.call_args.kwargs["model"] is adapter._model

View File

@@ -11,7 +11,7 @@ def make_adapter(events, timeout=0.05):
adapter = object.__new__(DeepAgentsAdapter) adapter = object.__new__(DeepAgentsAdapter)
adapter._closed = False adapter._closed = False
adapter._stream_event_timeout = timeout adapter._stream_event_timeout = timeout
adapter.agent = SimpleNamespace(astream=lambda *args, **kwargs: events) adapter._graph = SimpleNamespace(astream=lambda *args, **kwargs: events)
return adapter return adapter
@@ -138,9 +138,42 @@ def test_adapter_streams_real_deepagents_graph_with_fake_model():
adapter = object.__new__(DeepAgentsAdapter) adapter = object.__new__(DeepAgentsAdapter)
adapter._closed = False adapter._closed = False
adapter._stream_event_timeout = 2.0 adapter._stream_event_timeout = 2.0
adapter.agent = create_deep_agent( adapter._graph = create_deep_agent(
model=Model(responses=["Olá Lara."]), tools=[], checkpointer=MemorySaver(), model=Model(responses=["Olá Lara."]), tools=[], checkpointer=MemorySaver(),
) )
response = await adapter.process("session", 1, "Diga olá") response = await adapter.process("session", 1, "Diga olá")
assert response == "Olá Lara." assert response == "Olá Lara."
asyncio.run(scenario()) asyncio.run(scenario())
def test_process_rejects_empty_text_response_and_closes_stream():
async def scenario():
closed = asyncio.Event()
async def source():
try:
yield event([{"type": "reasoning", "text": "Interno"}])
finally:
closed.set()
with pytest.raises(RuntimeError, match="empty streaming response"):
await make_adapter(source()).process("session", 1, "Olá")
assert closed.is_set()
asyncio.run(scenario())
def test_process_propagates_provider_failure_and_closes_stream():
async def scenario():
closed = asyncio.Event()
async def source():
try:
yield event("Texto parcial")
raise ValueError("provider failure")
finally:
closed.set()
with pytest.raises(ValueError, match="provider failure"):
await make_adapter(source()).process("session", 1, "Olá")
assert closed.is_set()
asyncio.run(scenario())