refactor(reasoning)!: remove Node-RED engine and integrations
BREAKING CHANGE: NODE_RED reasoning is no longer supported. Unsupported mode values fall back to PROXY. PROXY and OPENAI remain available.
This commit is contained in:
@@ -11,7 +11,7 @@ import json
|
||||
import os
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from unittest.mock import patch
|
||||
|
||||
import httpx
|
||||
from fastapi.testclient import TestClient
|
||||
@@ -21,7 +21,7 @@ with patch.dict(os.environ, {}, clear=True), patch("dotenv.load_dotenv", return_
|
||||
from s2s_client.core import config as settings
|
||||
from s2s_client.providers import apipilot
|
||||
from s2s_client.realtime import proxy
|
||||
from s2s_client.services import context, reasoning
|
||||
from s2s_client.services import context
|
||||
|
||||
ASYNC_CLIENT = httpx.AsyncClient
|
||||
FIXTURE = Path(__file__).parent / "fixtures" / "legacy-contracts.json"
|
||||
@@ -47,7 +47,6 @@ BASE_CONFIG = {
|
||||
"S2S_APIPILOT_SERVICE_TOKEN": "",
|
||||
"S2S_APIPILOT_API_KEY": "",
|
||||
"OPENAI_API_KEY": "",
|
||||
"NODE_RED_REASONING_URL": "",
|
||||
"S2S_REPORTS_QUICK_HINT_MAX": 5,
|
||||
"S2S_REPORTS_INJECT_MODE": "minimal",
|
||||
}
|
||||
@@ -268,15 +267,14 @@ def cases():
|
||||
|
||||
for name, payload in (("missing", {}), ("proxy", {"transcript": "hello"}), ("proxy-blank", {"transcript": " "})):
|
||||
yield f"reasoning-{name}", "POST", "/api/reasoning", {"json": payload}, {}, []
|
||||
for mode in ("OPENAI", "NODE_RED"):
|
||||
yield (
|
||||
f"reasoning-{mode}-legacy-unbound-token",
|
||||
"POST",
|
||||
"/api/reasoning",
|
||||
{"json": {"transcript": "hello"}},
|
||||
{"S2S_REASONING_TYPE": mode},
|
||||
[],
|
||||
)
|
||||
yield (
|
||||
"reasoning-OPENAI-legacy-unbound-token",
|
||||
"POST",
|
||||
"/api/reasoning",
|
||||
{"json": {"transcript": "hello"}},
|
||||
{"S2S_REASONING_TYPE": "OPENAI"},
|
||||
[],
|
||||
)
|
||||
yield (
|
||||
"reasoning-legacy-unbound-profile",
|
||||
"POST",
|
||||
@@ -292,8 +290,7 @@ def cases():
|
||||
reasoning_config = {
|
||||
**profile_config,
|
||||
"S2S_INJECT_PROFILE": True,
|
||||
"S2S_REASONING_TYPE": "NODE_RED",
|
||||
"NODE_RED_REASONING_URL": "http://reasoning.test",
|
||||
"S2S_REASONING_TYPE": "OPENAI",
|
||||
}
|
||||
reasoning_request = {
|
||||
"json": {
|
||||
@@ -304,28 +301,6 @@ def cases():
|
||||
"future": True,
|
||||
}
|
||||
}
|
||||
for name, payload in (
|
||||
(
|
||||
"answer",
|
||||
{
|
||||
"answer": " answer ",
|
||||
"latency_ms": "loose",
|
||||
"intent": {"future": 1},
|
||||
"usage": {"future": True},
|
||||
"future": True,
|
||||
},
|
||||
),
|
||||
("log-only", {"answer": "", "log_only": True, "debug": {"future": True}}),
|
||||
("invalid-answer", {"answer": 5}),
|
||||
):
|
||||
yield (
|
||||
f"reasoning-node-{name}",
|
||||
"POST",
|
||||
"/api/reasoning",
|
||||
reasoning_request,
|
||||
reasoning_config,
|
||||
[(200, {"name": "Caller"}), (200, payload)],
|
||||
)
|
||||
yield (
|
||||
"reasoning-openai-answer",
|
||||
"POST",
|
||||
@@ -333,7 +308,6 @@ def cases():
|
||||
reasoning_request,
|
||||
{
|
||||
**reasoning_config,
|
||||
"S2S_REASONING_TYPE": "OPENAI",
|
||||
"OPENAI_API_KEY": "fixture-key",
|
||||
},
|
||||
[
|
||||
@@ -454,7 +428,6 @@ def capture_http():
|
||||
"x-demo-session",
|
||||
"x-api-key",
|
||||
"content-type",
|
||||
"x-s2s-caller",
|
||||
)
|
||||
if key in request.headers
|
||||
},
|
||||
@@ -534,8 +507,7 @@ async def capture_realtime():
|
||||
yield json.dumps({"type": "error", "error": {"message": "Unknown or invalid event: session.update"}})
|
||||
yield json.dumps({"type": "future.after.suppressed.error"})
|
||||
|
||||
with patch.object(reasoning, "_forward_agent_text_to_nodered", new_callable=AsyncMock):
|
||||
await proxy._upstream_to_client(Socket(), upstream(), "fixture", {})
|
||||
await proxy._upstream_to_client(Socket(), upstream(), "fixture", {})
|
||||
return frames
|
||||
|
||||
|
||||
@@ -548,6 +520,48 @@ def capture():
|
||||
|
||||
|
||||
class LegacyCompatibilityTests(unittest.TestCase):
|
||||
def test_native_transcripts_are_relayed_without_external_http_calls(self):
|
||||
events = [
|
||||
{"type": "conversation.item.input_audio_transcription.completed", "transcript": "Hola", "item_id": "u1"},
|
||||
{"type": "response.audio_transcript.done", "transcript": "Respuesta", "response_id": "r1"},
|
||||
{"type": "response.output_audio_transcript.done", "transcript": "Respuesta", "response_id": "r1"},
|
||||
{"type": "response.content_part.done", "part": {"type": "text", "text": "Respuesta"}},
|
||||
*[
|
||||
{
|
||||
"type": event_type,
|
||||
"item": {"id": "a1", "role": "assistant", "content": [{"type": "text", "text": "Respuesta"}]},
|
||||
}
|
||||
for event_type in ("response.output_item.done", "conversation.item.created")
|
||||
],
|
||||
{"type": "response.text.done", "text": "Respuesta", "response_id": "r1"},
|
||||
]
|
||||
messages = [json.dumps(event) for event in events]
|
||||
forwarded = []
|
||||
|
||||
class Socket:
|
||||
async def send_text(self, message):
|
||||
forwarded.append(message)
|
||||
|
||||
async def upstream():
|
||||
for message in messages:
|
||||
yield message
|
||||
|
||||
async def relay():
|
||||
await proxy._upstream_to_client(Socket(), upstream(), "fixture", {})
|
||||
# Allow any accidentally scheduled background HTTP work to run.
|
||||
await asyncio.sleep(0)
|
||||
|
||||
for mode in ("PROXY", "OPENAI"):
|
||||
forwarded.clear()
|
||||
with (
|
||||
self.subTest(mode=mode),
|
||||
patch.multiple(settings, **{**BASE_CONFIG, "S2S_REASONING_TYPE": mode}),
|
||||
patch.object(httpx, "AsyncClient", side_effect=AssertionError("Unexpected HTTP call")) as http,
|
||||
):
|
||||
asyncio.run(relay())
|
||||
self.assertEqual(forwarded, messages)
|
||||
http.assert_not_called()
|
||||
|
||||
def test_wire_contracts_match_pre_migration_capture(self):
|
||||
expected = json.loads(FIXTURE.read_text())
|
||||
actual = capture()
|
||||
|
||||
Reference in New Issue
Block a user