@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.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +436 -0
  3. package/dist/packages/interop/tsconfig.tsbuildinfo +1 -0
  4. package/dist/tsconfig.tsbuildinfo +1 -0
  5. package/dist/typescript/index.d.ts +33 -0
  6. package/dist/typescript/index.d.ts.map +1 -0
  7. package/dist/typescript/index.js +121 -0
  8. package/dist/typescript/python-bridge.d.ts +80 -0
  9. package/dist/typescript/python-bridge.d.ts.map +1 -0
  10. package/dist/typescript/python-bridge.js +334 -0
  11. package/dist/typescript/types.d.ts +301 -0
  12. package/dist/typescript/types.d.ts.map +1 -0
  13. package/dist/typescript/types.js +10 -0
  14. package/dist/typescript/wasm-loader.d.ts +32 -0
  15. package/dist/typescript/wasm-loader.d.ts.map +1 -0
  16. package/dist/typescript/wasm-loader.js +269 -0
  17. package/package.json +43 -0
  18. package/python/__init__.py +50 -0
  19. package/python/__pycache__/__init__.cpython-313.pyc +0 -0
  20. package/python/__pycache__/rust_ffi.cpython-313.pyc +0 -0
  21. package/python/__pycache__/ts_bridge.cpython-313.pyc +0 -0
  22. package/python/__pycache__/wasm_loader.cpython-313.pyc +0 -0
  23. package/python/bridge_server.py +505 -0
  24. package/python/rust_ffi.py +418 -0
  25. package/python/tests/__init__.py +1 -0
  26. package/python/tests/__pycache__/__init__.cpython-313.pyc +0 -0
  27. package/python/tests/__pycache__/test_bridge_server.cpython-313-pytest-9.0.2.pyc +0 -0
  28. package/python/tests/__pycache__/test_interop.cpython-313-pytest-9.0.2.pyc +0 -0
  29. package/python/tests/__pycache__/test_rust_ffi.cpython-313-pytest-9.0.2.pyc +0 -0
  30. package/python/tests/__pycache__/test_rust_ffi_loader.cpython-313-pytest-9.0.2.pyc +0 -0
  31. package/python/tests/test_bridge_server.py +525 -0
  32. package/python/tests/test_interop.py +319 -0
  33. package/python/tests/test_rust_ffi.py +352 -0
  34. package/python/tests/test_rust_ffi_loader.py +71 -0
  35. package/python/ts_bridge.py +452 -0
  36. package/python/ts_bridge_runner.mjs +284 -0
  37. package/python/wasm_loader.py +517 -0
  38. package/rust/ffi_exports.rs +362 -0
  39. package/rust/mod.rs +21 -0
  40. package/rust/py_loader.rs +412 -0
  41. package/rust/ts_loader.rs +467 -0
  42. 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
+