aidev-bkplugin 2.2.0.post4__tar.gz → 2.2.0.post10__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.post4 → aidev_bkplugin-2.2.0.post10}/PKG-INFO +2 -2
  2. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/services/agent_execution.py +4 -3
  3. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/views/chat.py +2 -16
  4. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/pyproject.toml +2 -2
  5. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/tests/services/test_agent_completion.py +20 -3
  6. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/tests/services/test_agui_handler.py +26 -11
  7. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/tests/views/test_chat_flow_agent.py +12 -0
  8. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/__init__.py +0 -0
  9. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/apps.py +0 -0
  10. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/constants.py +0 -0
  11. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/enums.py +0 -0
  12. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/management/__init__.py +0 -0
  13. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/management/commands/__init__.py +0 -0
  14. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/management/commands/upgrade_sessions.py +0 -0
  15. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/migrations/0001_initial.py +0 -0
  16. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/migrations/__init__.py +0 -0
  17. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/models.py +0 -0
  18. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/openapi/__init__.py +0 -0
  19. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/openapi/urls.py +0 -0
  20. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/openapi/views.py +0 -0
  21. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/packages/__init__.py +0 -0
  22. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/packages/apigw/__init__.py +0 -0
  23. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/packages/apigw/permissions.py +0 -0
  24. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/packages/checkpoint/__init__.py +0 -0
  25. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/packages/checkpoint/bk_django_saver.py +0 -0
  26. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/packages/drf/__init__.py +0 -0
  27. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/packages/drf/authentication.py +0 -0
  28. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/packages/drf/decorators.py +0 -0
  29. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/packages/drf/exception.py +0 -0
  30. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/packages/drf/renderers.py +0 -0
  31. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/permissions.py +0 -0
  32. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/private/__init__.py +0 -0
  33. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/private/permissions.py +0 -0
  34. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/private/urls.py +0 -0
  35. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/private/views.py +0 -0
  36. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/serializers/__init__.py +0 -0
  37. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/serializers/agent.py +0 -0
  38. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/serializers/chat_completion.py +0 -0
  39. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/serializers/user_operation.py +0 -0
  40. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/services/__init__.py +0 -0
  41. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/services/agent_bkplugin.py +0 -0
  42. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/services/agent_builder.py +0 -0
  43. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/services/agent_config.py +0 -0
  44. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/services/agent_helpers.py +0 -0
  45. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/services/agent_session.py +0 -0
  46. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/services/approval_resume.py +0 -0
  47. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/services/user_operation.py +0 -0
  48. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/settings.py +0 -0
  49. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/tasks.py +0 -0
  50. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/urls.py +0 -0
  51. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/utils.py +0 -0
  52. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/views/__init__.py +0 -0
  53. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/views/agent.py +0 -0
  54. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/views/base.py +0 -0
  55. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/views/chat_group.py +0 -0
  56. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/views/flow_agent.py +0 -0
  57. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/views/session.py +0 -0
  58. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/aidev_bkplugin/views/user_operation.py +0 -0
  59. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/readme.md +0 -0
  60. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/tests/__init__.py +0 -0
  61. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/tests/conftest.py +0 -0
  62. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/tests/packages/__init__.py +0 -0
  63. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/tests/packages/test_bk_django_saver.py +0 -0
  64. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/tests/private/__init__.py +0 -0
  65. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/tests/private/test_private_views.py +0 -0
  66. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/tests/services/__init__.py +0 -0
  67. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/tests/services/agent/__init__.py +0 -0
  68. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/tests/services/agent/conftest.py +0 -0
  69. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/tests/services/agent/test_agent_config_fetcher.py +0 -0
  70. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/tests/services/agent/test_builder.py +0 -0
  71. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/tests/services/agent/test_session_manager.py +0 -0
  72. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/tests/services/test_bkplugin.py +0 -0
  73. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/tests/services/test_thread_chat_completion.py +0 -0
  74. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/tests/services/test_user_operation.py +0 -0
  75. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/tests/settings.py +0 -0
  76. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/tests/test_constants.py +0 -0
  77. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/tests/test_utils_otel.py +0 -0
  78. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/tests/views/__init__.py +0 -0
  79. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/tests/views/test_chat_completion_serializer.py +0 -0
  80. {aidev_bkplugin-2.2.0.post4 → aidev_bkplugin-2.2.0.post10}/tests/views/test_chat_stream_status.py +0 -0
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: aidev-bkplugin
3
- Version: 2.2.0.post4
3
+ Version: 2.2.0.post10
4
4
  Summary: AiDev Agent Bkplugin builtin functions
5
5
  Requires-Python: <3.12,>=3.11
6
- Requires-Dist: aidev-agent>=2.2.0post4
6
+ Requires-Dist: aidev-agent>=2.2.0post10
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
@@ -77,6 +77,9 @@ class AgentExecutor:
77
77
  result = agent_instance.execute(execute_kwargs)
78
78
  # 非流式 ainvoke 不经过 AG-UI 事件分发,必须显式写回 assistant
79
79
  self.session_manager.save_ai_response(session_code, result, turn_id=turn_id)
80
+ event_handler = getattr(agent_instance, "event_handler", None)
81
+ if isinstance(event_handler, BaseSessionWriter) and hasattr(event_handler, "set_streaming_finished"):
82
+ event_handler.set_streaming_finished()
80
83
  return result
81
84
 
82
85
  @classmethod
@@ -89,7 +92,7 @@ class AgentExecutor:
89
92
  *,
90
93
  turn_id: str = "",
91
94
  ):
92
- """执行 agent 直至结束;后台消费者完整 drain 后统一收尾会话状态。"""
95
+ """执行 agent 直至结束;会话终态由 Agent 流完成回调统一写入。"""
93
96
  # 后台 drain(for _ in out: pass,无 SSE 下游):标记为 background_only,
94
97
  # 使消费者读到 EOD 时不立即清理队列,保留 DLQ 历史供前端在清理窗口内接管续流。
95
98
  execute_kwargs.background_only = True
@@ -108,8 +111,6 @@ class AgentExecutor:
108
111
  if execute_kwargs.stream:
109
112
  for _ in out:
110
113
  pass
111
- if isinstance(handler, BaseSessionWriter) and hasattr(handler, "set_streaming_finished"):
112
- handler.set_streaming_finished()
113
114
  return out
114
115
 
115
116
  def wrap_generator(self, generator, session_code: str, *, turn_id: str = ""):
@@ -136,14 +136,8 @@ class ChatCompletionViewSet(PluginViewSet):
136
136
  turn_id=turn_id,
137
137
  )
138
138
  handler = getattr(agent_instance, "event_handler", None)
139
- if handler and all(hasattr(handler, m) for m in ["set_streaming_started", "set_streaming_finished"]):
139
+ if handler and hasattr(handler, "set_streaming_started"):
140
140
  handler.set_streaming_started()
141
- stream_out = self._wrap_streaming_with_status(
142
- stream_out,
143
- handler,
144
- session_code=session_code,
145
- username=username,
146
- )
147
141
  return self.streaming_response(stream_out, session_code=session_code)
148
142
  else:
149
143
  result = AgentExecutor(SessionManager(username=username)).execute_with_save(
@@ -374,16 +368,8 @@ class ChatCompletionViewSet(PluginViewSet):
374
368
  raise ClientBlueException(message=message)
375
369
 
376
370
  logger.info(f"[FLOW_AGENT] Streaming started: session_code={session_code}, task_id={task_id}")
377
- if event_handler and all(
378
- hasattr(event_handler, m) for m in ["set_streaming_started", "set_streaming_finished"]
379
- ):
371
+ if event_handler and hasattr(event_handler, "set_streaming_started"):
380
372
  event_handler.set_streaming_started()
381
- generator = self._wrap_streaming_with_status(
382
- generator,
383
- event_handler,
384
- session_code=session_code,
385
- username=username,
386
- )
387
373
  return self.streaming_response(generator, session_code=session_code)
388
374
 
389
375
  def _wrap_streaming_with_status(self, generator, event_handler, session_code: str = "", username: str = ""):
@@ -1,11 +1,11 @@
1
1
  [project]
2
2
  name = "aidev-bkplugin"
3
- version = "2.2.0post4"
3
+ version = "2.2.0post10"
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.0post4",
8
+ "aidev-agent>=2.2.0post10",
9
9
  "blueapps>=4.15.1, <5.0",
10
10
  "django>=4.2.28,<5.0",
11
11
  ]
@@ -18,7 +18,24 @@ sys.modules.setdefault("bk_plugin_framework.kit.decorators", _bk_plugin_framewor
18
18
 
19
19
 
20
20
  class TestAgentExecutorCompletion:
21
- def test_sets_finished_after_stream_is_drained(self):
21
+ def test_non_streaming_sets_finished_after_response_is_saved(self):
22
+ from aidev_agent.services.event_handlers.base import BaseSessionWriter
23
+ from aidev_bkplugin.services.agent_execution import AgentExecutor
24
+
25
+ result = {"choices": [{"delta": {"content": "done"}}]}
26
+ handler = MagicMock(spec=BaseSessionWriter)
27
+ agent = MagicMock(event_handler=handler)
28
+ agent.execute.return_value = result
29
+ execute_kwargs = MagicMock(stream=False)
30
+ manager = MagicMock()
31
+
32
+ assert (
33
+ AgentExecutor(manager).execute_with_save(agent, execute_kwargs, "session-abc", turn_id="turn-1") == result
34
+ )
35
+ manager.save_ai_response.assert_called_once_with("session-abc", result, turn_id="turn-1")
36
+ handler.set_streaming_finished.assert_called_once_with()
37
+
38
+ def test_drain_does_not_write_terminal_state(self):
22
39
  from aidev_agent.services.event_handlers.base import BaseSessionWriter
23
40
  from aidev_bkplugin.services.agent_execution import AgentExecutor
24
41
 
@@ -29,14 +46,14 @@ class TestAgentExecutorCompletion:
29
46
  lifecycle.append("drained")
30
47
 
31
48
  handler = MagicMock(spec=BaseSessionWriter)
32
- handler.set_streaming_finished.side_effect = lambda: lifecycle.append("finished")
33
49
  agent = MagicMock(event_handler=handler)
34
50
  execute_kwargs = MagicMock(stream=True)
35
51
  with patch.object(AgentExecutor, "execute_with_save", return_value=stream()):
36
52
  AgentExecutor.run_agent_to_completion(agent, execute_kwargs, "session-abc", MagicMock())
37
53
 
38
54
  assert execute_kwargs.background_only is True
39
- assert lifecycle == ["drained", "finished"]
55
+ assert lifecycle == ["drained"]
56
+ handler.set_streaming_finished.assert_not_called()
40
57
 
41
58
  def test_does_not_set_finished_when_stream_drain_fails(self):
42
59
  from aidev_agent.services.event_handlers.base import BaseSessionWriter
@@ -254,8 +254,8 @@ class TestCustomEventSessionPath:
254
254
  class TestHandleRunError:
255
255
  """测试 handle_run_error 的回写逻辑"""
256
256
 
257
- def test_run_error_writes_fail_status(self, session_writer, mock_client):
258
- """运行错误时回写 fail 状态"""
257
+ def test_run_error_defers_session_status(self, session_writer, mock_client):
258
+ """RUN_ERROR 只记录错误内容,不能抢先写会话终态。"""
259
259
  event = RunErrorEvent(type=EventType.RUN_ERROR, message="执行过程中发生错误")
260
260
 
261
261
  session_writer(event)
@@ -268,24 +268,39 @@ class TestHandleRunError:
268
268
  assert payload["role"] == PromptRole.ASSISTANT.value
269
269
  assert payload["content"] == "执行过程中发生错误"
270
270
  assert payload["property"]["builtin_property"]["error"] is True
271
+ mock_client.api.update_chat_session.assert_not_called()
272
+
273
+ def test_run_error_finished_keeps_session_failed(self, session_writer, mock_client):
274
+ """运行错误后结束流,不应把会话覆盖为 finished"""
275
+ event = RunErrorEvent(type=EventType.RUN_ERROR, message="执行过程中发生错误")
276
+
277
+ session_writer(event)
278
+ session_writer.set_streaming_finished()
279
+
271
280
  mock_client.api.update_chat_session.assert_called_once_with(
272
281
  path_params={"session_code": "test-session-123"},
273
282
  json={"status": SessionsStatus.FAILED.value},
274
283
  headers={"X-BKAIDEV-USER": "test-user"},
275
284
  )
276
285
 
277
- def test_run_error_finished_keeps_session_failed(self, session_writer, mock_client):
278
- """运行错误后结束流,不应把会话覆盖为 finished"""
279
- event = RunErrorEvent(type=EventType.RUN_ERROR, message="执行过程中发生错误")
286
+ def test_finished_status_retries_then_succeeds(self, session_writer, mock_client, monkeypatch):
287
+ sleep = MagicMock()
288
+ monkeypatch.setattr("aidev_agent.services.event_handlers.agui_writer.time.sleep", sleep)
289
+ mock_client.api.update_chat_session.side_effect = [RuntimeError("one"), RuntimeError("two"), {}]
280
290
 
281
- session_writer(event)
282
291
  session_writer.set_streaming_finished()
283
292
 
284
- status_payloads = [call.kwargs["json"] for call in mock_client.api.update_chat_session.call_args_list]
285
- assert status_payloads == [
286
- {"status": SessionsStatus.FAILED.value},
287
- {"status": SessionsStatus.FAILED.value},
288
- ]
293
+ assert mock_client.api.update_chat_session.call_count == 3
294
+ assert [call.args[0] for call in sleep.call_args_list] == [0.2, 0.4]
295
+
296
+ def test_finished_status_raises_after_retries_exhausted(self, session_writer, mock_client, monkeypatch):
297
+ monkeypatch.setattr("aidev_agent.services.event_handlers.agui_writer.time.sleep", MagicMock())
298
+ mock_client.api.update_chat_session.side_effect = RuntimeError("platform unavailable")
299
+
300
+ with pytest.raises(RuntimeError, match="platform unavailable"):
301
+ session_writer.set_streaming_finished()
302
+
303
+ assert mock_client.api.update_chat_session.call_count == 3
289
304
 
290
305
 
291
306
  def make_flow_agent_result_event(task_id: str, task_state: str = "RUNNING"):
@@ -72,3 +72,15 @@ def test_no_marker_starts_new_task_without_touching_marker(flow_agent_env):
72
72
 
73
73
  assert build_agent.call_args.kwargs["task_id"] is None
74
74
  sm.set_flow_resume_pending.assert_not_called()
75
+
76
+
77
+ def test_flow_http_does_not_install_second_terminal_status_writer(flow_agent_env):
78
+ view, _build_agent, sm, writer_cls = flow_agent_env
79
+ sm.get_flow_info.return_value = {}
80
+ view._wrap_streaming_with_status = MagicMock(side_effect=AssertionError("duplicate terminal writer"))
81
+
82
+ result = view._handle_flow_agent(_data(), "sc-1", "alice", turn_id="turn-1")
83
+
84
+ assert result == "STREAM"
85
+ writer_cls.return_value.set_streaming_started.assert_called_once_with()
86
+ view._wrap_streaming_with_status.assert_not_called()