@elizaos/interop 2.0.0-alpha
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.
- package/LICENSE +21 -0
- package/README.md +436 -0
- package/dist/packages/interop/tsconfig.tsbuildinfo +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/typescript/index.d.ts +33 -0
- package/dist/typescript/index.d.ts.map +1 -0
- package/dist/typescript/index.js +121 -0
- package/dist/typescript/python-bridge.d.ts +80 -0
- package/dist/typescript/python-bridge.d.ts.map +1 -0
- package/dist/typescript/python-bridge.js +334 -0
- package/dist/typescript/types.d.ts +301 -0
- package/dist/typescript/types.d.ts.map +1 -0
- package/dist/typescript/types.js +10 -0
- package/dist/typescript/wasm-loader.d.ts +32 -0
- package/dist/typescript/wasm-loader.d.ts.map +1 -0
- package/dist/typescript/wasm-loader.js +269 -0
- package/package.json +43 -0
- package/python/__init__.py +50 -0
- package/python/__pycache__/__init__.cpython-313.pyc +0 -0
- package/python/__pycache__/rust_ffi.cpython-313.pyc +0 -0
- package/python/__pycache__/ts_bridge.cpython-313.pyc +0 -0
- package/python/__pycache__/wasm_loader.cpython-313.pyc +0 -0
- package/python/bridge_server.py +505 -0
- package/python/rust_ffi.py +418 -0
- package/python/tests/__init__.py +1 -0
- package/python/tests/__pycache__/__init__.cpython-313.pyc +0 -0
- package/python/tests/__pycache__/test_bridge_server.cpython-313-pytest-9.0.2.pyc +0 -0
- package/python/tests/__pycache__/test_interop.cpython-313-pytest-9.0.2.pyc +0 -0
- package/python/tests/__pycache__/test_rust_ffi.cpython-313-pytest-9.0.2.pyc +0 -0
- package/python/tests/__pycache__/test_rust_ffi_loader.cpython-313-pytest-9.0.2.pyc +0 -0
- package/python/tests/test_bridge_server.py +525 -0
- package/python/tests/test_interop.py +319 -0
- package/python/tests/test_rust_ffi.py +352 -0
- package/python/tests/test_rust_ffi_loader.py +71 -0
- package/python/ts_bridge.py +452 -0
- package/python/ts_bridge_runner.mjs +284 -0
- package/python/wasm_loader.py +517 -0
- package/rust/ffi_exports.rs +362 -0
- package/rust/mod.rs +21 -0
- package/rust/py_loader.rs +412 -0
- package/rust/ts_loader.rs +467 -0
- package/rust/wasm_plugin.rs +320 -0
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
//! Python Plugin Loader for elizaOS Rust Runtime
|
|
2
|
+
//!
|
|
3
|
+
//! This module provides utilities for loading Python plugins into the Rust runtime
|
|
4
|
+
//! via subprocess IPC communication.
|
|
5
|
+
|
|
6
|
+
use anyhow::{anyhow, Result};
|
|
7
|
+
use serde::{Deserialize, Serialize};
|
|
8
|
+
use std::collections::HashMap;
|
|
9
|
+
use std::io::{BufRead, BufReader, Write};
|
|
10
|
+
use std::path::Path;
|
|
11
|
+
use std::process::{Child, Command, Stdio};
|
|
12
|
+
use std::sync::atomic::{AtomicU64, Ordering};
|
|
13
|
+
use std::sync::Mutex;
|
|
14
|
+
|
|
15
|
+
/// Plugin manifest from Python
|
|
16
|
+
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
17
|
+
#[serde(rename_all = "camelCase")]
|
|
18
|
+
pub struct PythonManifest {
|
|
19
|
+
pub name: String,
|
|
20
|
+
pub description: String,
|
|
21
|
+
#[serde(default)]
|
|
22
|
+
pub version: Option<String>,
|
|
23
|
+
#[serde(default)]
|
|
24
|
+
pub language: Option<String>,
|
|
25
|
+
#[serde(default)]
|
|
26
|
+
pub config: Option<HashMap<String, serde_json::Value>>,
|
|
27
|
+
#[serde(default)]
|
|
28
|
+
pub dependencies: Option<Vec<String>>,
|
|
29
|
+
#[serde(default)]
|
|
30
|
+
pub actions: Vec<ActionManifest>,
|
|
31
|
+
#[serde(default)]
|
|
32
|
+
pub providers: Vec<ProviderManifest>,
|
|
33
|
+
#[serde(default)]
|
|
34
|
+
pub evaluators: Vec<EvaluatorManifest>,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
38
|
+
#[serde(rename_all = "camelCase")]
|
|
39
|
+
pub struct ActionManifest {
|
|
40
|
+
pub name: String,
|
|
41
|
+
#[serde(default)]
|
|
42
|
+
pub description: Option<String>,
|
|
43
|
+
#[serde(default)]
|
|
44
|
+
pub similes: Option<Vec<String>>,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
48
|
+
#[serde(rename_all = "camelCase")]
|
|
49
|
+
pub struct ProviderManifest {
|
|
50
|
+
pub name: String,
|
|
51
|
+
#[serde(default)]
|
|
52
|
+
pub description: Option<String>,
|
|
53
|
+
#[serde(default)]
|
|
54
|
+
pub dynamic: Option<bool>,
|
|
55
|
+
#[serde(default)]
|
|
56
|
+
pub position: Option<i32>,
|
|
57
|
+
#[serde(default)]
|
|
58
|
+
pub private: Option<bool>,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
62
|
+
#[serde(rename_all = "camelCase")]
|
|
63
|
+
pub struct EvaluatorManifest {
|
|
64
|
+
pub name: String,
|
|
65
|
+
#[serde(default)]
|
|
66
|
+
pub description: Option<String>,
|
|
67
|
+
#[serde(rename = "alwaysRun", default)]
|
|
68
|
+
pub always_run: Option<bool>,
|
|
69
|
+
#[serde(default)]
|
|
70
|
+
pub similes: Option<Vec<String>>,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/// Action result from Python
|
|
74
|
+
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
|
75
|
+
#[serde(rename_all = "camelCase")]
|
|
76
|
+
pub struct ActionResult {
|
|
77
|
+
pub success: bool,
|
|
78
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
79
|
+
pub text: Option<String>,
|
|
80
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
81
|
+
pub error: Option<String>,
|
|
82
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
83
|
+
pub data: Option<HashMap<String, serde_json::Value>>,
|
|
84
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
85
|
+
pub values: Option<HashMap<String, serde_json::Value>>,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// Provider result from Python
|
|
89
|
+
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
|
90
|
+
#[serde(rename_all = "camelCase")]
|
|
91
|
+
pub struct ProviderResult {
|
|
92
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
93
|
+
pub text: Option<String>,
|
|
94
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
95
|
+
pub values: Option<HashMap<String, serde_json::Value>>,
|
|
96
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
97
|
+
pub data: Option<HashMap<String, serde_json::Value>>,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/// IPC Request to Python
|
|
101
|
+
#[derive(Clone, Debug, Serialize)]
|
|
102
|
+
struct IpcRequest {
|
|
103
|
+
#[serde(rename = "type")]
|
|
104
|
+
msg_type: String,
|
|
105
|
+
id: String,
|
|
106
|
+
#[serde(flatten)]
|
|
107
|
+
payload: serde_json::Value,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/// IPC Response from Python
|
|
111
|
+
#[derive(Clone, Debug, Deserialize)]
|
|
112
|
+
struct IpcResponse {
|
|
113
|
+
#[serde(rename = "type")]
|
|
114
|
+
msg_type: String,
|
|
115
|
+
id: String,
|
|
116
|
+
#[serde(flatten)]
|
|
117
|
+
payload: serde_json::Value,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/// Python plugin bridge that communicates via subprocess
|
|
121
|
+
pub struct PythonPluginBridge {
|
|
122
|
+
process: Child,
|
|
123
|
+
manifest: PythonManifest,
|
|
124
|
+
request_counter: AtomicU64,
|
|
125
|
+
stdin_mutex: Mutex<()>,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
impl PythonPluginBridge {
|
|
129
|
+
/// Create a new Python plugin bridge
|
|
130
|
+
///
|
|
131
|
+
/// # Arguments
|
|
132
|
+
/// * `module_name` - The Python module name to load
|
|
133
|
+
/// * `python_path` - Path to Python executable (defaults to "python3")
|
|
134
|
+
/// * `cwd` - Working directory for the subprocess
|
|
135
|
+
///
|
|
136
|
+
/// # Returns
|
|
137
|
+
/// The bridge instance with the plugin loaded
|
|
138
|
+
pub fn new(
|
|
139
|
+
module_name: &str,
|
|
140
|
+
python_path: Option<&str>,
|
|
141
|
+
cwd: Option<&Path>,
|
|
142
|
+
) -> Result<Self> {
|
|
143
|
+
let python = python_path.unwrap_or("python3");
|
|
144
|
+
let bridge_script = Self::get_bridge_script()?;
|
|
145
|
+
|
|
146
|
+
let mut cmd = Command::new(python);
|
|
147
|
+
cmd.arg("-u")
|
|
148
|
+
.arg(&bridge_script)
|
|
149
|
+
.arg("--module")
|
|
150
|
+
.arg(module_name)
|
|
151
|
+
.stdin(Stdio::piped())
|
|
152
|
+
.stdout(Stdio::piped())
|
|
153
|
+
.stderr(Stdio::inherit());
|
|
154
|
+
|
|
155
|
+
if let Some(dir) = cwd {
|
|
156
|
+
cmd.current_dir(dir);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
let mut process = cmd.spawn()?;
|
|
160
|
+
|
|
161
|
+
// Wait for ready message
|
|
162
|
+
let stdout = process.stdout.take().ok_or_else(|| anyhow!("No stdout"))?;
|
|
163
|
+
let mut reader = BufReader::new(stdout);
|
|
164
|
+
let mut line = String::new();
|
|
165
|
+
reader.read_line(&mut line)?;
|
|
166
|
+
|
|
167
|
+
let response: serde_json::Value = serde_json::from_str(&line)?;
|
|
168
|
+
if response.get("type").and_then(|t| t.as_str()) != Some("ready") {
|
|
169
|
+
return Err(anyhow!("Expected ready message, got: {}", line));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let manifest: PythonManifest = serde_json::from_value(
|
|
173
|
+
response.get("manifest").cloned().unwrap_or_default()
|
|
174
|
+
)?;
|
|
175
|
+
|
|
176
|
+
// Put stdout back
|
|
177
|
+
process.stdout = Some(reader.into_inner());
|
|
178
|
+
|
|
179
|
+
Ok(Self {
|
|
180
|
+
process,
|
|
181
|
+
manifest,
|
|
182
|
+
request_counter: AtomicU64::new(0),
|
|
183
|
+
stdin_mutex: Mutex::new(()),
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
fn get_bridge_script() -> Result<String> {
|
|
188
|
+
// Use the existing bridge_server.py from the interop package
|
|
189
|
+
// For now, look in common locations
|
|
190
|
+
let possible_paths = [
|
|
191
|
+
// Relative to interop package
|
|
192
|
+
"packages/interop/python/bridge_server.py",
|
|
193
|
+
"../interop/python/bridge_server.py",
|
|
194
|
+
// In Python package
|
|
195
|
+
"packages/python/elizaos/interop/bridge_server.py",
|
|
196
|
+
];
|
|
197
|
+
|
|
198
|
+
for path in possible_paths {
|
|
199
|
+
let p = Path::new(path);
|
|
200
|
+
if p.exists() {
|
|
201
|
+
return Ok(p.to_string_lossy().to_string());
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Fall back to using -m to run as module
|
|
206
|
+
Err(anyhow!("Could not find bridge_server.py"))
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/// Get the plugin manifest
|
|
210
|
+
pub fn manifest(&self) -> &PythonManifest {
|
|
211
|
+
&self.manifest
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
fn next_id(&self) -> String {
|
|
215
|
+
format!("req_{}", self.request_counter.fetch_add(1, Ordering::SeqCst))
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/// Send a request and get response
|
|
219
|
+
fn send_request(&mut self, msg_type: &str, payload: serde_json::Value) -> Result<IpcResponse> {
|
|
220
|
+
let _lock = self.stdin_mutex.lock().unwrap();
|
|
221
|
+
|
|
222
|
+
let request = IpcRequest {
|
|
223
|
+
msg_type: msg_type.to_string(),
|
|
224
|
+
id: self.next_id(),
|
|
225
|
+
payload,
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
let stdin = self.process.stdin.as_mut().ok_or_else(|| anyhow!("No stdin"))?;
|
|
229
|
+
let json = serde_json::to_string(&request)?;
|
|
230
|
+
writeln!(stdin, "{}", json)?;
|
|
231
|
+
stdin.flush()?;
|
|
232
|
+
|
|
233
|
+
let stdout = self.process.stdout.as_mut().ok_or_else(|| anyhow!("No stdout"))?;
|
|
234
|
+
let mut reader = BufReader::new(stdout);
|
|
235
|
+
let mut line = String::new();
|
|
236
|
+
reader.read_line(&mut line)?;
|
|
237
|
+
|
|
238
|
+
Ok(serde_json::from_str(&line)?)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/// Initialize the plugin
|
|
242
|
+
pub fn init(&mut self, config: HashMap<String, String>) -> Result<()> {
|
|
243
|
+
let payload = serde_json::json!({ "config": config });
|
|
244
|
+
let response = self.send_request("plugin.init", payload)?;
|
|
245
|
+
|
|
246
|
+
if response.msg_type == "error" {
|
|
247
|
+
return Err(anyhow!(
|
|
248
|
+
"Init failed: {}",
|
|
249
|
+
response.payload.get("error").and_then(|e| e.as_str()).unwrap_or("Unknown")
|
|
250
|
+
));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
Ok(())
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/// Validate an action
|
|
257
|
+
pub fn validate_action(
|
|
258
|
+
&mut self,
|
|
259
|
+
name: &str,
|
|
260
|
+
memory: &serde_json::Value,
|
|
261
|
+
state: Option<&serde_json::Value>,
|
|
262
|
+
) -> Result<bool> {
|
|
263
|
+
let payload = serde_json::json!({
|
|
264
|
+
"action": name,
|
|
265
|
+
"memory": memory,
|
|
266
|
+
"state": state,
|
|
267
|
+
});
|
|
268
|
+
let response = self.send_request("action.validate", payload)?;
|
|
269
|
+
|
|
270
|
+
if response.msg_type == "error" {
|
|
271
|
+
return Err(anyhow!(
|
|
272
|
+
"Validate failed: {}",
|
|
273
|
+
response.payload.get("error").and_then(|e| e.as_str()).unwrap_or("Unknown")
|
|
274
|
+
));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
Ok(response.payload.get("valid").and_then(|v| v.as_bool()).unwrap_or(false))
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/// Invoke an action
|
|
281
|
+
pub fn invoke_action(
|
|
282
|
+
&mut self,
|
|
283
|
+
name: &str,
|
|
284
|
+
memory: &serde_json::Value,
|
|
285
|
+
state: Option<&serde_json::Value>,
|
|
286
|
+
options: Option<&serde_json::Value>,
|
|
287
|
+
) -> Result<Option<ActionResult>> {
|
|
288
|
+
let payload = serde_json::json!({
|
|
289
|
+
"action": name,
|
|
290
|
+
"memory": memory,
|
|
291
|
+
"state": state,
|
|
292
|
+
"options": options,
|
|
293
|
+
});
|
|
294
|
+
let response = self.send_request("action.invoke", payload)?;
|
|
295
|
+
|
|
296
|
+
if response.msg_type == "error" {
|
|
297
|
+
return Err(anyhow!(
|
|
298
|
+
"Invoke failed: {}",
|
|
299
|
+
response.payload.get("error").and_then(|e| e.as_str()).unwrap_or("Unknown")
|
|
300
|
+
));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
let result = response.payload.get("result");
|
|
304
|
+
if let Some(r) = result {
|
|
305
|
+
if r.is_null() {
|
|
306
|
+
return Ok(None);
|
|
307
|
+
}
|
|
308
|
+
return Ok(Some(serde_json::from_value(r.clone())?));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
Ok(None)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/// Get provider data
|
|
315
|
+
pub fn get_provider(
|
|
316
|
+
&mut self,
|
|
317
|
+
name: &str,
|
|
318
|
+
memory: &serde_json::Value,
|
|
319
|
+
state: &serde_json::Value,
|
|
320
|
+
) -> Result<ProviderResult> {
|
|
321
|
+
let payload = serde_json::json!({
|
|
322
|
+
"provider": name,
|
|
323
|
+
"memory": memory,
|
|
324
|
+
"state": state,
|
|
325
|
+
});
|
|
326
|
+
let response = self.send_request("provider.get", payload)?;
|
|
327
|
+
|
|
328
|
+
if response.msg_type == "error" {
|
|
329
|
+
return Err(anyhow!(
|
|
330
|
+
"Provider failed: {}",
|
|
331
|
+
response.payload.get("error").and_then(|e| e.as_str()).unwrap_or("Unknown")
|
|
332
|
+
));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
let result = response.payload.get("result").cloned().unwrap_or_default();
|
|
336
|
+
Ok(serde_json::from_value(result)?)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/// Invoke an evaluator
|
|
340
|
+
pub fn invoke_evaluator(
|
|
341
|
+
&mut self,
|
|
342
|
+
name: &str,
|
|
343
|
+
memory: &serde_json::Value,
|
|
344
|
+
state: Option<&serde_json::Value>,
|
|
345
|
+
) -> Result<Option<ActionResult>> {
|
|
346
|
+
let payload = serde_json::json!({
|
|
347
|
+
"evaluator": name,
|
|
348
|
+
"memory": memory,
|
|
349
|
+
"state": state,
|
|
350
|
+
});
|
|
351
|
+
let response = self.send_request("evaluator.invoke", payload)?;
|
|
352
|
+
|
|
353
|
+
if response.msg_type == "error" {
|
|
354
|
+
return Err(anyhow!(
|
|
355
|
+
"Evaluator failed: {}",
|
|
356
|
+
response.payload.get("error").and_then(|e| e.as_str()).unwrap_or("Unknown")
|
|
357
|
+
));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
let result = response.payload.get("result");
|
|
361
|
+
if let Some(r) = result {
|
|
362
|
+
if r.is_null() {
|
|
363
|
+
return Ok(None);
|
|
364
|
+
}
|
|
365
|
+
return Ok(Some(serde_json::from_value(r.clone())?));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
Ok(None)
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
impl Drop for PythonPluginBridge {
|
|
373
|
+
fn drop(&mut self) {
|
|
374
|
+
let _ = self.process.kill();
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
#[cfg(test)]
|
|
379
|
+
mod tests {
|
|
380
|
+
use super::*;
|
|
381
|
+
|
|
382
|
+
#[test]
|
|
383
|
+
fn test_manifest_deserialize() {
|
|
384
|
+
let json = r#"{
|
|
385
|
+
"name": "test-plugin",
|
|
386
|
+
"description": "A test plugin",
|
|
387
|
+
"language": "python",
|
|
388
|
+
"actions": [{"name": "test_action"}],
|
|
389
|
+
"providers": [],
|
|
390
|
+
"evaluators": []
|
|
391
|
+
}"#;
|
|
392
|
+
|
|
393
|
+
let manifest: PythonManifest = serde_json::from_str(json).unwrap();
|
|
394
|
+
assert_eq!(manifest.name, "test-plugin");
|
|
395
|
+
assert_eq!(manifest.language, Some("python".to_string()));
|
|
396
|
+
assert_eq!(manifest.actions.len(), 1);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
#[test]
|
|
400
|
+
fn test_action_result_deserialize() {
|
|
401
|
+
let json = r#"{"success": true, "text": "Done"}"#;
|
|
402
|
+
let result: ActionResult = serde_json::from_str(json).unwrap();
|
|
403
|
+
assert!(result.success);
|
|
404
|
+
assert_eq!(result.text, Some("Done".to_string()));
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
|