aidev-bkplugin 2.2.0.post2__tar.gz → 2.2.0.post4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/PKG-INFO +2 -2
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/services/agent_bkplugin.py +17 -1
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/services/agent_execution.py +12 -18
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/views/chat.py +3 -1
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/pyproject.toml +2 -2
- aidev_bkplugin-2.2.0.post4/tests/services/test_agent_completion.py +58 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/tests/services/test_bkplugin.py +38 -11
- aidev_bkplugin-2.2.0.post4/tests/views/test_chat_stream_status.py +40 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/__init__.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/apps.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/constants.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/enums.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/management/__init__.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/management/commands/__init__.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/management/commands/upgrade_sessions.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/migrations/0001_initial.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/migrations/__init__.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/models.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/openapi/__init__.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/openapi/urls.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/openapi/views.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/packages/__init__.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/packages/apigw/__init__.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/packages/apigw/permissions.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/packages/checkpoint/__init__.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/packages/checkpoint/bk_django_saver.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/packages/drf/__init__.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/packages/drf/authentication.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/packages/drf/decorators.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/packages/drf/exception.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/packages/drf/renderers.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/permissions.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/private/__init__.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/private/permissions.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/private/urls.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/private/views.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/serializers/__init__.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/serializers/agent.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/serializers/chat_completion.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/serializers/user_operation.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/services/__init__.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/services/agent_builder.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/services/agent_config.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/services/agent_helpers.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/services/agent_session.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/services/approval_resume.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/services/user_operation.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/settings.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/tasks.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/urls.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/utils.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/views/__init__.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/views/agent.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/views/base.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/views/chat_group.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/views/flow_agent.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/views/session.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/views/user_operation.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/readme.md +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/tests/__init__.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/tests/conftest.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/tests/packages/__init__.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/tests/packages/test_bk_django_saver.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/tests/private/__init__.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/tests/private/test_private_views.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/tests/services/__init__.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/tests/services/agent/__init__.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/tests/services/agent/conftest.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/tests/services/agent/test_agent_config_fetcher.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/tests/services/agent/test_builder.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/tests/services/agent/test_session_manager.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/tests/services/test_agui_handler.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/tests/services/test_thread_chat_completion.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/tests/services/test_user_operation.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/tests/settings.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/tests/test_constants.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/tests/test_utils_otel.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/tests/views/__init__.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/tests/views/test_chat_completion_serializer.py +0 -0
- {aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/tests/views/test_chat_flow_agent.py +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: aidev-bkplugin
|
|
3
|
-
Version: 2.2.0.
|
|
3
|
+
Version: 2.2.0.post4
|
|
4
4
|
Summary: AiDev Agent Bkplugin builtin functions
|
|
5
5
|
Requires-Python: <3.12,>=3.11
|
|
6
|
-
Requires-Dist: aidev-agent>=2.2.
|
|
6
|
+
Requires-Dist: aidev-agent>=2.2.0post4
|
|
7
7
|
Requires-Dist: blueapps<5.0,>=4.15.1
|
|
8
8
|
Requires-Dist: django<5.0,>=4.2.28
|
|
9
9
|
Description-Content-Type: text/markdown
|
{aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/services/agent_bkplugin.py
RENAMED
|
@@ -28,6 +28,7 @@ from aidev_agent.pydantic_models import ExecuteKwargs
|
|
|
28
28
|
from aidev_agent.services.agent import AgentInstanceFactory
|
|
29
29
|
from aidev_agent.services.common_agent import common_agent_factory
|
|
30
30
|
from aidev_agent.services.event_handlers.agui_writer import AGUISessionWriter
|
|
31
|
+
from aidev_agent.services.messages_handler import RetryableHeartbeatTimeoutError
|
|
31
32
|
from aidev_agent.utils.local import request_local
|
|
32
33
|
from django.contrib.auth import get_user_model
|
|
33
34
|
|
|
@@ -173,6 +174,13 @@ class BkpluginAgentRunner(ABC):
|
|
|
173
174
|
"""同步执行 Agent,返回最终 AI 回复字符串;异常时写回失败状态 (D-01)。"""
|
|
174
175
|
try:
|
|
175
176
|
return self._do_execute()
|
|
177
|
+
except RetryableHeartbeatTimeoutError:
|
|
178
|
+
logger.warning(
|
|
179
|
+
"[Bkplugin] skip session terminal update for retryable consumer heartbeat timeout session_code=%s",
|
|
180
|
+
self.session_code or "",
|
|
181
|
+
exc_info=True,
|
|
182
|
+
)
|
|
183
|
+
raise
|
|
176
184
|
except Exception as e:
|
|
177
185
|
logger.exception("[Bkplugin] execute error: %s", e)
|
|
178
186
|
session_code = self.session_code or ""
|
|
@@ -212,7 +220,7 @@ class BkpluginAgentRunner(ABC):
|
|
|
212
220
|
*,
|
|
213
221
|
chat_context: list[dict] | None = None,
|
|
214
222
|
) -> None:
|
|
215
|
-
"""Celery worker
|
|
223
|
+
"""Celery worker 入口:业务异常写失败;可重试消费异常保持 session 原状态。"""
|
|
216
224
|
logger.info(
|
|
217
225
|
"[Bkplugin] run_worker enter session_code=%s turn_id=%s thread=%s",
|
|
218
226
|
session_code,
|
|
@@ -221,6 +229,14 @@ class BkpluginAgentRunner(ABC):
|
|
|
221
229
|
)
|
|
222
230
|
try:
|
|
223
231
|
self.invoke_agent(session_code, execute_payload, chat_context=chat_context or [])
|
|
232
|
+
except RetryableHeartbeatTimeoutError:
|
|
233
|
+
logger.warning(
|
|
234
|
+
"[Bkplugin] skip session terminal update for retryable consumer heartbeat timeout "
|
|
235
|
+
"session_code=%s turn_id=%s",
|
|
236
|
+
session_code,
|
|
237
|
+
execute_payload.get("turn_id") or "",
|
|
238
|
+
exc_info=True,
|
|
239
|
+
)
|
|
224
240
|
except Exception as e:
|
|
225
241
|
logger.exception("[Bkplugin] worker error session_code=%s", session_code)
|
|
226
242
|
manager = SessionManager(self.username or "")
|
{aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/services/agent_execution.py
RENAMED
|
@@ -89,7 +89,7 @@ class AgentExecutor:
|
|
|
89
89
|
*,
|
|
90
90
|
turn_id: str = "",
|
|
91
91
|
):
|
|
92
|
-
"""执行 agent
|
|
92
|
+
"""执行 agent 直至结束;后台消费者完整 drain 后统一收尾会话状态。"""
|
|
93
93
|
# 后台 drain(for _ in out: pass,无 SSE 下游):标记为 background_only,
|
|
94
94
|
# 使消费者读到 EOD 时不立即清理队列,保留 DLQ 历史供前端在清理窗口内接管续流。
|
|
95
95
|
execute_kwargs.background_only = True
|
|
@@ -99,24 +99,18 @@ class AgentExecutor:
|
|
|
99
99
|
handler.turn_id = turn_id
|
|
100
100
|
if hasattr(handler, "set_streaming_started"):
|
|
101
101
|
handler.set_streaming_started()
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
return out
|
|
113
|
-
finally:
|
|
114
|
-
if (
|
|
115
|
-
execute_kwargs.stream
|
|
116
|
-
and isinstance(handler, BaseSessionWriter)
|
|
117
|
-
and hasattr(handler, "set_streaming_finished")
|
|
118
|
-
):
|
|
102
|
+
out = cls(session_manager).execute_with_save(
|
|
103
|
+
agent_instance,
|
|
104
|
+
execute_kwargs,
|
|
105
|
+
session_code,
|
|
106
|
+
turn_id=turn_id,
|
|
107
|
+
)
|
|
108
|
+
if execute_kwargs.stream:
|
|
109
|
+
for _ in out:
|
|
110
|
+
pass
|
|
111
|
+
if isinstance(handler, BaseSessionWriter) and hasattr(handler, "set_streaming_finished"):
|
|
119
112
|
handler.set_streaming_finished()
|
|
113
|
+
return out
|
|
120
114
|
|
|
121
115
|
def wrap_generator(self, generator, session_code: str, *, turn_id: str = ""):
|
|
122
116
|
"""SSE 数据格式约定:
|
|
@@ -397,9 +397,11 @@ class ChatCompletionViewSet(PluginViewSet):
|
|
|
397
397
|
_preempted = False
|
|
398
398
|
_client_disconnected = False
|
|
399
399
|
_cancelled = False
|
|
400
|
+
_stream_completed = False
|
|
400
401
|
try:
|
|
401
402
|
for chunk in generator:
|
|
402
403
|
yield chunk
|
|
404
|
+
_stream_completed = True
|
|
403
405
|
except GeneratorExit:
|
|
404
406
|
_client_disconnected = True
|
|
405
407
|
logger.info(f"[WRAP_STATUS] Client disconnected (GeneratorExit): session_code={session_code}")
|
|
@@ -421,7 +423,7 @@ class ChatCompletionViewSet(PluginViewSet):
|
|
|
421
423
|
if GeneratorStreamingHelper.is_cancelled(session_code):
|
|
422
424
|
_cancelled = True
|
|
423
425
|
logger.info(f"[WRAP_STATUS] Generator结束后检测到取消标志: session_code={session_code}")
|
|
424
|
-
if not _preempted and not _client_disconnected and not _cancelled:
|
|
426
|
+
if _stream_completed and not _preempted and not _client_disconnected and not _cancelled:
|
|
425
427
|
logger.info(f"[WRAP_STATUS] 流正常结束, 调用 set_streaming_finished: session_code={session_code}")
|
|
426
428
|
event_handler.set_streaming_finished()
|
|
427
429
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "aidev-bkplugin"
|
|
3
|
-
version = "2.2.
|
|
3
|
+
version = "2.2.0post4"
|
|
4
4
|
description = "AiDev Agent Bkplugin builtin functions"
|
|
5
5
|
readme = "readme.md"
|
|
6
6
|
requires-python = "<3.12,>=3.11"
|
|
7
7
|
dependencies = [
|
|
8
|
-
"aidev-agent>=2.2.
|
|
8
|
+
"aidev-agent>=2.2.0post4",
|
|
9
9
|
"blueapps>=4.15.1, <5.0",
|
|
10
10
|
"django>=4.2.28,<5.0",
|
|
11
11
|
]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""后台 Agent 流式终态写入回归测试。"""
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
from unittest.mock import MagicMock, patch
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
if "pkg_resources" not in sys.modules:
|
|
10
|
+
sys.modules["pkg_resources"] = MagicMock()
|
|
11
|
+
|
|
12
|
+
sys.modules.setdefault("aidev_bkplugin.models", MagicMock())
|
|
13
|
+
_bk_plugin_framework = MagicMock()
|
|
14
|
+
_bk_plugin_framework.kit.decorators.inject_user_token = lambda func: func
|
|
15
|
+
sys.modules.setdefault("bk_plugin_framework", _bk_plugin_framework)
|
|
16
|
+
sys.modules.setdefault("bk_plugin_framework.kit", _bk_plugin_framework.kit)
|
|
17
|
+
sys.modules.setdefault("bk_plugin_framework.kit.decorators", _bk_plugin_framework.kit.decorators)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TestAgentExecutorCompletion:
|
|
21
|
+
def test_sets_finished_after_stream_is_drained(self):
|
|
22
|
+
from aidev_agent.services.event_handlers.base import BaseSessionWriter
|
|
23
|
+
from aidev_bkplugin.services.agent_execution import AgentExecutor
|
|
24
|
+
|
|
25
|
+
lifecycle = []
|
|
26
|
+
|
|
27
|
+
def stream():
|
|
28
|
+
yield "chunk"
|
|
29
|
+
lifecycle.append("drained")
|
|
30
|
+
|
|
31
|
+
handler = MagicMock(spec=BaseSessionWriter)
|
|
32
|
+
handler.set_streaming_finished.side_effect = lambda: lifecycle.append("finished")
|
|
33
|
+
agent = MagicMock(event_handler=handler)
|
|
34
|
+
execute_kwargs = MagicMock(stream=True)
|
|
35
|
+
with patch.object(AgentExecutor, "execute_with_save", return_value=stream()):
|
|
36
|
+
AgentExecutor.run_agent_to_completion(agent, execute_kwargs, "session-abc", MagicMock())
|
|
37
|
+
|
|
38
|
+
assert execute_kwargs.background_only is True
|
|
39
|
+
assert lifecycle == ["drained", "finished"]
|
|
40
|
+
|
|
41
|
+
def test_does_not_set_finished_when_stream_drain_fails(self):
|
|
42
|
+
from aidev_agent.services.event_handlers.base import BaseSessionWriter
|
|
43
|
+
from aidev_bkplugin.services.agent_execution import AgentExecutor
|
|
44
|
+
|
|
45
|
+
def failing_stream():
|
|
46
|
+
yield "chunk"
|
|
47
|
+
raise RuntimeError("producer heartbeat timeout")
|
|
48
|
+
|
|
49
|
+
handler = MagicMock(spec=BaseSessionWriter)
|
|
50
|
+
agent = MagicMock(event_handler=handler)
|
|
51
|
+
execute_kwargs = MagicMock(stream=True)
|
|
52
|
+
with (
|
|
53
|
+
patch.object(AgentExecutor, "execute_with_save", return_value=failing_stream()),
|
|
54
|
+
pytest.raises(RuntimeError, match="heartbeat timeout"),
|
|
55
|
+
):
|
|
56
|
+
AgentExecutor.run_agent_to_completion(agent, execute_kwargs, "session-abc", MagicMock())
|
|
57
|
+
|
|
58
|
+
handler.set_streaming_finished.assert_not_called()
|
|
@@ -23,6 +23,7 @@ sys.modules.setdefault("bk_plugin_framework.kit", _bk_plugin_framework.kit)
|
|
|
23
23
|
sys.modules.setdefault("bk_plugin_framework.kit.decorators", _bk_plugin_framework.kit.decorators)
|
|
24
24
|
|
|
25
25
|
from aidev_agent.enums import AgentType, ChatContentStatus, PromptRole, SessionsStatus # noqa: E402
|
|
26
|
+
from aidev_agent.services.messages_handler import RetryableHeartbeatTimeoutError # noqa: E402
|
|
26
27
|
from aidev_bkplugin.enums import PluginPollTaskState # noqa: E402
|
|
27
28
|
from aidev_bkplugin.services.agent_session import SessionManager # noqa: E402
|
|
28
29
|
|
|
@@ -272,14 +273,10 @@ class TestBkpluginExecution:
|
|
|
272
273
|
from aidev_bkplugin.services.agent_bkplugin import BkpluginChat
|
|
273
274
|
|
|
274
275
|
agent = BkpluginChat(chat_history=[], execute_kwargs={}, username="alice")
|
|
275
|
-
monkeypatch.setattr(
|
|
276
|
-
agent, "invoke_agent", MagicMock(side_effect=RuntimeError("生产者心跳超时"))
|
|
277
|
-
)
|
|
276
|
+
monkeypatch.setattr(agent, "invoke_agent", MagicMock(side_effect=RuntimeError("生产者心跳超时")))
|
|
278
277
|
mock_sm = MagicMock()
|
|
279
278
|
mock_sm.retrieve_session.return_value = {"status": SessionsStatus.FINISHED.value}
|
|
280
|
-
monkeypatch.setattr(
|
|
281
|
-
"aidev_bkplugin.services.agent_bkplugin.SessionManager", lambda username: mock_sm
|
|
282
|
-
)
|
|
279
|
+
monkeypatch.setattr("aidev_bkplugin.services.agent_bkplugin.SessionManager", lambda username: mock_sm)
|
|
283
280
|
|
|
284
281
|
agent.run_worker("sess-1", {"turn_id": "t1"})
|
|
285
282
|
|
|
@@ -291,16 +288,46 @@ class TestBkpluginExecution:
|
|
|
291
288
|
from aidev_bkplugin.services.agent_bkplugin import BkpluginChat
|
|
292
289
|
|
|
293
290
|
agent = BkpluginChat(chat_history=[], execute_kwargs={}, username="alice")
|
|
294
|
-
monkeypatch.setattr(
|
|
295
|
-
agent, "invoke_agent", MagicMock(side_effect=RuntimeError("生产者心跳超时"))
|
|
296
|
-
)
|
|
291
|
+
monkeypatch.setattr(agent, "invoke_agent", MagicMock(side_effect=RuntimeError("producer crashed")))
|
|
297
292
|
mock_sm = MagicMock()
|
|
298
293
|
mock_sm.retrieve_session.return_value = {"status": SessionsStatus.RUNNING.value}
|
|
294
|
+
monkeypatch.setattr("aidev_bkplugin.services.agent_bkplugin.SessionManager", lambda username: mock_sm)
|
|
295
|
+
|
|
296
|
+
agent.run_worker("sess-1", {"turn_id": "t1"})
|
|
297
|
+
|
|
298
|
+
mock_sm.save_stream_failure.assert_called_once_with("sess-1", ANY, turn_id="t1")
|
|
299
|
+
|
|
300
|
+
def test_run_worker_skips_session_update_for_retryable_heartbeat_timeout(self, monkeypatch):
|
|
301
|
+
"""消费者心跳超时可重试,不应更新 session 终态。"""
|
|
302
|
+
from aidev_bkplugin.services.agent_bkplugin import BkpluginChat
|
|
303
|
+
|
|
304
|
+
agent = BkpluginChat(chat_history=[], execute_kwargs={}, username="alice")
|
|
299
305
|
monkeypatch.setattr(
|
|
300
|
-
|
|
306
|
+
agent,
|
|
307
|
+
"invoke_agent",
|
|
308
|
+
MagicMock(side_effect=RetryableHeartbeatTimeoutError("生产者心跳超时")),
|
|
301
309
|
)
|
|
310
|
+
manager_factory = MagicMock()
|
|
311
|
+
monkeypatch.setattr("aidev_bkplugin.services.agent_bkplugin.SessionManager", manager_factory)
|
|
302
312
|
|
|
303
313
|
agent.run_worker("sess-1", {"turn_id": "t1"})
|
|
304
314
|
|
|
305
|
-
|
|
315
|
+
manager_factory.assert_not_called()
|
|
316
|
+
|
|
317
|
+
def test_execute_skips_session_update_for_retryable_heartbeat_timeout(self, monkeypatch):
|
|
318
|
+
"""同步消费遇到可重试心跳超时,也不应覆盖 session 状态。"""
|
|
319
|
+
from aidev_bkplugin.services.agent_bkplugin import BkpluginChat
|
|
320
|
+
|
|
321
|
+
agent = BkpluginChat(chat_history=[], execute_kwargs={"session_code": "sess-1"}, username="alice")
|
|
322
|
+
monkeypatch.setattr(
|
|
323
|
+
agent,
|
|
324
|
+
"_do_execute",
|
|
325
|
+
MagicMock(side_effect=RetryableHeartbeatTimeoutError("生产者心跳超时")),
|
|
326
|
+
)
|
|
327
|
+
manager_factory = MagicMock()
|
|
328
|
+
monkeypatch.setattr("aidev_bkplugin.services.agent_bkplugin.SessionManager", manager_factory)
|
|
329
|
+
|
|
330
|
+
with pytest.raises(RetryableHeartbeatTimeoutError):
|
|
331
|
+
agent.execute()
|
|
306
332
|
|
|
333
|
+
manager_factory.assert_not_called()
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Chat SSE 异常时的会话终态回归测试。"""
|
|
3
|
+
|
|
4
|
+
from unittest.mock import MagicMock
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.mark.parametrize("error", [RuntimeError("生产者心跳超时"), SystemExit("worker abort")])
|
|
10
|
+
def test_unexpected_stream_error_does_not_mark_session_finished(monkeypatch, error):
|
|
11
|
+
from aidev_agent.services.messages_handler.streaming_helper import GeneratorStreamingHelper
|
|
12
|
+
from aidev_bkplugin.views.chat import ChatCompletionViewSet
|
|
13
|
+
|
|
14
|
+
def failing_stream():
|
|
15
|
+
yield "chunk"
|
|
16
|
+
raise error
|
|
17
|
+
|
|
18
|
+
monkeypatch.setattr(GeneratorStreamingHelper, "is_cancelled", lambda _session_code: False)
|
|
19
|
+
event_handler = MagicMock()
|
|
20
|
+
stream = ChatCompletionViewSet()._wrap_streaming_with_status(
|
|
21
|
+
failing_stream(), event_handler, session_code="session-1"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
assert next(stream) == "chunk"
|
|
25
|
+
with pytest.raises(type(error)):
|
|
26
|
+
next(stream)
|
|
27
|
+
event_handler.set_streaming_finished.assert_not_called()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_normal_stream_marks_session_finished(monkeypatch):
|
|
31
|
+
from aidev_agent.services.messages_handler.streaming_helper import GeneratorStreamingHelper
|
|
32
|
+
from aidev_bkplugin.views.chat import ChatCompletionViewSet
|
|
33
|
+
|
|
34
|
+
monkeypatch.setattr(GeneratorStreamingHelper, "is_cancelled", lambda _session_code: False)
|
|
35
|
+
event_handler = MagicMock()
|
|
36
|
+
|
|
37
|
+
assert list(
|
|
38
|
+
ChatCompletionViewSet()._wrap_streaming_with_status(iter(["chunk"]), event_handler, session_code="session-1")
|
|
39
|
+
) == ["chunk"]
|
|
40
|
+
event_handler.set_streaming_finished.assert_called_once_with()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/management/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/migrations/0001_initial.py
RENAMED
|
File without changes
|
{aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/migrations/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/openapi/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/packages/__init__.py
RENAMED
|
File without changes
|
{aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/packages/apigw/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/packages/drf/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/packages/drf/decorators.py
RENAMED
|
File without changes
|
{aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/packages/drf/exception.py
RENAMED
|
File without changes
|
{aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/packages/drf/renderers.py
RENAMED
|
File without changes
|
|
File without changes
|
{aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/private/__init__.py
RENAMED
|
File without changes
|
{aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/private/permissions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/serializers/__init__.py
RENAMED
|
File without changes
|
{aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/serializers/agent.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/services/__init__.py
RENAMED
|
File without changes
|
{aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/services/agent_builder.py
RENAMED
|
File without changes
|
{aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/services/agent_config.py
RENAMED
|
File without changes
|
{aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/services/agent_helpers.py
RENAMED
|
File without changes
|
{aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/services/agent_session.py
RENAMED
|
File without changes
|
{aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/services/approval_resume.py
RENAMED
|
File without changes
|
{aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/services/user_operation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/views/chat_group.py
RENAMED
|
File without changes
|
{aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/views/flow_agent.py
RENAMED
|
File without changes
|
|
File without changes
|
{aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/views/user_operation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/tests/packages/test_bk_django_saver.py
RENAMED
|
File without changes
|
|
File without changes
|
{aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/tests/private/test_private_views.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/tests/services/agent/test_builder.py
RENAMED
|
File without changes
|
|
File without changes
|
{aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/tests/services/test_agui_handler.py
RENAMED
|
File without changes
|
|
File without changes
|
{aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/tests/services/test_user_operation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aidev_bkplugin-2.2.0.post2 → aidev_bkplugin-2.2.0.post4}/tests/views/test_chat_flow_agent.py
RENAMED
|
File without changes
|