astra-plugin-sdk 0.1.0__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.
- astra_plugin_sdk-0.1.0/PKG-INFO +74 -0
- astra_plugin_sdk-0.1.0/README.md +61 -0
- astra_plugin_sdk-0.1.0/astra_plugin_sdk/__init__.py +8 -0
- astra_plugin_sdk-0.1.0/astra_plugin_sdk/decorators.py +318 -0
- astra_plugin_sdk-0.1.0/astra_plugin_sdk/host_client.py +93 -0
- astra_plugin_sdk-0.1.0/astra_plugin_sdk/plugin.py +420 -0
- astra_plugin_sdk-0.1.0/astra_plugin_sdk/proto/__init__.py +6 -0
- astra_plugin_sdk-0.1.0/astra_plugin_sdk/proto/plugin_pb2.py +124 -0
- astra_plugin_sdk-0.1.0/astra_plugin_sdk/proto/plugin_pb2_grpc.py +1142 -0
- astra_plugin_sdk-0.1.0/astra_plugin_sdk.egg-info/PKG-INFO +74 -0
- astra_plugin_sdk-0.1.0/astra_plugin_sdk.egg-info/SOURCES.txt +14 -0
- astra_plugin_sdk-0.1.0/astra_plugin_sdk.egg-info/dependency_links.txt +1 -0
- astra_plugin_sdk-0.1.0/astra_plugin_sdk.egg-info/requires.txt +3 -0
- astra_plugin_sdk-0.1.0/astra_plugin_sdk.egg-info/top_level.txt +1 -0
- astra_plugin_sdk-0.1.0/pyproject.toml +23 -0
- astra_plugin_sdk-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: astra-plugin-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for building Astra plugins
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/mihailinl/AstraPlugins
|
|
7
|
+
Project-URL: Repository, https://github.com/mihailinl/AstraPlugins/tree/master/astra-plugin-sdk-python
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: grpcio>=1.60.0
|
|
11
|
+
Requires-Dist: grpcio-tools>=1.60.0
|
|
12
|
+
Requires-Dist: protobuf>=4.25.0
|
|
13
|
+
|
|
14
|
+
# Astra Plugin SDK (Python)
|
|
15
|
+
|
|
16
|
+
Build plugins for [Astra](https://github.com/astra-assistant) in Python.
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install astra-plugin-sdk
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from astra_plugin_sdk import Plugin
|
|
28
|
+
|
|
29
|
+
class MyPlugin(Plugin):
|
|
30
|
+
async def list_tools(self):
|
|
31
|
+
return [{
|
|
32
|
+
"name": "hello",
|
|
33
|
+
"description": "Say hello",
|
|
34
|
+
"parameters_json": '{"type": "object", "properties": {}}',
|
|
35
|
+
}]
|
|
36
|
+
|
|
37
|
+
async def call_tool(self, name, arguments_json):
|
|
38
|
+
if name == "hello":
|
|
39
|
+
return {"success": True, "result": "Hello from the plugin!"}
|
|
40
|
+
return {"success": False, "error": f"Unknown tool: {name}"}
|
|
41
|
+
|
|
42
|
+
if __name__ == "__main__":
|
|
43
|
+
MyPlugin().run()
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Capabilities
|
|
47
|
+
|
|
48
|
+
Override the methods you need:
|
|
49
|
+
|
|
50
|
+
- **Tools**: `list_tools()`, `call_tool(name, args)`
|
|
51
|
+
- **TTS**: `tts_list_voices()`, `tts_synthesize(text, voice_id, speed, pitch)`
|
|
52
|
+
- **STT**: `stt_get_languages()`
|
|
53
|
+
- **AI Provider**: `ai_get_models()`
|
|
54
|
+
- **Actions**: `get_action_types()`, `execute_action(type, params)`
|
|
55
|
+
- **Triggers**: `get_trigger_types()`
|
|
56
|
+
- **Lifecycle**: `on_config_changed(config)`, `on_shutdown()`, `health_check()`
|
|
57
|
+
|
|
58
|
+
## Host Client
|
|
59
|
+
|
|
60
|
+
Access daemon services from your plugin:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
class MyPlugin(Plugin):
|
|
64
|
+
async def on_config_changed(self, config):
|
|
65
|
+
# Log to daemon
|
|
66
|
+
await self.host.log("info", f"Config updated: {config}")
|
|
67
|
+
|
|
68
|
+
# Fire a trigger
|
|
69
|
+
await self.host.fire_trigger("my_trigger", '{"key": "value"}')
|
|
70
|
+
|
|
71
|
+
# Get daemon info
|
|
72
|
+
info = await self.host.get_daemon_info()
|
|
73
|
+
print(f"Daemon version: {info.version}")
|
|
74
|
+
```
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Astra Plugin SDK (Python)
|
|
2
|
+
|
|
3
|
+
Build plugins for [Astra](https://github.com/astra-assistant) in Python.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install astra-plugin-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from astra_plugin_sdk import Plugin
|
|
15
|
+
|
|
16
|
+
class MyPlugin(Plugin):
|
|
17
|
+
async def list_tools(self):
|
|
18
|
+
return [{
|
|
19
|
+
"name": "hello",
|
|
20
|
+
"description": "Say hello",
|
|
21
|
+
"parameters_json": '{"type": "object", "properties": {}}',
|
|
22
|
+
}]
|
|
23
|
+
|
|
24
|
+
async def call_tool(self, name, arguments_json):
|
|
25
|
+
if name == "hello":
|
|
26
|
+
return {"success": True, "result": "Hello from the plugin!"}
|
|
27
|
+
return {"success": False, "error": f"Unknown tool: {name}"}
|
|
28
|
+
|
|
29
|
+
if __name__ == "__main__":
|
|
30
|
+
MyPlugin().run()
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Capabilities
|
|
34
|
+
|
|
35
|
+
Override the methods you need:
|
|
36
|
+
|
|
37
|
+
- **Tools**: `list_tools()`, `call_tool(name, args)`
|
|
38
|
+
- **TTS**: `tts_list_voices()`, `tts_synthesize(text, voice_id, speed, pitch)`
|
|
39
|
+
- **STT**: `stt_get_languages()`
|
|
40
|
+
- **AI Provider**: `ai_get_models()`
|
|
41
|
+
- **Actions**: `get_action_types()`, `execute_action(type, params)`
|
|
42
|
+
- **Triggers**: `get_trigger_types()`
|
|
43
|
+
- **Lifecycle**: `on_config_changed(config)`, `on_shutdown()`, `health_check()`
|
|
44
|
+
|
|
45
|
+
## Host Client
|
|
46
|
+
|
|
47
|
+
Access daemon services from your plugin:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
class MyPlugin(Plugin):
|
|
51
|
+
async def on_config_changed(self, config):
|
|
52
|
+
# Log to daemon
|
|
53
|
+
await self.host.log("info", f"Config updated: {config}")
|
|
54
|
+
|
|
55
|
+
# Fire a trigger
|
|
56
|
+
await self.host.fire_trigger("my_trigger", '{"key": "value"}')
|
|
57
|
+
|
|
58
|
+
# Get daemon info
|
|
59
|
+
info = await self.host.get_daemon_info()
|
|
60
|
+
print(f"Daemon version: {info.version}")
|
|
61
|
+
```
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Astra Plugin SDK — build plugins for Astra in Python."""
|
|
2
|
+
|
|
3
|
+
from astra_plugin_sdk.plugin import Plugin
|
|
4
|
+
from astra_plugin_sdk.host_client import HostClient
|
|
5
|
+
from astra_plugin_sdk.decorators import tool, action, trigger, Field
|
|
6
|
+
|
|
7
|
+
__all__ = ["Plugin", "HostClient", "tool", "action", "trigger", "Field"]
|
|
8
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"""Decorators and helpers for declarative plugin definitions.
|
|
2
|
+
|
|
3
|
+
Use ``@tool``, ``@action``, and ``@trigger`` to define capabilities with
|
|
4
|
+
minimal boilerplate. The ``Field`` class provides builder methods for
|
|
5
|
+
action/trigger field definitions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import inspect
|
|
9
|
+
import json
|
|
10
|
+
import typing
|
|
11
|
+
from typing import Any, Literal, Optional, Union, get_type_hints
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
# Type-hint -> JSON Schema mapping
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
_PY_TO_JSON_TYPE = {
|
|
19
|
+
str: "string",
|
|
20
|
+
int: "integer",
|
|
21
|
+
float: "number",
|
|
22
|
+
bool: "boolean",
|
|
23
|
+
list: "array",
|
|
24
|
+
dict: "object",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _type_to_schema(hint: Any) -> dict:
|
|
29
|
+
"""Convert a Python type hint to a JSON Schema fragment."""
|
|
30
|
+
# Plain types
|
|
31
|
+
if hint in _PY_TO_JSON_TYPE:
|
|
32
|
+
return {"type": _PY_TO_JSON_TYPE[hint]}
|
|
33
|
+
|
|
34
|
+
origin = typing.get_origin(hint)
|
|
35
|
+
args = typing.get_args(hint)
|
|
36
|
+
|
|
37
|
+
# Literal["a", "b"] -> enum
|
|
38
|
+
if origin is Literal:
|
|
39
|
+
return {"type": "string", "enum": list(args)}
|
|
40
|
+
|
|
41
|
+
# Optional[X] (Union[X, None])
|
|
42
|
+
if origin is Union:
|
|
43
|
+
non_none = [a for a in args if a is not type(None)]
|
|
44
|
+
if len(non_none) == 1:
|
|
45
|
+
return _type_to_schema(non_none[0])
|
|
46
|
+
|
|
47
|
+
# list[str] etc.
|
|
48
|
+
if origin is list:
|
|
49
|
+
schema: dict = {"type": "array"}
|
|
50
|
+
if args:
|
|
51
|
+
schema["items"] = _type_to_schema(args[0])
|
|
52
|
+
return schema
|
|
53
|
+
|
|
54
|
+
# Fallback
|
|
55
|
+
return {"type": "string"}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _build_json_schema(fn: Any) -> str:
|
|
59
|
+
"""Build a JSON Schema string from a function's type hints."""
|
|
60
|
+
try:
|
|
61
|
+
hints = get_type_hints(fn)
|
|
62
|
+
except Exception:
|
|
63
|
+
hints = {}
|
|
64
|
+
|
|
65
|
+
sig = inspect.signature(fn)
|
|
66
|
+
properties: dict[str, dict] = {}
|
|
67
|
+
required: list[str] = []
|
|
68
|
+
|
|
69
|
+
for name, param in sig.parameters.items():
|
|
70
|
+
if name == "self":
|
|
71
|
+
continue
|
|
72
|
+
hint = hints.get(name, str) # default to string
|
|
73
|
+
prop = _type_to_schema(hint)
|
|
74
|
+
|
|
75
|
+
# Use parameter name as description placeholder if no docstring parsing
|
|
76
|
+
properties[name] = prop
|
|
77
|
+
|
|
78
|
+
# Required if no default
|
|
79
|
+
if param.default is inspect.Parameter.empty:
|
|
80
|
+
origin = typing.get_origin(hint)
|
|
81
|
+
args = typing.get_args(hint)
|
|
82
|
+
is_optional = (
|
|
83
|
+
origin is Union
|
|
84
|
+
and type(None) in args
|
|
85
|
+
)
|
|
86
|
+
if not is_optional:
|
|
87
|
+
required.append(name)
|
|
88
|
+
|
|
89
|
+
schema = {"type": "object", "properties": properties}
|
|
90
|
+
if required:
|
|
91
|
+
schema["required"] = required
|
|
92
|
+
return json.dumps(schema)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# ---------------------------------------------------------------------------
|
|
96
|
+
# Decorators
|
|
97
|
+
# ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
def tool(description: str):
|
|
100
|
+
"""Mark a method as a plugin tool.
|
|
101
|
+
|
|
102
|
+
The decorated method's type hints are used to auto-generate JSON Schema
|
|
103
|
+
for the tool parameters. The return value is automatically wrapped in
|
|
104
|
+
``{"success": True, "result": ...}`` by the SDK.
|
|
105
|
+
|
|
106
|
+
Example::
|
|
107
|
+
|
|
108
|
+
@tool("Count words in text")
|
|
109
|
+
async def word_count(self, text: str):
|
|
110
|
+
return {"words": len(text.split())}
|
|
111
|
+
"""
|
|
112
|
+
def decorator(fn):
|
|
113
|
+
fn._astra_tool_meta = {
|
|
114
|
+
"name": fn.__name__,
|
|
115
|
+
"description": description,
|
|
116
|
+
"parameters_json": _build_json_schema(fn),
|
|
117
|
+
}
|
|
118
|
+
return fn
|
|
119
|
+
return decorator
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def action(
|
|
123
|
+
label: str,
|
|
124
|
+
*,
|
|
125
|
+
icon_svg: str = "",
|
|
126
|
+
fields: list[dict] | None = None,
|
|
127
|
+
ai_available: bool = False,
|
|
128
|
+
ai_description: str = "",
|
|
129
|
+
ai_primary_field: str = "",
|
|
130
|
+
):
|
|
131
|
+
"""Mark a method as a plugin action type.
|
|
132
|
+
|
|
133
|
+
Example::
|
|
134
|
+
|
|
135
|
+
@action("Transform Text", fields=[
|
|
136
|
+
Field.dropdown("op", "Operation", options=["upper", "lower"]),
|
|
137
|
+
])
|
|
138
|
+
async def transform_text(self, op: str, input_text: str):
|
|
139
|
+
...
|
|
140
|
+
"""
|
|
141
|
+
def decorator(fn):
|
|
142
|
+
fn._astra_action_meta = {
|
|
143
|
+
"type": fn.__name__,
|
|
144
|
+
"label": label,
|
|
145
|
+
"icon_svg": icon_svg,
|
|
146
|
+
"fields": fields or [],
|
|
147
|
+
"ai_available": ai_available,
|
|
148
|
+
"ai_description": ai_description,
|
|
149
|
+
"ai_primary_field": ai_primary_field,
|
|
150
|
+
}
|
|
151
|
+
return fn
|
|
152
|
+
return decorator
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def trigger(
|
|
156
|
+
label: str,
|
|
157
|
+
*,
|
|
158
|
+
icon_svg: str = "",
|
|
159
|
+
fields: list[dict] | None = None,
|
|
160
|
+
):
|
|
161
|
+
"""Mark a method as a plugin trigger type definition.
|
|
162
|
+
|
|
163
|
+
The method itself is not called automatically — it just holds metadata.
|
|
164
|
+
Use ``self.fire_trigger(...)`` to fire the trigger from a background task.
|
|
165
|
+
|
|
166
|
+
Example::
|
|
167
|
+
|
|
168
|
+
@trigger("Scheduled Time", fields=[
|
|
169
|
+
Field.text("time", "Time", default="09:00", placeholder="HH:MM"),
|
|
170
|
+
])
|
|
171
|
+
def on_time(self):
|
|
172
|
+
pass
|
|
173
|
+
"""
|
|
174
|
+
def decorator(fn):
|
|
175
|
+
fn._astra_trigger_meta = {
|
|
176
|
+
"type": fn.__name__,
|
|
177
|
+
"label": label,
|
|
178
|
+
"icon_svg": icon_svg,
|
|
179
|
+
"fields": fields or [],
|
|
180
|
+
}
|
|
181
|
+
return fn
|
|
182
|
+
return decorator
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# ---------------------------------------------------------------------------
|
|
186
|
+
# Field builder
|
|
187
|
+
# ---------------------------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
class Field:
|
|
190
|
+
"""Builder for action/trigger field definitions.
|
|
191
|
+
|
|
192
|
+
Each static method returns a dict matching the proto ``FieldDefinitionMsg``
|
|
193
|
+
structure, ready to pass into ``@action(fields=[...])`` or
|
|
194
|
+
``@trigger(fields=[...])``.
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
@staticmethod
|
|
198
|
+
def text(
|
|
199
|
+
id: str,
|
|
200
|
+
label: str,
|
|
201
|
+
*,
|
|
202
|
+
placeholder: str = "",
|
|
203
|
+
default: str = "",
|
|
204
|
+
description: str = "",
|
|
205
|
+
conditions: list[dict] | None = None,
|
|
206
|
+
) -> dict:
|
|
207
|
+
return {
|
|
208
|
+
"id": id, "label": label, "field_type": "text",
|
|
209
|
+
"placeholder": placeholder, "default_value": default,
|
|
210
|
+
"description": description, "conditions": conditions or [],
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
@staticmethod
|
|
214
|
+
def textarea(
|
|
215
|
+
id: str,
|
|
216
|
+
label: str,
|
|
217
|
+
*,
|
|
218
|
+
placeholder: str = "",
|
|
219
|
+
default: str = "",
|
|
220
|
+
description: str = "",
|
|
221
|
+
conditions: list[dict] | None = None,
|
|
222
|
+
) -> dict:
|
|
223
|
+
return {
|
|
224
|
+
"id": id, "label": label, "field_type": "textarea",
|
|
225
|
+
"placeholder": placeholder, "default_value": default,
|
|
226
|
+
"description": description, "conditions": conditions or [],
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
@staticmethod
|
|
230
|
+
def textarea_with_variables(
|
|
231
|
+
id: str,
|
|
232
|
+
label: str,
|
|
233
|
+
*,
|
|
234
|
+
placeholder: str = "",
|
|
235
|
+
default: str = "",
|
|
236
|
+
description: str = "",
|
|
237
|
+
conditions: list[dict] | None = None,
|
|
238
|
+
) -> dict:
|
|
239
|
+
return {
|
|
240
|
+
"id": id, "label": label, "field_type": "textarea_with_variables",
|
|
241
|
+
"placeholder": placeholder, "default_value": default,
|
|
242
|
+
"description": description, "conditions": conditions or [],
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
@staticmethod
|
|
246
|
+
def dropdown(
|
|
247
|
+
id: str,
|
|
248
|
+
label: str,
|
|
249
|
+
*,
|
|
250
|
+
options: list,
|
|
251
|
+
default: str = "",
|
|
252
|
+
description: str = "",
|
|
253
|
+
conditions: list[dict] | None = None,
|
|
254
|
+
) -> dict:
|
|
255
|
+
"""Create a dropdown field.
|
|
256
|
+
|
|
257
|
+
``options`` accepts:
|
|
258
|
+
- ``[("value", "Label"), ...]`` — tuple pairs
|
|
259
|
+
- ``[{"value": ..., "label": ...}, ...]`` — explicit dicts
|
|
260
|
+
- ``["value1", "value2"]`` — strings (value = label)
|
|
261
|
+
"""
|
|
262
|
+
normalized = []
|
|
263
|
+
for opt in options:
|
|
264
|
+
if isinstance(opt, dict):
|
|
265
|
+
normalized.append(opt)
|
|
266
|
+
elif isinstance(opt, (tuple, list)) and len(opt) == 2:
|
|
267
|
+
normalized.append({"value": opt[0], "label": opt[1]})
|
|
268
|
+
else:
|
|
269
|
+
normalized.append({"value": str(opt), "label": str(opt)})
|
|
270
|
+
return {
|
|
271
|
+
"id": id, "label": label, "field_type": "dropdown",
|
|
272
|
+
"options": normalized, "default_value": default,
|
|
273
|
+
"description": description, "conditions": conditions or [],
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
@staticmethod
|
|
277
|
+
def number(
|
|
278
|
+
id: str,
|
|
279
|
+
label: str,
|
|
280
|
+
*,
|
|
281
|
+
min: float | None = None,
|
|
282
|
+
max: float | None = None,
|
|
283
|
+
step: float | None = None,
|
|
284
|
+
default: str = "",
|
|
285
|
+
description: str = "",
|
|
286
|
+
conditions: list[dict] | None = None,
|
|
287
|
+
) -> dict:
|
|
288
|
+
return {
|
|
289
|
+
"id": id, "label": label, "field_type": "number",
|
|
290
|
+
"default_value": default, "description": description,
|
|
291
|
+
"has_min": min is not None,
|
|
292
|
+
"has_max": max is not None,
|
|
293
|
+
"has_step": step is not None,
|
|
294
|
+
"min": float(min) if min is not None else 0.0,
|
|
295
|
+
"max": float(max) if max is not None else 0.0,
|
|
296
|
+
"step": float(step) if step is not None else 0.0,
|
|
297
|
+
"conditions": conditions or [],
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
@staticmethod
|
|
301
|
+
def toggle(
|
|
302
|
+
id: str,
|
|
303
|
+
label: str,
|
|
304
|
+
*,
|
|
305
|
+
default: bool = False,
|
|
306
|
+
description: str = "",
|
|
307
|
+
conditions: list[dict] | None = None,
|
|
308
|
+
) -> dict:
|
|
309
|
+
return {
|
|
310
|
+
"id": id, "label": label, "field_type": "toggle",
|
|
311
|
+
"default_value": "true" if default else "false",
|
|
312
|
+
"description": description, "conditions": conditions or [],
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
@staticmethod
|
|
316
|
+
def condition(field_id: str, operator: str, value: str = "") -> dict:
|
|
317
|
+
"""Build a field visibility condition."""
|
|
318
|
+
return {"field_id": field_id, "operator": operator, "value": value}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""HostClient — plugin-side gRPC client for calling the Astra daemon."""
|
|
2
|
+
|
|
3
|
+
import grpc
|
|
4
|
+
|
|
5
|
+
from astra_plugin_sdk.proto import plugin_pb2, plugin_pb2_grpc
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class HostClient:
|
|
9
|
+
"""Client for calling daemon services from a plugin."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, daemon_addr: str, plugin_id: str):
|
|
12
|
+
self.daemon_addr = daemon_addr
|
|
13
|
+
self.plugin_id = plugin_id
|
|
14
|
+
self._channel: grpc.aio.Channel | None = None
|
|
15
|
+
self._stub: plugin_pb2_grpc.PluginHostServiceStub | None = None
|
|
16
|
+
|
|
17
|
+
async def connect(self):
|
|
18
|
+
"""Connect to the daemon's PluginHostService."""
|
|
19
|
+
self._channel = grpc.aio.insecure_channel(self.daemon_addr)
|
|
20
|
+
self._stub = plugin_pb2_grpc.PluginHostServiceStub(self._channel)
|
|
21
|
+
|
|
22
|
+
async def register(
|
|
23
|
+
self, port: int, capabilities: list[str]
|
|
24
|
+
) -> plugin_pb2.PluginRegisterResponse:
|
|
25
|
+
"""Register this plugin with the daemon."""
|
|
26
|
+
return await self._stub.Register(
|
|
27
|
+
plugin_pb2.PluginRegisterRequest(
|
|
28
|
+
plugin_id=self.plugin_id,
|
|
29
|
+
port=port,
|
|
30
|
+
capabilities=capabilities,
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
async def fire_trigger(self, trigger_type: str, payload_json: str = "{}"):
|
|
35
|
+
"""Fire a trigger (for trigger plugins)."""
|
|
36
|
+
await self._stub.FireTrigger(
|
|
37
|
+
plugin_pb2.PluginFireTriggerRequest(
|
|
38
|
+
trigger_type=trigger_type,
|
|
39
|
+
payload_json=payload_json,
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
async def log(self, level: str, message: str):
|
|
44
|
+
"""Log a message to the daemon's log buffer."""
|
|
45
|
+
await self._stub.PluginLog(
|
|
46
|
+
plugin_pb2.PluginLogRequest(
|
|
47
|
+
plugin_id=self.plugin_id,
|
|
48
|
+
level=level,
|
|
49
|
+
message=message,
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
async def get_config(self) -> str:
|
|
54
|
+
"""Get this plugin's current config from the daemon."""
|
|
55
|
+
response = await self._stub.GetPluginSelfConfig(
|
|
56
|
+
plugin_pb2.PluginSelfIdRequest(plugin_id=self.plugin_id)
|
|
57
|
+
)
|
|
58
|
+
return response.config_json
|
|
59
|
+
|
|
60
|
+
async def get_daemon_info(self) -> plugin_pb2.PluginDaemonInfoResponse:
|
|
61
|
+
"""Get daemon info (version, state, port)."""
|
|
62
|
+
return await self._stub.GetDaemonInfo(plugin_pb2.Empty())
|
|
63
|
+
|
|
64
|
+
async def subscribe_events(self, event_types: list[str] | None = None):
|
|
65
|
+
"""Subscribe to daemon events. Returns an async iterator."""
|
|
66
|
+
return self._stub.SubscribeEvents(
|
|
67
|
+
plugin_pb2.PluginEventFilter(
|
|
68
|
+
plugin_id=self.plugin_id,
|
|
69
|
+
event_types=event_types or [],
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
async def set_variable(self, name: str, value: str, scope: str = "session"):
|
|
74
|
+
"""Set a variable in the daemon's variable context.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
name: Variable name.
|
|
78
|
+
value: Variable value.
|
|
79
|
+
scope: "session" (default, cleared on restart) or "persistent" (saved to disk).
|
|
80
|
+
"""
|
|
81
|
+
await self._stub.SetVariable(
|
|
82
|
+
plugin_pb2.PluginSetVariableRequest(
|
|
83
|
+
plugin_id=self.plugin_id,
|
|
84
|
+
name=name,
|
|
85
|
+
value=value,
|
|
86
|
+
scope=scope,
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
async def close(self):
|
|
91
|
+
"""Close the gRPC channel."""
|
|
92
|
+
if self._channel:
|
|
93
|
+
await self._channel.close()
|