borgee-plugin-sdk 0.1.2__py3-none-any.whl
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.
- borgee_plugin_sdk/__init__.py +83 -0
- borgee_plugin_sdk/_connection.py +1680 -0
- borgee_plugin_sdk/_wire/__init__.py +5 -0
- borgee_plugin_sdk/_wire/frames_generated.py +272 -0
- borgee_plugin_sdk/client.py +400 -0
- borgee_plugin_sdk/cursor.py +220 -0
- borgee_plugin_sdk/errors.py +61 -0
- borgee_plugin_sdk/models.py +326 -0
- borgee_plugin_sdk/py.typed +1 -0
- borgee_plugin_sdk/testing/__init__.py +16 -0
- borgee_plugin_sdk/testing/fake_transport.py +125 -0
- borgee_plugin_sdk-0.1.2.dist-info/METADATA +72 -0
- borgee_plugin_sdk-0.1.2.dist-info/RECORD +16 -0
- borgee_plugin_sdk-0.1.2.dist-info/WHEEL +5 -0
- borgee_plugin_sdk-0.1.2.dist-info/licenses/LICENSE +21 -0
- borgee_plugin_sdk-0.1.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# Code generated by cmd/bpp-tsgen; DO NOT EDIT.
|
|
2
|
+
#
|
|
3
|
+
# Source of truth: packages/server-go/internal/bpp/envelope.go
|
|
4
|
+
# Regenerate: cd packages/server-go && go run ./cmd/bpp-tsgen
|
|
5
|
+
# (or: go generate ./internal/bpp/...)
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Literal, NotRequired, TypeAlias, TypedDict
|
|
10
|
+
|
|
11
|
+
BppDirection: TypeAlias = Literal["server_to_plugin", "plugin_to_server"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class InboundAttachment(TypedDict):
|
|
15
|
+
url: str
|
|
16
|
+
filename: NotRequired[str]
|
|
17
|
+
content_type: str
|
|
18
|
+
size_bytes: NotRequired[int]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AgentConfigUpdateFrame(TypedDict):
|
|
22
|
+
type: Literal["agent_config_update"]
|
|
23
|
+
cursor: int
|
|
24
|
+
agent_id: str
|
|
25
|
+
schema_version: int
|
|
26
|
+
blob: str
|
|
27
|
+
idempotency_key: str
|
|
28
|
+
created_at: int
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AgentToggleFrame(TypedDict):
|
|
32
|
+
type: Literal["agent_toggle"]
|
|
33
|
+
agent_id: str
|
|
34
|
+
action: str
|
|
35
|
+
reason: str
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class InboundMessageFrame(TypedDict):
|
|
39
|
+
type: Literal["inbound_message"]
|
|
40
|
+
cursor: int
|
|
41
|
+
agent_id: str
|
|
42
|
+
channel_id: str
|
|
43
|
+
channel_type: str
|
|
44
|
+
kind: str
|
|
45
|
+
message_id: str
|
|
46
|
+
author_id: str
|
|
47
|
+
author_name: str
|
|
48
|
+
body: str
|
|
49
|
+
content_type: str
|
|
50
|
+
msg_type: str
|
|
51
|
+
attachments: NotRequired[list[InboundAttachment]]
|
|
52
|
+
created_at: int
|
|
53
|
+
emoji: str
|
|
54
|
+
reactor_id: str
|
|
55
|
+
reaction_added: bool
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class PermissionDeniedFrame(TypedDict):
|
|
59
|
+
type: Literal["permission_denied"]
|
|
60
|
+
cursor: int
|
|
61
|
+
agent_id: str
|
|
62
|
+
request_id: str
|
|
63
|
+
attempted_action: str
|
|
64
|
+
required_capability: str
|
|
65
|
+
current_scope: str
|
|
66
|
+
denied_at: int
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class SemanticActionResultFrame(TypedDict):
|
|
70
|
+
type: Literal["semantic_action_result"]
|
|
71
|
+
nonce: str
|
|
72
|
+
cursor: int
|
|
73
|
+
status: str
|
|
74
|
+
payload: str
|
|
75
|
+
error: str
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class SessionResumeAck(TypedDict):
|
|
79
|
+
type: Literal["session.resume_ack"]
|
|
80
|
+
replay_mode: str
|
|
81
|
+
count: int
|
|
82
|
+
high_water: int
|
|
83
|
+
reset: bool
|
|
84
|
+
reason: str
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class SessionSummaryFrame(TypedDict):
|
|
88
|
+
type: Literal["session.summary"]
|
|
89
|
+
cursor: int
|
|
90
|
+
missed_count: int
|
|
91
|
+
since_cursor: int
|
|
92
|
+
through_cursor: int
|
|
93
|
+
summary: str
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class ConnectFrame(TypedDict):
|
|
97
|
+
type: Literal["connect"]
|
|
98
|
+
plugin_id: str
|
|
99
|
+
token: str
|
|
100
|
+
version: str
|
|
101
|
+
capabilities: str
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class AgentRegisterFrame(TypedDict):
|
|
105
|
+
type: Literal["agent_register"]
|
|
106
|
+
plugin_id: str
|
|
107
|
+
agent_ids: list[str]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class RuntimeSchemaAdvertiseFrame(TypedDict):
|
|
111
|
+
type: Literal["runtime_schema_advertise"]
|
|
112
|
+
plugin_id: str
|
|
113
|
+
models: str
|
|
114
|
+
blob_keys: str
|
|
115
|
+
schema_ver: int
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class HeartbeatFrame(TypedDict):
|
|
119
|
+
type: Literal["heartbeat"]
|
|
120
|
+
plugin_id: str
|
|
121
|
+
agent_id: str
|
|
122
|
+
status: str
|
|
123
|
+
reason: str
|
|
124
|
+
timestamp: int
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class SemanticActionFrame(TypedDict):
|
|
128
|
+
type: Literal["semantic_action"]
|
|
129
|
+
agent_id: str
|
|
130
|
+
action: str
|
|
131
|
+
payload: str
|
|
132
|
+
nonce: str
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class ErrorReportFrame(TypedDict):
|
|
136
|
+
type: Literal["error_report"]
|
|
137
|
+
agent_id: str
|
|
138
|
+
kind: str
|
|
139
|
+
detail: str
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class AgentConfigAckFrame(TypedDict):
|
|
143
|
+
type: Literal["agent_config_ack"]
|
|
144
|
+
cursor: int
|
|
145
|
+
agent_id: str
|
|
146
|
+
schema_version: int
|
|
147
|
+
idempotency_key: str
|
|
148
|
+
status: str
|
|
149
|
+
reason: str
|
|
150
|
+
applied_at: int
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class TaskStartedFrame(TypedDict):
|
|
154
|
+
type: Literal["task_started"]
|
|
155
|
+
task_id: str
|
|
156
|
+
agent_id: str
|
|
157
|
+
channel_id: str
|
|
158
|
+
subject: str
|
|
159
|
+
started_at: int
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class TaskFinishedFrame(TypedDict):
|
|
163
|
+
type: Literal["task_finished"]
|
|
164
|
+
task_id: str
|
|
165
|
+
agent_id: str
|
|
166
|
+
channel_id: str
|
|
167
|
+
outcome: str
|
|
168
|
+
reason: str
|
|
169
|
+
finished_at: int
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class ColdStartHandshakeFrame(TypedDict):
|
|
173
|
+
type: Literal["cold_start_handshake"]
|
|
174
|
+
plugin_id: str
|
|
175
|
+
agent_id: str
|
|
176
|
+
restart_at: int
|
|
177
|
+
restart_reason: str
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class SessionResumeRequest(TypedDict):
|
|
181
|
+
type: Literal["session.resume"]
|
|
182
|
+
replay_mode: str
|
|
183
|
+
since_cursor: int
|
|
184
|
+
latest_n: NotRequired[int]
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
BppServerToPluginFrame: TypeAlias = (
|
|
188
|
+
AgentConfigUpdateFrame
|
|
189
|
+
| AgentToggleFrame
|
|
190
|
+
| InboundMessageFrame
|
|
191
|
+
| PermissionDeniedFrame
|
|
192
|
+
| SemanticActionResultFrame
|
|
193
|
+
| SessionResumeAck
|
|
194
|
+
| SessionSummaryFrame
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
BppPluginToServerFrame: TypeAlias = (
|
|
198
|
+
ConnectFrame
|
|
199
|
+
| AgentRegisterFrame
|
|
200
|
+
| RuntimeSchemaAdvertiseFrame
|
|
201
|
+
| HeartbeatFrame
|
|
202
|
+
| SemanticActionFrame
|
|
203
|
+
| ErrorReportFrame
|
|
204
|
+
| AgentConfigAckFrame
|
|
205
|
+
| TaskStartedFrame
|
|
206
|
+
| TaskFinishedFrame
|
|
207
|
+
| ColdStartHandshakeFrame
|
|
208
|
+
| SessionResumeRequest
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
BppFrame: TypeAlias = (
|
|
212
|
+
AgentConfigUpdateFrame
|
|
213
|
+
| AgentToggleFrame
|
|
214
|
+
| InboundMessageFrame
|
|
215
|
+
| PermissionDeniedFrame
|
|
216
|
+
| SemanticActionResultFrame
|
|
217
|
+
| SessionResumeAck
|
|
218
|
+
| SessionSummaryFrame
|
|
219
|
+
| ConnectFrame
|
|
220
|
+
| AgentRegisterFrame
|
|
221
|
+
| RuntimeSchemaAdvertiseFrame
|
|
222
|
+
| HeartbeatFrame
|
|
223
|
+
| SemanticActionFrame
|
|
224
|
+
| ErrorReportFrame
|
|
225
|
+
| AgentConfigAckFrame
|
|
226
|
+
| TaskStartedFrame
|
|
227
|
+
| TaskFinishedFrame
|
|
228
|
+
| ColdStartHandshakeFrame
|
|
229
|
+
| SessionResumeRequest
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
BPP_FRAME_TYPES: dict[str, str] = {
|
|
233
|
+
"AgentConfigUpdateFrame": "agent_config_update",
|
|
234
|
+
"AgentToggleFrame": "agent_toggle",
|
|
235
|
+
"InboundMessageFrame": "inbound_message",
|
|
236
|
+
"PermissionDeniedFrame": "permission_denied",
|
|
237
|
+
"SemanticActionResultFrame": "semantic_action_result",
|
|
238
|
+
"SessionResumeAck": "session.resume_ack",
|
|
239
|
+
"SessionSummaryFrame": "session.summary",
|
|
240
|
+
"ConnectFrame": "connect",
|
|
241
|
+
"AgentRegisterFrame": "agent_register",
|
|
242
|
+
"RuntimeSchemaAdvertiseFrame": "runtime_schema_advertise",
|
|
243
|
+
"HeartbeatFrame": "heartbeat",
|
|
244
|
+
"SemanticActionFrame": "semantic_action",
|
|
245
|
+
"ErrorReportFrame": "error_report",
|
|
246
|
+
"AgentConfigAckFrame": "agent_config_ack",
|
|
247
|
+
"TaskStartedFrame": "task_started",
|
|
248
|
+
"TaskFinishedFrame": "task_finished",
|
|
249
|
+
"ColdStartHandshakeFrame": "cold_start_handshake",
|
|
250
|
+
"SessionResumeRequest": "session.resume",
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
BPP_FRAME_DIRECTIONS: dict[str, BppDirection] = {
|
|
254
|
+
"agent_config_update": "server_to_plugin",
|
|
255
|
+
"agent_toggle": "server_to_plugin",
|
|
256
|
+
"inbound_message": "server_to_plugin",
|
|
257
|
+
"permission_denied": "server_to_plugin",
|
|
258
|
+
"semantic_action_result": "server_to_plugin",
|
|
259
|
+
"session.resume_ack": "server_to_plugin",
|
|
260
|
+
"session.summary": "server_to_plugin",
|
|
261
|
+
"connect": "plugin_to_server",
|
|
262
|
+
"agent_register": "plugin_to_server",
|
|
263
|
+
"runtime_schema_advertise": "plugin_to_server",
|
|
264
|
+
"heartbeat": "plugin_to_server",
|
|
265
|
+
"semantic_action": "plugin_to_server",
|
|
266
|
+
"error_report": "plugin_to_server",
|
|
267
|
+
"agent_config_ack": "plugin_to_server",
|
|
268
|
+
"task_started": "plugin_to_server",
|
|
269
|
+
"task_finished": "plugin_to_server",
|
|
270
|
+
"cold_start_handshake": "plugin_to_server",
|
|
271
|
+
"session.resume": "plugin_to_server",
|
|
272
|
+
}
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
"""Public async Borgee plugin client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
import random
|
|
8
|
+
from collections.abc import AsyncIterator
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Any, Protocol
|
|
11
|
+
|
|
12
|
+
from ._connection import (
|
|
13
|
+
BppConnection,
|
|
14
|
+
ConfigHandler,
|
|
15
|
+
ConfigObserver,
|
|
16
|
+
ConnectionOptions,
|
|
17
|
+
Delivery,
|
|
18
|
+
ServerRequestHandler,
|
|
19
|
+
)
|
|
20
|
+
from .cursor import CursorStore, MemoryCursorStore
|
|
21
|
+
from .errors import ProtocolError
|
|
22
|
+
from .models import (
|
|
23
|
+
ConnectionState,
|
|
24
|
+
CreatedDM,
|
|
25
|
+
FaultReason,
|
|
26
|
+
ReplayMode,
|
|
27
|
+
SentMessage,
|
|
28
|
+
Task,
|
|
29
|
+
TaskOutcome,
|
|
30
|
+
User,
|
|
31
|
+
task_from_wire,
|
|
32
|
+
user_from_wire,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class _ClientTransport(Protocol):
|
|
37
|
+
@property
|
|
38
|
+
def agent_id(self) -> str: ...
|
|
39
|
+
@property
|
|
40
|
+
def state(self) -> ConnectionState: ...
|
|
41
|
+
async def connect(self) -> None: ...
|
|
42
|
+
async def close(self) -> None: ...
|
|
43
|
+
async def perform(self, operation: str, payload: dict[str, Any]) -> Any: ...
|
|
44
|
+
async def report_task_started(self, task_id: str, channel_id: str, subject: str) -> None: ...
|
|
45
|
+
async def report_task_finished(
|
|
46
|
+
self, task_id: str, channel_id: str, outcome: str, reason: str
|
|
47
|
+
) -> None: ...
|
|
48
|
+
def send_typing_nowait(self, channel_id: str) -> None: ...
|
|
49
|
+
def deliveries(self) -> AsyncIterator[Delivery]: ...
|
|
50
|
+
def state_changes(self) -> AsyncIterator[ConnectionState]: ...
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(slots=True)
|
|
54
|
+
class BorgeePluginOptions:
|
|
55
|
+
base_url: str
|
|
56
|
+
api_key: str
|
|
57
|
+
agent_id: str = ""
|
|
58
|
+
plugin_id: str = ""
|
|
59
|
+
cursor_store: CursorStore = field(default_factory=MemoryCursorStore)
|
|
60
|
+
allow_insecure_loopback: bool = False
|
|
61
|
+
replay_mode: ReplayMode = ReplayMode.LATEST_N
|
|
62
|
+
replay_latest_n: int = 200
|
|
63
|
+
config_update_handler: ConfigHandler | None = None
|
|
64
|
+
config_update_observer: ConfigObserver | None = None
|
|
65
|
+
server_request_handler: ServerRequestHandler | None = None
|
|
66
|
+
logger: logging.Logger = field(default_factory=lambda: logging.getLogger("borgee_plugin_sdk"))
|
|
67
|
+
action_timeout: float = 30.0
|
|
68
|
+
config_apply_timeout: float = 5.0
|
|
69
|
+
auto_reconnect: bool = True
|
|
70
|
+
reconnect_base: float = 0.5
|
|
71
|
+
reconnect_max: float = 30.0
|
|
72
|
+
heartbeat_interval: float = 30.0
|
|
73
|
+
random_source: random.Random = field(default_factory=random.Random, repr=False)
|
|
74
|
+
|
|
75
|
+
def __post_init__(self) -> None:
|
|
76
|
+
for name in (
|
|
77
|
+
"action_timeout",
|
|
78
|
+
"config_apply_timeout",
|
|
79
|
+
"reconnect_base",
|
|
80
|
+
"reconnect_max",
|
|
81
|
+
"heartbeat_interval",
|
|
82
|
+
):
|
|
83
|
+
if getattr(self, name) <= 0:
|
|
84
|
+
raise ValueError(f"{name} must be positive")
|
|
85
|
+
|
|
86
|
+
def to_connection_options(self) -> ConnectionOptions:
|
|
87
|
+
return ConnectionOptions(
|
|
88
|
+
base_url=self.base_url,
|
|
89
|
+
api_key=self.api_key,
|
|
90
|
+
agent_id=self.agent_id,
|
|
91
|
+
plugin_id=self.plugin_id,
|
|
92
|
+
cursor_store=self.cursor_store,
|
|
93
|
+
allow_insecure_loopback=self.allow_insecure_loopback,
|
|
94
|
+
replay_mode=self.replay_mode,
|
|
95
|
+
replay_latest_n=self.replay_latest_n,
|
|
96
|
+
config_update_handler=self.config_update_handler,
|
|
97
|
+
config_update_observer=self.config_update_observer,
|
|
98
|
+
server_request_handler=self.server_request_handler,
|
|
99
|
+
logger=self.logger,
|
|
100
|
+
action_timeout=self.action_timeout,
|
|
101
|
+
config_apply_timeout=self.config_apply_timeout,
|
|
102
|
+
auto_reconnect=self.auto_reconnect,
|
|
103
|
+
reconnect_base=self.reconnect_base,
|
|
104
|
+
reconnect_max=self.reconnect_max,
|
|
105
|
+
heartbeat_interval=self.heartbeat_interval,
|
|
106
|
+
random_source=self.random_source,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class BorgeePluginClient:
|
|
111
|
+
def __init__(self, options: BorgeePluginOptions, transport: _ClientTransport | None = None) -> None:
|
|
112
|
+
self._options = options
|
|
113
|
+
self._transport = transport or BppConnection(options.to_connection_options())
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def agent_id(self) -> str:
|
|
117
|
+
return self._transport.agent_id
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def state(self) -> ConnectionState:
|
|
121
|
+
return self._transport.state
|
|
122
|
+
|
|
123
|
+
async def connect(self) -> None:
|
|
124
|
+
await self._transport.connect()
|
|
125
|
+
|
|
126
|
+
async def close(self) -> None:
|
|
127
|
+
await self._transport.close()
|
|
128
|
+
|
|
129
|
+
async def __aenter__(self) -> BorgeePluginClient:
|
|
130
|
+
await self.connect()
|
|
131
|
+
return self
|
|
132
|
+
|
|
133
|
+
async def __aexit__(self, exc_type: object, exc: object, traceback: object) -> None:
|
|
134
|
+
await self.close()
|
|
135
|
+
|
|
136
|
+
def deliveries(self) -> AsyncIterator[Delivery]:
|
|
137
|
+
return self._transport.deliveries()
|
|
138
|
+
|
|
139
|
+
def subscribe(self) -> AsyncIterator[Delivery]:
|
|
140
|
+
return self.deliveries()
|
|
141
|
+
|
|
142
|
+
def state_changes(self) -> AsyncIterator[ConnectionState]:
|
|
143
|
+
return self._transport.state_changes()
|
|
144
|
+
|
|
145
|
+
async def send_message(
|
|
146
|
+
self,
|
|
147
|
+
channel_id: str,
|
|
148
|
+
body: str,
|
|
149
|
+
*,
|
|
150
|
+
reply_to_id: str | None = None,
|
|
151
|
+
) -> SentMessage:
|
|
152
|
+
result = await self._transport.perform(
|
|
153
|
+
"send_message",
|
|
154
|
+
{
|
|
155
|
+
"channel_id": channel_id,
|
|
156
|
+
"content": body,
|
|
157
|
+
"reply_to_id": reply_to_id,
|
|
158
|
+
},
|
|
159
|
+
)
|
|
160
|
+
if not isinstance(result, dict) or not isinstance(result.get("message_id"), str):
|
|
161
|
+
raise ProtocolError("send_message returned an invalid result")
|
|
162
|
+
cursor = result.get("cursor", 0)
|
|
163
|
+
if not isinstance(cursor, int) or isinstance(cursor, bool):
|
|
164
|
+
cursor = 0
|
|
165
|
+
return SentMessage(result["message_id"], cursor)
|
|
166
|
+
|
|
167
|
+
async def edit_message(self, message_id: str, body: str) -> None:
|
|
168
|
+
await self._transport.perform("edit_message", {"message_id": message_id, "content": body})
|
|
169
|
+
|
|
170
|
+
async def delete_message(self, message_id: str) -> None:
|
|
171
|
+
await self._transport.perform("delete_message", {"message_id": message_id})
|
|
172
|
+
|
|
173
|
+
async def react(self, message_id: str, emoji: str) -> None:
|
|
174
|
+
await self._transport.perform("react", {"message_id": message_id, "emoji": emoji, "removed": False})
|
|
175
|
+
|
|
176
|
+
async def unreact(self, message_id: str, emoji: str) -> None:
|
|
177
|
+
await self._transport.perform("react", {"message_id": message_id, "emoji": emoji, "removed": True})
|
|
178
|
+
|
|
179
|
+
async def create_dm(self, user_id: str) -> CreatedDM:
|
|
180
|
+
result = await self._transport.perform("create_dm", {"user_id": user_id})
|
|
181
|
+
if not isinstance(result, dict) or not isinstance(result.get("channel_id"), str):
|
|
182
|
+
raise ProtocolError("create_dm returned an invalid result")
|
|
183
|
+
return CreatedDM(result["channel_id"])
|
|
184
|
+
|
|
185
|
+
async def get_me(self) -> User:
|
|
186
|
+
return user_from_wire(await self._transport.perform("get_me", {}))
|
|
187
|
+
|
|
188
|
+
async def report_task_started(self, task_id: str, channel_id: str, subject: str) -> None:
|
|
189
|
+
await self._transport.report_task_started(task_id, channel_id, subject)
|
|
190
|
+
|
|
191
|
+
async def report_task_finished(
|
|
192
|
+
self,
|
|
193
|
+
task_id: str,
|
|
194
|
+
channel_id: str,
|
|
195
|
+
outcome: TaskOutcome,
|
|
196
|
+
reason: FaultReason | None = None,
|
|
197
|
+
) -> None:
|
|
198
|
+
await self._transport.report_task_finished(
|
|
199
|
+
task_id,
|
|
200
|
+
channel_id,
|
|
201
|
+
outcome.value,
|
|
202
|
+
reason.value if reason is not None else "",
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# ─── Task CRUD (BPP semantic actions) ───────────────────
|
|
206
|
+
|
|
207
|
+
async def create_task(
|
|
208
|
+
self,
|
|
209
|
+
channel_id: str,
|
|
210
|
+
title: str,
|
|
211
|
+
*,
|
|
212
|
+
description: str | None = None,
|
|
213
|
+
assignee_id: str | None = None,
|
|
214
|
+
heartbeat_interval_ms: int | None = None,
|
|
215
|
+
heartbeat_prompt: str | None = None,
|
|
216
|
+
) -> Task:
|
|
217
|
+
payload: dict[str, Any] = {"channel_id": channel_id, "title": title}
|
|
218
|
+
if description is not None:
|
|
219
|
+
payload["description"] = description
|
|
220
|
+
if assignee_id is not None:
|
|
221
|
+
payload["assignee_id"] = assignee_id
|
|
222
|
+
if heartbeat_interval_ms is not None:
|
|
223
|
+
payload["heartbeat_interval_ms"] = heartbeat_interval_ms
|
|
224
|
+
if heartbeat_prompt is not None:
|
|
225
|
+
payload["heartbeat_prompt"] = heartbeat_prompt
|
|
226
|
+
result = await self._transport.perform("create_task", payload)
|
|
227
|
+
return task_from_wire(result)
|
|
228
|
+
|
|
229
|
+
async def list_tasks(
|
|
230
|
+
self,
|
|
231
|
+
*,
|
|
232
|
+
channel_id: str | None = None,
|
|
233
|
+
guild_id: str | None = None,
|
|
234
|
+
) -> list[Task]:
|
|
235
|
+
if (channel_id is None) == (guild_id is None):
|
|
236
|
+
raise ValueError("list_tasks requires exactly one of channel_id or guild_id")
|
|
237
|
+
payload: dict[str, Any] = {}
|
|
238
|
+
if channel_id is not None:
|
|
239
|
+
payload["channel_id"] = channel_id
|
|
240
|
+
if guild_id is not None:
|
|
241
|
+
payload["guild_id"] = guild_id
|
|
242
|
+
result = await self._transport.perform("list_tasks", payload)
|
|
243
|
+
if not isinstance(result, list):
|
|
244
|
+
raise ProtocolError("list_tasks did not return an array")
|
|
245
|
+
return [task_from_wire(item) for item in result]
|
|
246
|
+
|
|
247
|
+
async def get_task(self, task_id: str) -> Task:
|
|
248
|
+
result = await self._transport.perform("get_task", {"task_id": task_id})
|
|
249
|
+
return task_from_wire(result)
|
|
250
|
+
|
|
251
|
+
async def update_task(
|
|
252
|
+
self,
|
|
253
|
+
task_id: str,
|
|
254
|
+
*,
|
|
255
|
+
status: str | None = None,
|
|
256
|
+
assignee_id: str | None = None,
|
|
257
|
+
title: str | None = None,
|
|
258
|
+
heartbeat_interval_ms: int | None = None,
|
|
259
|
+
heartbeat_prompt: str | None = None,
|
|
260
|
+
) -> Task:
|
|
261
|
+
if (
|
|
262
|
+
status is None
|
|
263
|
+
and assignee_id is None
|
|
264
|
+
and title is None
|
|
265
|
+
and heartbeat_interval_ms is None
|
|
266
|
+
and heartbeat_prompt is None
|
|
267
|
+
):
|
|
268
|
+
raise ValueError(
|
|
269
|
+
"update_task requires at least one of: status, assignee_id, title,"
|
|
270
|
+
" heartbeat_interval_ms, heartbeat_prompt"
|
|
271
|
+
)
|
|
272
|
+
payload: dict[str, Any] = {"task_id": task_id}
|
|
273
|
+
if status is not None:
|
|
274
|
+
payload["status"] = status
|
|
275
|
+
if assignee_id is not None:
|
|
276
|
+
payload["assignee_id"] = assignee_id
|
|
277
|
+
if title is not None:
|
|
278
|
+
payload["title"] = title
|
|
279
|
+
if heartbeat_interval_ms is not None:
|
|
280
|
+
payload["heartbeat_interval_ms"] = heartbeat_interval_ms
|
|
281
|
+
if heartbeat_prompt is not None:
|
|
282
|
+
payload["heartbeat_prompt"] = heartbeat_prompt
|
|
283
|
+
result = await self._transport.perform("update_task", payload)
|
|
284
|
+
return task_from_wire(result)
|
|
285
|
+
|
|
286
|
+
# ─── Channel management (BPP semantic actions) ─────────
|
|
287
|
+
|
|
288
|
+
async def list_channels(self) -> list[dict[str, Any]]:
|
|
289
|
+
result = await self._transport.perform("list_channels", {})
|
|
290
|
+
if not isinstance(result, list):
|
|
291
|
+
raise ProtocolError("list_channels did not return an array")
|
|
292
|
+
return result
|
|
293
|
+
|
|
294
|
+
async def create_channel(
|
|
295
|
+
self,
|
|
296
|
+
name: str,
|
|
297
|
+
*,
|
|
298
|
+
topic: str | None = None,
|
|
299
|
+
visibility: str | None = None,
|
|
300
|
+
member_ids: list[str] | None = None,
|
|
301
|
+
) -> dict[str, Any]:
|
|
302
|
+
payload: dict[str, Any] = {"name": name}
|
|
303
|
+
if topic is not None:
|
|
304
|
+
payload["topic"] = topic
|
|
305
|
+
if visibility is not None:
|
|
306
|
+
payload["visibility"] = visibility
|
|
307
|
+
if member_ids is not None:
|
|
308
|
+
payload["member_ids"] = member_ids
|
|
309
|
+
result = await self._transport.perform("create_channel", payload)
|
|
310
|
+
if not isinstance(result, dict):
|
|
311
|
+
raise ProtocolError("create_channel did not return an object")
|
|
312
|
+
return result
|
|
313
|
+
|
|
314
|
+
async def update_channel(
|
|
315
|
+
self,
|
|
316
|
+
channel_id: str,
|
|
317
|
+
*,
|
|
318
|
+
name: str | None = None,
|
|
319
|
+
visibility: str | None = None,
|
|
320
|
+
archived: bool | None = None,
|
|
321
|
+
) -> dict[str, Any]:
|
|
322
|
+
payload: dict[str, Any] = {"channel_id": channel_id}
|
|
323
|
+
if name is not None:
|
|
324
|
+
payload["name"] = name
|
|
325
|
+
if visibility is not None:
|
|
326
|
+
payload["visibility"] = visibility
|
|
327
|
+
if archived is not None:
|
|
328
|
+
payload["archived"] = archived
|
|
329
|
+
result = await self._transport.perform("update_channel", payload)
|
|
330
|
+
if not isinstance(result, dict):
|
|
331
|
+
raise ProtocolError("update_channel did not return an object")
|
|
332
|
+
return result
|
|
333
|
+
|
|
334
|
+
async def set_topic(self, channel_id: str, topic: str) -> None:
|
|
335
|
+
await self._transport.perform("set_topic", {"channel_id": channel_id, "topic": topic})
|
|
336
|
+
|
|
337
|
+
async def delete_channel(self, channel_id: str) -> None:
|
|
338
|
+
await self._transport.perform("delete_channel", {"channel_id": channel_id})
|
|
339
|
+
|
|
340
|
+
async def read_history(
|
|
341
|
+
self,
|
|
342
|
+
channel_id: str,
|
|
343
|
+
*,
|
|
344
|
+
limit: int | None = None,
|
|
345
|
+
before: str | None = None,
|
|
346
|
+
after: str | None = None,
|
|
347
|
+
) -> list[dict[str, Any]]:
|
|
348
|
+
payload: dict[str, Any] = {"channel_id": channel_id}
|
|
349
|
+
if limit is not None:
|
|
350
|
+
payload["limit"] = limit
|
|
351
|
+
if before is not None:
|
|
352
|
+
payload["before"] = before
|
|
353
|
+
if after is not None:
|
|
354
|
+
payload["after"] = after
|
|
355
|
+
result = await self._transport.perform("read_channel_history", payload)
|
|
356
|
+
if not isinstance(result, list):
|
|
357
|
+
raise ProtocolError("read_channel_history did not return an array")
|
|
358
|
+
return result
|
|
359
|
+
|
|
360
|
+
def start_typing(self, channel_id: str) -> _TypingHandle:
|
|
361
|
+
"""Begin a typing indicator loop. Call stop() on the handle to end it."""
|
|
362
|
+
handle = _TypingHandle(self._transport, channel_id)
|
|
363
|
+
handle.start()
|
|
364
|
+
return handle
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
class _TypingHandle:
|
|
368
|
+
"""Sends typing frames every 2s until stop() is called."""
|
|
369
|
+
|
|
370
|
+
_INTERVAL = 2.0
|
|
371
|
+
|
|
372
|
+
def __init__(self, transport: _ClientTransport, channel_id: str) -> None:
|
|
373
|
+
self._transport = transport
|
|
374
|
+
self._channel_id = channel_id
|
|
375
|
+
self._task: asyncio.Task[None] | None = None
|
|
376
|
+
|
|
377
|
+
def start(self) -> None:
|
|
378
|
+
if self._task is None or self._task.done():
|
|
379
|
+
self._task = asyncio.create_task(self._loop())
|
|
380
|
+
|
|
381
|
+
async def _loop(self) -> None:
|
|
382
|
+
try:
|
|
383
|
+
while True:
|
|
384
|
+
self._transport.send_typing_nowait(self._channel_id)
|
|
385
|
+
await asyncio.sleep(self._INTERVAL)
|
|
386
|
+
except asyncio.CancelledError:
|
|
387
|
+
pass
|
|
388
|
+
|
|
389
|
+
def stop(self) -> None:
|
|
390
|
+
"""Stop emitting typing indicators."""
|
|
391
|
+
if self._task is not None:
|
|
392
|
+
self._task.cancel()
|
|
393
|
+
self._task = None
|
|
394
|
+
|
|
395
|
+
def __del__(self) -> None:
|
|
396
|
+
self.stop()
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def create_borgee_plugin(options: BorgeePluginOptions) -> BorgeePluginClient:
|
|
400
|
+
return BorgeePluginClient(options)
|