aidev-bkplugin 2.2.0.post3__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.
Files changed (80) hide show
  1. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/PKG-INFO +2 -2
  2. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/services/agent_bkplugin.py +17 -1
  3. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/views/chat.py +3 -1
  4. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/pyproject.toml +2 -2
  5. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/tests/services/test_bkplugin.py +38 -11
  6. aidev_bkplugin-2.2.0.post4/tests/views/test_chat_stream_status.py +40 -0
  7. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/__init__.py +0 -0
  8. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/apps.py +0 -0
  9. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/constants.py +0 -0
  10. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/enums.py +0 -0
  11. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/management/__init__.py +0 -0
  12. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/management/commands/__init__.py +0 -0
  13. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/management/commands/upgrade_sessions.py +0 -0
  14. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/migrations/0001_initial.py +0 -0
  15. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/migrations/__init__.py +0 -0
  16. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/models.py +0 -0
  17. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/openapi/__init__.py +0 -0
  18. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/openapi/urls.py +0 -0
  19. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/openapi/views.py +0 -0
  20. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/packages/__init__.py +0 -0
  21. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/packages/apigw/__init__.py +0 -0
  22. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/packages/apigw/permissions.py +0 -0
  23. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/packages/checkpoint/__init__.py +0 -0
  24. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/packages/checkpoint/bk_django_saver.py +0 -0
  25. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/packages/drf/__init__.py +0 -0
  26. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/packages/drf/authentication.py +0 -0
  27. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/packages/drf/decorators.py +0 -0
  28. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/packages/drf/exception.py +0 -0
  29. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/packages/drf/renderers.py +0 -0
  30. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/permissions.py +0 -0
  31. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/private/__init__.py +0 -0
  32. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/private/permissions.py +0 -0
  33. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/private/urls.py +0 -0
  34. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/private/views.py +0 -0
  35. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/serializers/__init__.py +0 -0
  36. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/serializers/agent.py +0 -0
  37. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/serializers/chat_completion.py +0 -0
  38. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/serializers/user_operation.py +0 -0
  39. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/services/__init__.py +0 -0
  40. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/services/agent_builder.py +0 -0
  41. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/services/agent_config.py +0 -0
  42. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/services/agent_execution.py +0 -0
  43. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/services/agent_helpers.py +0 -0
  44. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/services/agent_session.py +0 -0
  45. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/services/approval_resume.py +0 -0
  46. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/services/user_operation.py +0 -0
  47. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/settings.py +0 -0
  48. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/tasks.py +0 -0
  49. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/urls.py +0 -0
  50. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/utils.py +0 -0
  51. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/views/__init__.py +0 -0
  52. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/views/agent.py +0 -0
  53. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/views/base.py +0 -0
  54. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/views/chat_group.py +0 -0
  55. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/views/flow_agent.py +0 -0
  56. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/views/session.py +0 -0
  57. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/aidev_bkplugin/views/user_operation.py +0 -0
  58. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/readme.md +0 -0
  59. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/tests/__init__.py +0 -0
  60. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/tests/conftest.py +0 -0
  61. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/tests/packages/__init__.py +0 -0
  62. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/tests/packages/test_bk_django_saver.py +0 -0
  63. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/tests/private/__init__.py +0 -0
  64. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/tests/private/test_private_views.py +0 -0
  65. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/tests/services/__init__.py +0 -0
  66. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/tests/services/agent/__init__.py +0 -0
  67. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/tests/services/agent/conftest.py +0 -0
  68. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/tests/services/agent/test_agent_config_fetcher.py +0 -0
  69. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/tests/services/agent/test_builder.py +0 -0
  70. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/tests/services/agent/test_session_manager.py +0 -0
  71. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/tests/services/test_agent_completion.py +0 -0
  72. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/tests/services/test_agui_handler.py +0 -0
  73. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/tests/services/test_thread_chat_completion.py +0 -0
  74. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/tests/services/test_user_operation.py +0 -0
  75. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/tests/settings.py +0 -0
  76. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/tests/test_constants.py +0 -0
  77. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/tests/test_utils_otel.py +0 -0
  78. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/tests/views/__init__.py +0 -0
  79. {aidev_bkplugin-2.2.0.post3 → aidev_bkplugin-2.2.0.post4}/tests/views/test_chat_completion_serializer.py +0 -0
  80. {aidev_bkplugin-2.2.0.post3 → 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.post3
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.0post3
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
@@ -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 入口:捕获异常并写 session 失败状态。"""
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 "")
@@ -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.0post3"
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.0post3",
8
+ "aidev-agent>=2.2.0post4",
9
9
  "blueapps>=4.15.1, <5.0",
10
10
  "django>=4.2.28,<5.0",
11
11
  ]
@@ -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
- "aidev_bkplugin.services.agent_bkplugin.SessionManager", lambda username: mock_sm
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
- mock_sm.save_stream_failure.assert_called_once_with("sess-1", ANY, turn_id="t1")
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()