diff --git a/agentrun/integration/builtin/sandbox.py b/agentrun/integration/builtin/sandbox.py index 4ac0467..a61b357 100644 --- a/agentrun/integration/builtin/sandbox.py +++ b/agentrun/integration/builtin/sandbox.py @@ -2,7 +2,7 @@ import base64 import threading -from typing import Any, Callable, Dict, Optional +from typing import Any, Callable, Dict, Optional, TYPE_CHECKING from agentrun.integration.utils.tool import CommonToolSet, tool from agentrun.sandbox import Sandbox, TemplateType @@ -12,6 +12,18 @@ from agentrun.utils.config import Config from agentrun.utils.log import logger +if TYPE_CHECKING: + from agentrun.sandbox.api.playwright_sync import BrowserPlaywrightSync + +try: + from playwright.sync_api import Error as PlaywrightError +except ImportError: + + class PlaywrightError(Exception): # type: ignore[no-redef] + """Fallback Playwright error used when Playwright is not installed.""" + + pass + class SandboxToolSet(CommonToolSet): """沙箱工具集基类 @@ -47,6 +59,8 @@ def close(self): self.sandbox.stop() except Exception as e: logger.debug("delete sandbox failed, due to %s", e) + self.sandbox = None + self.sandbox_id = "" def _ensure_sandbox(self): """确保沙箱实例存在,如果不存在则创建""" @@ -689,6 +703,126 @@ def __init__( sandbox_idle_timeout_seconds=sandbox_idle_timeout_seconds, config=config, ) + self._playwright_sync: Optional["BrowserPlaywrightSync"] = None + + def _get_playwright(self, sb: BrowserSandbox) -> "BrowserPlaywrightSync": + """获取或创建 Playwright 连接 / Get or create Playwright connection + + 复用已有连接以减少连接建立开销和瞬态错误。 + 使用双重检查锁定避免并发调用时创建多个连接导致资源泄漏。 + Reuses existing connection to reduce connection overhead and transient errors. + Uses double-checked locking to avoid leaking connections under concurrent calls. + """ + if self._playwright_sync is not None: + return self._playwright_sync + + with self.lock: + if self._playwright_sync is None: + playwright_sync = sb.sync_playwright() + playwright_sync.open() + self._playwright_sync = playwright_sync + return self._playwright_sync + + def _reset_playwright(self) -> None: + """重置 Playwright 连接 / Reset Playwright connection + + 在沙箱重建时调用,清理旧的连接。 + Called when sandbox is recreated, cleans up old connection. + """ + with self.lock: + if self._playwright_sync is not None: + try: + self._playwright_sync.close() + except Exception as e: + logger.debug( + "Error while closing Playwright connection: %s", + e, + exc_info=True, + ) + self._playwright_sync = None + + def _run_in_sandbox(self, callback: Callable[[Sandbox], Any]) -> Any: + """在沙箱中执行操作,智能区分错误类型 / Execute in sandbox with smart error handling + + 与基类不同,此方法区分两类错误: + - 基础设施错误(连接断开、沙箱崩溃):重建沙箱并重试 + - 工具级错误(JS 执行失败、元素找不到):直接返回错误,不重建 + + Unlike the base class, this method distinguishes two types of errors: + - Infrastructure errors (connection lost, sandbox crashed): recreate sandbox and retry + - Tool-level errors (JS execution failed, element not found): return error without recreating + """ + sb = self._ensure_sandbox() + try: + return callback(sb) + except (ConnectionError, OSError, BrokenPipeError) as e: + logger.debug( + "Browser sandbox infrastructure error: %s, recreating sandbox", + e, + ) + self._reset_playwright() + self.sandbox = None + try: + sb = self._ensure_sandbox() + return callback(sb) + except Exception as e2: + logger.debug("Recreated sandbox run failed: %s", e2) + return {"error": f"{e!s}"} + except PlaywrightError as e: + error_msg = str(e) + if self._is_infrastructure_error(error_msg): + logger.debug( + "Browser sandbox CDP connection error: %s, recreating" + " sandbox", + e, + ) + self._reset_playwright() + self.sandbox = None + try: + sb = self._ensure_sandbox() + return callback(sb) + except Exception as e2: + logger.debug("Recreated sandbox run failed: %s", e2) + return {"error": f"{e!s}"} + else: + logger.debug( + "Browser tool-level error (no sandbox rebuild): %s", e + ) + return {"error": f"{e!s}"} + except Exception as e: + logger.debug("Unexpected error in browser sandbox: %s", e) + return {"error": f"{e!s}"} + + def _is_infrastructure_error(self, error_msg: str) -> bool: + """判断是否为基础设施错误 / Check if error is infrastructure-level + + 基础设施错误表示沙箱或 CDP 连接出现问题,需要重建。 + 工具级错误(如 JS 执行失败、元素找不到)不需要重建。 + + Infrastructure errors indicate sandbox or CDP connection issues, requiring rebuild. + Tool-level errors (e.g., JS execution failed, element not found) don't need rebuild. + """ + infrastructure_patterns = [ + "Target closed", + "Connection closed", + "Browser closed", + "Protocol error", + "WebSocket", + "ECONNREFUSED", + "ECONNRESET", + "EPIPE", + "Target page, context or browser has been closed", + ] + error_lower = error_msg.lower() + return any( + pattern.lower() in error_lower + for pattern in infrastructure_patterns + ) + + def close(self) -> None: + """关闭并释放沙箱和 Playwright 资源 / Close and release sandbox and Playwright resources""" + self._reset_playwright() + super().close() # ==================== 健康检查 / Health Check ==================== @@ -731,13 +865,13 @@ def browser_navigate( def inner(sb: Sandbox): assert isinstance(sb, BrowserSandbox) - with sb.sync_playwright() as p: - response = p.goto(url, wait_until=wait_until, timeout=timeout) - return { - "url": url, - "success": True, - "status": response.status if response else None, - } + p = self._get_playwright(sb) + response = p.goto(url, wait_until=wait_until, timeout=timeout) + return { + "url": url, + "success": True, + "status": response.status if response else None, + } return self._run_in_sandbox(inner) @@ -770,12 +904,12 @@ def browser_navigate_back( def inner(sb: Sandbox): assert isinstance(sb, BrowserSandbox) - with sb.sync_playwright() as p: - response = p.go_back(wait_until=wait_until, timeout=timeout) - return { - "success": True, - "status": response.status if response else None, - } + p = self._get_playwright(sb) + response = p.go_back(wait_until=wait_until, timeout=timeout) + return { + "success": True, + "status": response.status if response else None, + } return self._run_in_sandbox(inner) @@ -796,12 +930,12 @@ def browser_go_forward( def inner(sb: Sandbox): assert isinstance(sb, BrowserSandbox) - with sb.sync_playwright() as p: - response = p.go_forward(wait_until=wait_until, timeout=timeout) - return { - "success": True, - "status": response.status if response else None, - } + p = self._get_playwright(sb) + response = p.go_forward(wait_until=wait_until, timeout=timeout) + return { + "success": True, + "status": response.status if response else None, + } return self._run_in_sandbox(inner) @@ -827,14 +961,14 @@ def browser_click( def inner(sb: Sandbox): assert isinstance(sb, BrowserSandbox) - with sb.sync_playwright() as p: - p.click( - selector, - button=button, - click_count=click_count, - timeout=timeout, - ) - return {"selector": selector, "success": True} + p = self._get_playwright(sb) + p.click( + selector, + button=button, + click_count=click_count, + timeout=timeout, + ) + return {"selector": selector, "success": True} return self._run_in_sandbox(inner) @@ -867,9 +1001,9 @@ def browser_dblclick( def inner(sb: Sandbox): assert isinstance(sb, BrowserSandbox) - with sb.sync_playwright() as p: - p.dblclick(selector, timeout=timeout) - return {"selector": selector, "success": True} + p = self._get_playwright(sb) + p.dblclick(selector, timeout=timeout) + return {"selector": selector, "success": True} return self._run_in_sandbox(inner) @@ -890,15 +1024,13 @@ def browser_drag( def inner(sb: Sandbox): assert isinstance(sb, BrowserSandbox) - with sb.sync_playwright() as p: - p.drag_and_drop( - source_selector, target_selector, timeout=timeout - ) - return { - "source": source_selector, - "target": target_selector, - "success": True, - } + p = self._get_playwright(sb) + p.drag_and_drop(source_selector, target_selector, timeout=timeout) + return { + "source": source_selector, + "target": target_selector, + "success": True, + } return self._run_in_sandbox(inner) @@ -918,9 +1050,9 @@ def browser_hover( def inner(sb: Sandbox): assert isinstance(sb, BrowserSandbox) - with sb.sync_playwright() as p: - p.hover(selector, timeout=timeout) - return {"selector": selector, "success": True} + p = self._get_playwright(sb) + p.hover(selector, timeout=timeout) + return {"selector": selector, "success": True} return self._run_in_sandbox(inner) @@ -947,9 +1079,9 @@ def browser_type( def inner(sb: Sandbox): assert isinstance(sb, BrowserSandbox) - with sb.sync_playwright() as p: - p.type(selector, text, delay=delay, timeout=timeout) - return {"selector": selector, "text": text, "success": True} + p = self._get_playwright(sb) + p.type(selector, text, delay=delay, timeout=timeout) + return {"selector": selector, "text": text, "success": True} return self._run_in_sandbox(inner) @@ -972,9 +1104,9 @@ def browser_fill( def inner(sb: Sandbox): assert isinstance(sb, BrowserSandbox) - with sb.sync_playwright() as p: - p.fill(selector, value, timeout=timeout) - return {"selector": selector, "value": value, "success": True} + p = self._get_playwright(sb) + p.fill(selector, value, timeout=timeout) + return {"selector": selector, "value": value, "success": True} return self._run_in_sandbox(inner) @@ -1005,10 +1137,10 @@ def browser_snapshot(self) -> Dict[str, Any]: def inner(sb: Sandbox): assert isinstance(sb, BrowserSandbox) - with sb.sync_playwright() as p: - html = p.html_content() - title = p.title() - return {"html": html, "title": title} + p = self._get_playwright(sb) + html = p.html_content() + title = p.title() + return {"html": html, "title": title} return self._run_in_sandbox(inner) @@ -1042,16 +1174,16 @@ def browser_take_screenshot( def inner(sb: Sandbox): assert isinstance(sb, BrowserSandbox) - with sb.sync_playwright() as p: - screenshot_bytes = p.screenshot(full_page=full_page, type=type) - screenshot_base64 = base64.b64encode(screenshot_bytes).decode( - "utf-8" - ) - return { - "screenshot": screenshot_base64, - "format": type, - "full_page": full_page, - } + p = self._get_playwright(sb) + screenshot_bytes = p.screenshot(full_page=full_page, type=type) + screenshot_base64 = base64.b64encode(screenshot_bytes).decode( + "utf-8" + ) + return { + "screenshot": screenshot_base64, + "format": type, + "full_page": full_page, + } return self._run_in_sandbox(inner) @@ -1067,9 +1199,9 @@ def browser_get_title(self) -> Dict[str, Any]: def inner(sb: Sandbox): assert isinstance(sb, BrowserSandbox) - with sb.sync_playwright() as p: - title = p.title() - return {"title": title} + p = self._get_playwright(sb) + title = p.title() + return {"title": title} return self._run_in_sandbox(inner) @@ -1088,16 +1220,16 @@ def browser_tabs_list(self) -> Dict[str, Any]: def inner(sb: Sandbox): assert isinstance(sb, BrowserSandbox) - with sb.sync_playwright() as p: - pages = p.list_pages() - tabs = [] - for i, page in enumerate(pages): - tabs.append({ - "index": i, - "url": page.url, - "title": page.title(), - }) - return {"tabs": tabs, "count": len(tabs)} + p = self._get_playwright(sb) + pages = p.list_pages() + tabs = [] + for i, page in enumerate(pages): + tabs.append({ + "index": i, + "url": page.url, + "title": page.title(), + }) + return {"tabs": tabs, "count": len(tabs)} return self._run_in_sandbox(inner) @@ -1114,15 +1246,15 @@ def browser_tabs_new(self, url: Optional[str] = None) -> Dict[str, Any]: def inner(sb: Sandbox): assert isinstance(sb, BrowserSandbox) - with sb.sync_playwright() as p: - page = p.new_page() - if url: - page.goto(url) - return { - "success": True, - "url": page.url, - "title": page.title(), - } + p = self._get_playwright(sb) + page = p.new_page() + if url: + page.goto(url) + return { + "success": True, + "url": page.url, + "title": page.title(), + } return self._run_in_sandbox(inner) @@ -1139,14 +1271,14 @@ def browser_tabs_select(self, index: int) -> Dict[str, Any]: def inner(sb: Sandbox): assert isinstance(sb, BrowserSandbox) - with sb.sync_playwright() as p: - page = p.select_tab(index) - return { - "success": True, - "index": index, - "url": page.url, - "title": page.title(), - } + p = self._get_playwright(sb) + page = p.select_tab(index) + return { + "success": True, + "index": index, + "url": page.url, + "title": page.title(), + } return self._run_in_sandbox(inner) @@ -1171,9 +1303,9 @@ def browser_evaluate( def inner(sb: Sandbox): assert isinstance(sb, BrowserSandbox) - with sb.sync_playwright() as p: - result = p.evaluate(expression, arg=arg) - return {"result": result} + p = self._get_playwright(sb) + result = p.evaluate(expression, arg=arg) + return {"result": result} return self._run_in_sandbox(inner) @@ -1204,9 +1336,9 @@ def browser_wait_for(self, timeout: float) -> Dict[str, Any]: def inner(sb: Sandbox): assert isinstance(sb, BrowserSandbox) - with sb.sync_playwright() as p: - p.wait(timeout) - return {"success": True, "waited_ms": timeout} + p = self._get_playwright(sb) + p.wait(timeout) + return {"success": True, "waited_ms": timeout} return self._run_in_sandbox(inner) diff --git a/agentrun/integration/utils/tool.py b/agentrun/integration/utils/tool.py index 5b0e874..bde72a5 100644 --- a/agentrun/integration/utils/tool.py +++ b/agentrun/integration/utils/tool.py @@ -1562,8 +1562,8 @@ def _build_openapi_schema( if isinstance(schema, dict): properties[name] = { **schema, - "description": ( - param.get("description") or schema.get("description", "") + "description": param.get("description") or schema.get( + "description", "" ), } if param.get("required"): diff --git a/agentrun/utils/exception.py b/agentrun/utils/exception.py index c98d01d..dcdbf1b 100644 --- a/agentrun/utils/exception.py +++ b/agentrun/utils/exception.py @@ -149,3 +149,25 @@ def __init__( msg += f" Reason: {message}" super().__init__(msg) + + +class BrowserToolError(AgentRunError): + """浏览器工具级错误 / Browser tool-level error + + 此类异常表示浏览器操作失败(如 JS 执行错误、元素找不到等), + 但不代表沙箱基础设施故障,不应触发沙箱重建。 + + This exception indicates browser operation failures (e.g., JS execution errors, + element not found), but does not indicate sandbox infrastructure failure + and should not trigger sandbox recreation. + """ + + def __init__( + self, + message: str, + operation: Optional[str] = None, + ): + self.operation = operation + if operation: + message = f"{operation}: {message}" + super().__init__(message) diff --git a/tests/unittests/integration/test_browser_toolset_error_handling.py b/tests/unittests/integration/test_browser_toolset_error_handling.py new file mode 100644 index 0000000..fb1ea71 --- /dev/null +++ b/tests/unittests/integration/test_browser_toolset_error_handling.py @@ -0,0 +1,309 @@ +"""BrowserToolSet 错误处理单元测试 / BrowserToolSet Error Handling Unit Tests + +测试 BrowserToolSet 的错误处理机制,确保工具级错误不会触发沙箱重建。 +Tests BrowserToolSet error handling to ensure tool-level errors don't trigger sandbox recreation. +""" + +import threading +from unittest.mock import MagicMock, patch + +import pytest + +from agentrun.integration.builtin.sandbox import BrowserToolSet + + +class TestBrowserToolSetIsInfrastructureError: + """测试 _is_infrastructure_error 方法""" + + @pytest.fixture + def toolset(self): + """创建 BrowserToolSet 实例(不初始化沙箱)""" + with patch.object(BrowserToolSet, "__init__", lambda self: None): + ts = BrowserToolSet() + ts._playwright_sync = None + ts.sandbox = None + ts.sandbox_id = "" + return ts + + def test_connection_closed_is_infrastructure_error(self, toolset): + """测试连接关闭是基础设施错误""" + assert toolset._is_infrastructure_error("Target closed") is True + assert toolset._is_infrastructure_error("Connection closed") is True + assert toolset._is_infrastructure_error("Browser closed") is True + + def test_protocol_error_is_infrastructure_error(self, toolset): + """测试协议错误是基础设施错误""" + assert ( + toolset._is_infrastructure_error("Protocol error: session closed") + is True + ) + assert ( + toolset._is_infrastructure_error("WebSocket disconnected") is True + ) + + def test_network_error_is_infrastructure_error(self, toolset): + """测试网络错误是基础设施错误""" + assert toolset._is_infrastructure_error("ECONNREFUSED") is True + assert toolset._is_infrastructure_error("ECONNRESET") is True + assert toolset._is_infrastructure_error("EPIPE") is True + + def test_js_error_is_not_infrastructure_error(self, toolset): + """测试 JS 执行错误不是基础设施错误""" + assert ( + toolset._is_infrastructure_error( + "Evaluation failed: TypeError: Cannot read property" + " 'textContent' of null" + ) + is False + ) + + def test_element_not_found_is_not_infrastructure_error(self, toolset): + """测试元素找不到错误不是基础设施错误""" + assert ( + toolset._is_infrastructure_error( + "Error: Timeout 30000ms exceeded while waiting for selector" + " '.nonexistent'" + ) + is False + ) + + def test_timeout_error_is_not_infrastructure_error(self, toolset): + """测试超时错误不是基础设施错误""" + assert ( + toolset._is_infrastructure_error( + "Error: page.click: Timeout 5000ms exceeded." + ) + is False + ) + + +class TestBrowserToolSetRunInSandbox: + """测试 _run_in_sandbox 方法的错误处理""" + + @pytest.fixture + def mock_sandbox(self): + """创建模拟的沙箱""" + return MagicMock() + + @pytest.fixture + def toolset(self, mock_sandbox): + """创建带有模拟沙箱的 BrowserToolSet 实例""" + with patch.object(BrowserToolSet, "__init__", lambda self: None): + ts = BrowserToolSet() + ts._playwright_sync = None + ts.sandbox = mock_sandbox + ts.sandbox_id = "test-sandbox-id" + ts.lock = MagicMock() + ts._reset_playwright = MagicMock() + ts._ensure_sandbox = MagicMock(return_value=mock_sandbox) + return ts + + def test_successful_callback_returns_result(self, toolset): + """测试成功的回调返回结果""" + + def callback(sb): + return {"success": True, "data": "test"} + + result = toolset._run_in_sandbox(callback) + + assert result == {"success": True, "data": "test"} + assert toolset.sandbox is not None + + def test_tool_level_error_returns_error_without_rebuild(self, toolset): + """测试工具级错误返回错误字典,不重建沙箱""" + try: + from playwright.sync_api import Error as PlaywrightError + except ImportError: + pytest.skip("Playwright not installed") + + original_sandbox = toolset.sandbox + + def callback(sb): + raise PlaywrightError( + "Evaluation failed: TypeError: Cannot read property" + ) + + result = toolset._run_in_sandbox(callback) + + assert "error" in result + assert "Evaluation failed" in result["error"] + assert toolset.sandbox is original_sandbox + toolset._reset_playwright.assert_not_called() + + def test_infrastructure_error_triggers_rebuild(self, toolset, mock_sandbox): + """测试基础设施错误触发沙箱重建""" + try: + from playwright.sync_api import Error as PlaywrightError + except ImportError: + pytest.skip("Playwright not installed") + + call_count = 0 + + def callback(sb): + nonlocal call_count + call_count += 1 + if call_count == 1: + raise PlaywrightError("Target closed") + return {"success": True} + + result = toolset._run_in_sandbox(callback) + + assert result == {"success": True} + assert call_count == 2 + toolset._reset_playwright.assert_called_once() + + def test_connection_error_triggers_rebuild(self, toolset, mock_sandbox): + """测试连接错误触发沙箱重建""" + call_count = 0 + + def callback(sb): + nonlocal call_count + call_count += 1 + if call_count == 1: + raise ConnectionError("Connection refused") + return {"success": True} + + result = toolset._run_in_sandbox(callback) + + assert result == {"success": True} + assert call_count == 2 + toolset._reset_playwright.assert_called_once() + + def test_os_error_triggers_rebuild(self, toolset, mock_sandbox): + """测试 OS 错误触发沙箱重建""" + call_count = 0 + + def callback(sb): + nonlocal call_count + call_count += 1 + if call_count == 1: + raise OSError("Broken pipe") + return {"success": True} + + result = toolset._run_in_sandbox(callback) + + assert result == {"success": True} + assert call_count == 2 + toolset._reset_playwright.assert_called_once() + + def test_unexpected_error_returns_error_without_rebuild(self, toolset): + """测试未知异常返回错误,不重建沙箱""" + original_sandbox = toolset.sandbox + + def callback(sb): + raise ValueError("Some unexpected error") + + result = toolset._run_in_sandbox(callback) + + assert "error" in result + assert "Some unexpected error" in result["error"] + assert toolset.sandbox is original_sandbox + toolset._reset_playwright.assert_not_called() + + +class TestBrowserToolSetPlaywrightCaching: + """测试 Playwright 连接缓存机制""" + + @pytest.fixture + def mock_sandbox(self): + """创建模拟的沙箱""" + sb = MagicMock() + mock_playwright = MagicMock() + sb.sync_playwright.return_value = mock_playwright + return sb + + @pytest.fixture + def toolset(self, mock_sandbox): + """创建带有模拟沙箱的 BrowserToolSet 实例""" + with patch.object(BrowserToolSet, "__init__", lambda self: None): + ts = BrowserToolSet() + ts._playwright_sync = None + ts.sandbox = mock_sandbox + ts.sandbox_id = "test-sandbox-id" + ts.lock = threading.Lock() + return ts + + def test_get_playwright_creates_connection_once( + self, toolset, mock_sandbox + ): + """测试 _get_playwright 只创建一次连接""" + p1 = toolset._get_playwright(mock_sandbox) + p2 = toolset._get_playwright(mock_sandbox) + + assert p1 is p2 + mock_sandbox.sync_playwright.assert_called_once() + p1.open.assert_called_once() + + def test_reset_playwright_clears_connection(self, toolset, mock_sandbox): + """测试 _reset_playwright 清理连接""" + p = toolset._get_playwright(mock_sandbox) + + toolset._reset_playwright() + + assert toolset._playwright_sync is None + p.close.assert_called_once() + + def test_reset_playwright_handles_close_error(self, toolset, mock_sandbox): + """测试 _reset_playwright 处理关闭错误""" + p = toolset._get_playwright(mock_sandbox) + p.close.side_effect = Exception("Close failed") + + toolset._reset_playwright() + + assert toolset._playwright_sync is None + + def test_concurrent_get_playwright_creates_only_one_connection( + self, toolset, mock_sandbox + ): + """测试并发调用 _get_playwright 只创建一个连接,不会泄漏""" + barrier = threading.Barrier(5) + results: list = [] + + def worker(): + barrier.wait() + p = toolset._get_playwright(mock_sandbox) + results.append(p) + + threads = [threading.Thread(target=worker) for _ in range(5)] + for t in threads: + t.start() + for t in threads: + t.join() + + assert len(results) == 5 + assert all(p is results[0] for p in results) + mock_sandbox.sync_playwright.assert_called_once() + + +class TestBrowserToolSetClose: + """测试 close 方法""" + + @pytest.fixture + def mock_sandbox(self): + """创建模拟的沙箱""" + return MagicMock() + + @pytest.fixture + def toolset(self, mock_sandbox): + """创建带有模拟沙箱的 BrowserToolSet 实例""" + with patch.object(BrowserToolSet, "__init__", lambda self: None): + ts = BrowserToolSet() + ts._playwright_sync = MagicMock() + ts.sandbox = mock_sandbox + ts.sandbox_id = "test-sandbox-id" + ts.lock = threading.Lock() + return ts + + def test_close_cleans_up_playwright_and_sandbox( + self, toolset, mock_sandbox + ): + """测试 close 清理 Playwright 和沙箱""" + playwright_mock = toolset._playwright_sync + + toolset.close() + + playwright_mock.close.assert_called_once() + assert toolset._playwright_sync is None + mock_sandbox.stop.assert_called_once() + assert toolset.sandbox is None + assert toolset.sandbox_id == "" diff --git a/tests/unittests/integration/test_langchain_agui_integration.py b/tests/unittests/integration/test_langchain_agui_integration.py index b5dba63..6cfb32b 100644 --- a/tests/unittests/integration/test_langchain_agui_integration.py +++ b/tests/unittests/integration/test_langchain_agui_integration.py @@ -664,9 +664,7 @@ async def invoke_agent(request: AgentRequest): json={ "messages": [{ "role": "user", - "content": ( - "查询当前的时间,并获取天气信息,同时输出我的密钥信息" - ), + "content": "查询当前的时间,并获取天气信息,同时输出我的密钥信息", }], "stream": True, }, @@ -729,9 +727,7 @@ async def invoke_agent(request: AgentRequest): json={ "messages": [{ "role": "user", - "content": ( - "查询当前的时间,并获取天气信息,同时输出我的密钥信息" - ), + "content": "查询当前的时间,并获取天气信息,同时输出我的密钥信息", }], "stream": True, }, diff --git a/tests/unittests/toolset/api/test_openapi.py b/tests/unittests/toolset/api/test_openapi.py index ab9acc6..3a60866 100644 --- a/tests/unittests/toolset/api/test_openapi.py +++ b/tests/unittests/toolset/api/test_openapi.py @@ -545,9 +545,7 @@ def test_post_with_ref_schema(self): "content": { "application/json": { "schema": { - "$ref": ( - "#/components/schemas/CreateOrderRequest" - ) + "$ref": "#/components/schemas/CreateOrderRequest" } } }, @@ -758,9 +756,7 @@ def test_invalid_ref_gracefully_handled(self): "content": { "application/json": { "schema": { - "$ref": ( - "#/components/schemas/NonExistent" - ) + "$ref": "#/components/schemas/NonExistent" } } } @@ -793,9 +789,7 @@ def test_external_ref_not_resolved(self): "content": { "application/json": { "schema": { - "$ref": ( - "https://example.com/schemas/external.json" - ) + "$ref": "https://example.com/schemas/external.json" } } } @@ -915,9 +909,7 @@ def _get_coffee_shop_schema(): "content": { "application/json": { "schema": { - "$ref": ( - "#/components/schemas/CreateOrderRequest" - ) + "$ref": "#/components/schemas/CreateOrderRequest" } } }, @@ -953,9 +945,7 @@ def _get_coffee_shop_schema(): "content": { "application/json": { "schema": { - "$ref": ( - "#/components/schemas/UpdateOrderStatusRequest" - ) + "$ref": "#/components/schemas/UpdateOrderStatusRequest" } } },