refactor: simplifica o adapter DeepAgents
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
Reference in New Issue
Block a user