aidev-bkplugin 2.2.0__tar.gz → 2.2.0.post3__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 → aidev_bkplugin-2.2.0.post3}/PKG-INFO +2 -2
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/packages/checkpoint/bk_django_saver.py +26 -12
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/services/agent_bkplugin.py +34 -2
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/services/agent_execution.py +12 -18
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/services/agent_session.py +20 -5
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/pyproject.toml +2 -2
- aidev_bkplugin-2.2.0.post3/tests/packages/test_bk_django_saver.py +69 -0
- aidev_bkplugin-2.2.0.post3/tests/services/test_agent_completion.py +58 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/tests/services/test_bkplugin.py +38 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/__init__.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/apps.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/constants.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/enums.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/management/__init__.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/management/commands/__init__.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/management/commands/upgrade_sessions.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/migrations/0001_initial.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/migrations/__init__.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/models.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/openapi/__init__.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/openapi/urls.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/openapi/views.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/packages/__init__.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/packages/apigw/__init__.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/packages/apigw/permissions.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/packages/checkpoint/__init__.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/packages/drf/__init__.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/packages/drf/authentication.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/packages/drf/decorators.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/packages/drf/exception.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/packages/drf/renderers.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/permissions.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/private/__init__.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/private/permissions.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/private/urls.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/private/views.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/serializers/__init__.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/serializers/agent.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/serializers/chat_completion.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/serializers/user_operation.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/services/__init__.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/services/agent_builder.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/services/agent_config.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/services/agent_helpers.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/services/approval_resume.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/services/user_operation.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/settings.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/tasks.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/urls.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/utils.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/views/__init__.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/views/agent.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/views/base.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/views/chat.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/views/chat_group.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/views/flow_agent.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/views/session.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/views/user_operation.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/readme.md +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/tests/__init__.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/tests/conftest.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/tests/packages/__init__.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/tests/private/__init__.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/tests/private/test_private_views.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/tests/services/__init__.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/tests/services/agent/__init__.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/tests/services/agent/conftest.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/tests/services/agent/test_agent_config_fetcher.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/tests/services/agent/test_builder.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/tests/services/agent/test_session_manager.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/tests/services/test_agui_handler.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/tests/services/test_thread_chat_completion.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/tests/services/test_user_operation.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/tests/settings.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/tests/test_constants.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/tests/test_utils_otel.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/tests/views/__init__.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/tests/views/test_chat_completion_serializer.py +0 -0
- {aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/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.post3
|
|
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.0post3
|
|
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
|
|
@@ -21,10 +21,11 @@ from __future__ import annotations
|
|
|
21
21
|
import json
|
|
22
22
|
import random
|
|
23
23
|
import threading
|
|
24
|
+
import time
|
|
24
25
|
from collections.abc import AsyncIterator, Iterator, Sequence
|
|
25
26
|
from typing import Any, Optional, Tuple, cast
|
|
26
27
|
|
|
27
|
-
from django.db import connections, router, transaction
|
|
28
|
+
from django.db import OperationalError, close_old_connections, connections, router, transaction
|
|
28
29
|
from langchain_core.runnables import RunnableConfig
|
|
29
30
|
from langgraph.checkpoint.base import (
|
|
30
31
|
WRITES_IDX_MAP,
|
|
@@ -40,6 +41,10 @@ from langgraph.checkpoint.base import (
|
|
|
40
41
|
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
|
|
41
42
|
from langgraph.checkpoint.serde.types import ChannelProtocol
|
|
42
43
|
|
|
44
|
+
RETRYABLE_DATABASE_ERROR_CODES = {1205, 1213}
|
|
45
|
+
CHECKPOINT_WRITE_MAX_RETRIES = 3
|
|
46
|
+
CHECKPOINT_WRITE_RETRY_DELAY_SECONDS = 0.05
|
|
47
|
+
|
|
43
48
|
|
|
44
49
|
def bulk_upsert(model, objs, update_fields, unique_fields):
|
|
45
50
|
"""
|
|
@@ -605,17 +610,26 @@ class BKDjangoSaver(BaseCheckpointSaver[str]):
|
|
|
605
610
|
serialized_metadata = json.loads(json.dumps(raw_metadata, default=str).replace("\\u0000", ""))
|
|
606
611
|
# 使用Django的update_or_create方法
|
|
607
612
|
with self.lock:
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
613
|
+
for attempt in range(CHECKPOINT_WRITE_MAX_RETRIES):
|
|
614
|
+
try:
|
|
615
|
+
self.checkpoint_model.objects.update_or_create(
|
|
616
|
+
thread_id=thread_id,
|
|
617
|
+
checkpoint_ns=checkpoint_ns,
|
|
618
|
+
checkpoint_id=checkpoint["id"],
|
|
619
|
+
defaults={
|
|
620
|
+
"parent_checkpoint_id": config["configurable"].get("checkpoint_id"),
|
|
621
|
+
"type": type_,
|
|
622
|
+
"checkpoint": serialized_checkpoint,
|
|
623
|
+
"metadata": serialized_metadata,
|
|
624
|
+
},
|
|
625
|
+
)
|
|
626
|
+
break
|
|
627
|
+
except OperationalError as exc:
|
|
628
|
+
error_code = exc.args[0] if exc.args else None
|
|
629
|
+
if error_code not in RETRYABLE_DATABASE_ERROR_CODES or attempt == CHECKPOINT_WRITE_MAX_RETRIES - 1:
|
|
630
|
+
raise
|
|
631
|
+
close_old_connections()
|
|
632
|
+
time.sleep(CHECKPOINT_WRITE_RETRY_DELAY_SECONDS * (2**attempt))
|
|
619
633
|
|
|
620
634
|
return {
|
|
621
635
|
"configurable": {
|
{aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/services/agent_bkplugin.py
RENAMED
|
@@ -16,12 +16,13 @@
|
|
|
16
16
|
from __future__ import annotations
|
|
17
17
|
|
|
18
18
|
import logging
|
|
19
|
+
import threading
|
|
19
20
|
import uuid
|
|
20
21
|
from abc import ABC, abstractmethod
|
|
21
22
|
from typing import Any, ClassVar
|
|
22
23
|
|
|
23
24
|
from aidev_agent.config import settings as agent_settings
|
|
24
|
-
from aidev_agent.enums import AgentBuildType, AgentType, ChannelType, PromptRole
|
|
25
|
+
from aidev_agent.enums import AgentBuildType, AgentType, ChannelType, PromptRole, SessionsStatus
|
|
25
26
|
from aidev_agent.packages.resource_manager.agent import AgentResourceManager
|
|
26
27
|
from aidev_agent.pydantic_models import ExecuteKwargs
|
|
27
28
|
from aidev_agent.services.agent import AgentInstanceFactory
|
|
@@ -212,11 +213,42 @@ class BkpluginAgentRunner(ABC):
|
|
|
212
213
|
chat_context: list[dict] | None = None,
|
|
213
214
|
) -> None:
|
|
214
215
|
"""Celery worker 入口:捕获异常并写 session 失败状态。"""
|
|
216
|
+
logger.info(
|
|
217
|
+
"[Bkplugin] run_worker enter session_code=%s turn_id=%s thread=%s",
|
|
218
|
+
session_code,
|
|
219
|
+
execute_payload.get("turn_id") or "",
|
|
220
|
+
threading.current_thread().name,
|
|
221
|
+
)
|
|
215
222
|
try:
|
|
216
223
|
self.invoke_agent(session_code, execute_payload, chat_context=chat_context or [])
|
|
217
224
|
except Exception as e:
|
|
218
225
|
logger.exception("[Bkplugin] worker error session_code=%s", session_code)
|
|
219
|
-
SessionManager(self.username or "")
|
|
226
|
+
manager = SessionManager(self.username or "")
|
|
227
|
+
# 幂等保护:心跳超时误报时 producer 实际已完成(session=FINISHED),
|
|
228
|
+
# 不应再覆盖为 FAILED。仅当 session 仍处于非终态时才写失败,
|
|
229
|
+
# 避免覆盖已确定的终态(FINISHED/CANCELLED/FAILED)。
|
|
230
|
+
try:
|
|
231
|
+
current_status = str(manager.retrieve_session(session_code).get("status") or "")
|
|
232
|
+
except Exception:
|
|
233
|
+
logger.exception(
|
|
234
|
+
"[Bkplugin] retrieve_session failed before save_stream_failure session_code=%s",
|
|
235
|
+
session_code,
|
|
236
|
+
)
|
|
237
|
+
current_status = ""
|
|
238
|
+
if current_status in (
|
|
239
|
+
SessionsStatus.FINISHED.value,
|
|
240
|
+
SessionsStatus.CANCELLED.value,
|
|
241
|
+
SessionsStatus.FAILED.value,
|
|
242
|
+
):
|
|
243
|
+
logger.warning(
|
|
244
|
+
"[Bkplugin] skip save_stream_failure: session already %s "
|
|
245
|
+
"(likely heartbeat-timeout false positive) session_code=%s exc=%r",
|
|
246
|
+
current_status,
|
|
247
|
+
session_code,
|
|
248
|
+
e,
|
|
249
|
+
)
|
|
250
|
+
return
|
|
251
|
+
manager.save_stream_failure(
|
|
220
252
|
session_code,
|
|
221
253
|
f"Agent 执行异常: {e}",
|
|
222
254
|
turn_id=execute_payload.get("turn_id") or "",
|
{aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/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 数据格式约定:
|
{aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/services/agent_session.py
RENAMED
|
@@ -238,24 +238,39 @@ class SessionManager:
|
|
|
238
238
|
if status in (SessionsStatus.PENDING.value, SessionsStatus.RUNNING.value):
|
|
239
239
|
return PluginPollTaskState.RUNNING, ""
|
|
240
240
|
if status in (SessionsStatus.FAILED.value, SessionsStatus.CANCELLED.value):
|
|
241
|
-
|
|
241
|
+
# 取回 save_stream_failure 写入的实际异常内容,避免 UI 只显示通用 "Agent 执行失败"
|
|
242
|
+
return PluginPollTaskState.FAILED, self._last_output(
|
|
243
|
+
self.list_session_contents(session_code),
|
|
244
|
+
turn_id=turn_id,
|
|
245
|
+
statuses=(ChatContentStatus.ERROR.value,),
|
|
246
|
+
) or "Agent 执行失败"
|
|
242
247
|
if status == SessionsStatus.FINISHED.value:
|
|
243
|
-
return PluginPollTaskState.SUCCESS, self.
|
|
248
|
+
return PluginPollTaskState.SUCCESS, self._last_output(
|
|
244
249
|
self.list_session_contents(session_code),
|
|
245
250
|
turn_id=turn_id,
|
|
251
|
+
statuses=(ChatContentStatus.COMPLETE.value, ChatContentStatus.SUCCESS.value),
|
|
246
252
|
)
|
|
247
253
|
return PluginPollTaskState.RUNNING, ""
|
|
248
254
|
|
|
249
255
|
@staticmethod
|
|
250
|
-
def
|
|
251
|
-
|
|
256
|
+
def _last_output(
|
|
257
|
+
items: list[dict],
|
|
258
|
+
*,
|
|
259
|
+
turn_id: str = "",
|
|
260
|
+
statuses: tuple[str, ...],
|
|
261
|
+
) -> str:
|
|
262
|
+
"""取最后一条指定 status 的 assistant/ai 内容;有 ``turn_id`` 时仅取同轮消息。
|
|
263
|
+
|
|
264
|
+
FINISHED 取 COMPLETE/SUCCESS 的正常回复;FAILED/CANCELLED 取 ERROR 的实际异常
|
|
265
|
+
(save_stream_failure 写入),避免上层只看到通用 "Agent 执行失败"。
|
|
266
|
+
"""
|
|
252
267
|
assistant_roles = (PromptRole.ASSISTANT.value, PromptRole.AI.value)
|
|
253
268
|
for item in reversed(items):
|
|
254
269
|
if item.get("role") not in assistant_roles:
|
|
255
270
|
continue
|
|
256
271
|
if turn_id and (item.get("property") or {}).get("turn_id") != turn_id:
|
|
257
272
|
continue
|
|
258
|
-
if item.get("status") not in
|
|
273
|
+
if item.get("status") not in statuses:
|
|
259
274
|
continue
|
|
260
275
|
text = str(item.get("content") or "").strip()
|
|
261
276
|
if text:
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "aidev-bkplugin"
|
|
3
|
-
version = "2.2.
|
|
3
|
+
version = "2.2.0post3"
|
|
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.0post3",
|
|
9
9
|
"blueapps>=4.15.1, <5.0",
|
|
10
10
|
"django>=4.2.28,<5.0",
|
|
11
11
|
]
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from aidev_bkplugin.packages.checkpoint.bk_django_saver import BKDjangoSaver
|
|
7
|
+
from django.db import OperationalError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.fixture
|
|
11
|
+
def saver(mocker):
|
|
12
|
+
instance = object.__new__(BKDjangoSaver)
|
|
13
|
+
instance.lock = threading.Lock()
|
|
14
|
+
instance.checkpoint_model = mocker.Mock()
|
|
15
|
+
instance.serde = mocker.Mock()
|
|
16
|
+
instance.serde.dumps_typed.return_value = ("json", b"checkpoint")
|
|
17
|
+
return instance
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.fixture
|
|
21
|
+
def checkpoint_args():
|
|
22
|
+
return (
|
|
23
|
+
{"configurable": {"thread_id": "thread-id"}},
|
|
24
|
+
{"id": "checkpoint-id"},
|
|
25
|
+
{},
|
|
26
|
+
{},
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@pytest.mark.parametrize("error_code", [1205, 1213])
|
|
31
|
+
def test_put_retries_transient_database_lock_error(mocker, saver, checkpoint_args, error_code):
|
|
32
|
+
saver.checkpoint_model.objects.update_or_create.side_effect = [
|
|
33
|
+
OperationalError(error_code, "retryable"),
|
|
34
|
+
(mocker.Mock(), True),
|
|
35
|
+
]
|
|
36
|
+
close_old_connections = mocker.patch("aidev_bkplugin.packages.checkpoint.bk_django_saver.close_old_connections")
|
|
37
|
+
sleep = mocker.patch("aidev_bkplugin.packages.checkpoint.bk_django_saver.time.sleep")
|
|
38
|
+
|
|
39
|
+
saver.put(*checkpoint_args)
|
|
40
|
+
|
|
41
|
+
assert saver.checkpoint_model.objects.update_or_create.call_count == 2
|
|
42
|
+
close_old_connections.assert_called_once_with()
|
|
43
|
+
sleep.assert_called_once_with(0.05)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_put_does_not_retry_other_database_error(mocker, saver, checkpoint_args):
|
|
47
|
+
error = OperationalError(2006, "server has gone away")
|
|
48
|
+
saver.checkpoint_model.objects.update_or_create.side_effect = error
|
|
49
|
+
sleep = mocker.patch("aidev_bkplugin.packages.checkpoint.bk_django_saver.time.sleep")
|
|
50
|
+
|
|
51
|
+
with pytest.raises(OperationalError) as exc_info:
|
|
52
|
+
saver.put(*checkpoint_args)
|
|
53
|
+
|
|
54
|
+
assert exc_info.value is error
|
|
55
|
+
assert saver.checkpoint_model.objects.update_or_create.call_count == 1
|
|
56
|
+
sleep.assert_not_called()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_put_raises_after_database_lock_retries_exhausted(mocker, saver, checkpoint_args):
|
|
60
|
+
error = OperationalError(1213, "deadlock")
|
|
61
|
+
saver.checkpoint_model.objects.update_or_create.side_effect = error
|
|
62
|
+
sleep = mocker.patch("aidev_bkplugin.packages.checkpoint.bk_django_saver.time.sleep")
|
|
63
|
+
|
|
64
|
+
with pytest.raises(OperationalError) as exc_info:
|
|
65
|
+
saver.put(*checkpoint_args)
|
|
66
|
+
|
|
67
|
+
assert exc_info.value is error
|
|
68
|
+
assert saver.checkpoint_model.objects.update_or_create.call_count == 3
|
|
69
|
+
assert [call.args[0] for call in sleep.call_args_list] == [0.05, 0.1]
|
|
@@ -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()
|
|
@@ -266,3 +266,41 @@ class TestBkpluginExecution:
|
|
|
266
266
|
assert runner.execute_kwargs["session_code"] == "thread-x"
|
|
267
267
|
assert runner.plugin_context == [{"k": "v"}]
|
|
268
268
|
mock_resolve.assert_called_once_with("alice")
|
|
269
|
+
|
|
270
|
+
def test_run_worker_skips_save_stream_failure_when_session_finished(self, monkeypatch):
|
|
271
|
+
"""心跳超时误报:producer 实际已完成(session=FINISHED),不应覆盖为 FAILED。"""
|
|
272
|
+
from aidev_bkplugin.services.agent_bkplugin import BkpluginChat
|
|
273
|
+
|
|
274
|
+
agent = BkpluginChat(chat_history=[], execute_kwargs={}, username="alice")
|
|
275
|
+
monkeypatch.setattr(
|
|
276
|
+
agent, "invoke_agent", MagicMock(side_effect=RuntimeError("生产者心跳超时"))
|
|
277
|
+
)
|
|
278
|
+
mock_sm = MagicMock()
|
|
279
|
+
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
|
+
)
|
|
283
|
+
|
|
284
|
+
agent.run_worker("sess-1", {"turn_id": "t1"})
|
|
285
|
+
|
|
286
|
+
mock_sm.retrieve_session.assert_called_once_with("sess-1")
|
|
287
|
+
mock_sm.save_stream_failure.assert_not_called()
|
|
288
|
+
|
|
289
|
+
def test_run_worker_calls_save_stream_failure_when_session_running(self, monkeypatch):
|
|
290
|
+
"""producer 真崩溃:session 仍 RUNNING 时应写失败。"""
|
|
291
|
+
from aidev_bkplugin.services.agent_bkplugin import BkpluginChat
|
|
292
|
+
|
|
293
|
+
agent = BkpluginChat(chat_history=[], execute_kwargs={}, username="alice")
|
|
294
|
+
monkeypatch.setattr(
|
|
295
|
+
agent, "invoke_agent", MagicMock(side_effect=RuntimeError("生产者心跳超时"))
|
|
296
|
+
)
|
|
297
|
+
mock_sm = MagicMock()
|
|
298
|
+
mock_sm.retrieve_session.return_value = {"status": SessionsStatus.RUNNING.value}
|
|
299
|
+
monkeypatch.setattr(
|
|
300
|
+
"aidev_bkplugin.services.agent_bkplugin.SessionManager", lambda username: mock_sm
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
agent.run_worker("sess-1", {"turn_id": "t1"})
|
|
304
|
+
|
|
305
|
+
mock_sm.save_stream_failure.assert_called_once_with("sess-1", ANY, turn_id="t1")
|
|
306
|
+
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/management/commands/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/migrations/0001_initial.py
RENAMED
|
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 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/packages/apigw/__init__.py
RENAMED
|
File without changes
|
{aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/packages/apigw/permissions.py
RENAMED
|
File without changes
|
{aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/packages/checkpoint/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/packages/drf/authentication.py
RENAMED
|
File without changes
|
{aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/packages/drf/decorators.py
RENAMED
|
File without changes
|
{aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/packages/drf/exception.py
RENAMED
|
File without changes
|
{aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/packages/drf/renderers.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 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/serializers/chat_completion.py
RENAMED
|
File without changes
|
{aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/serializers/user_operation.py
RENAMED
|
File without changes
|
|
File without changes
|
{aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/services/agent_builder.py
RENAMED
|
File without changes
|
|
File without changes
|
{aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/services/agent_helpers.py
RENAMED
|
File without changes
|
{aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/aidev_bkplugin/services/approval_resume.py
RENAMED
|
File without changes
|
{aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
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 → aidev_bkplugin-2.2.0.post3}/tests/services/agent/test_session_manager.py
RENAMED
|
File without changes
|
|
File without changes
|
{aidev_bkplugin-2.2.0 → aidev_bkplugin-2.2.0.post3}/tests/services/test_thread_chat_completion.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 → aidev_bkplugin-2.2.0.post3}/tests/views/test_chat_completion_serializer.py
RENAMED
|
File without changes
|
|
File without changes
|