rahul7star commited on
Commit
b4f5cdf
·
verified ·
1 Parent(s): 5ad3fdb

Update pygmyclaw_multitool.py

Browse files
Files changed (1) hide show
  1. pygmyclaw_multitool.py +209 -95
pygmyclaw_multitool.py CHANGED
@@ -1,28 +1,30 @@
1
  #!/usr/bin/env python3
2
  """
3
  PygmyClaw Multitool – Contains the actual tool implementations.
4
- Works both as:
5
- 1) CLI tool (stdin → stdout JSON)
6
- 2) Python module (import + run_tool())
7
  """
8
-
9
  import json
10
  import sys
11
  import os
12
- import platform
13
  import time
 
 
14
  from pathlib import Path
15
 
16
- SCRIPT_DIR = Path(__file__).parent.resolve()
 
 
 
 
 
17
 
 
18
  ERROR_LOG = SCRIPT_DIR / "error_log.json"
19
-
20
  MAX_LOG_ENTRIES = 1000
21
-
22
 
23
  # ----------------------------------------------------------------------
24
  # Tool definitions
25
-
26
  TOOLS = {
27
  "list_tools_detailed": {
28
  "name": "list_tools_detailed",
@@ -30,14 +32,12 @@ TOOLS = {
30
  "parameters": {},
31
  "func": "do_list_tools"
32
  },
33
-
34
  "sys_info": {
35
  "name": "sys_info",
36
  "description": "Get system information (OS, Python version, etc.).",
37
  "parameters": {},
38
  "func": "do_sys_info"
39
  },
40
-
41
  "log_error": {
42
  "name": "log_error",
43
  "description": "Log an error message to the error log.",
@@ -47,39 +47,70 @@ TOOLS = {
47
  },
48
  "func": "do_log_error"
49
  },
50
-
51
  "echo": {
52
  "name": "echo",
53
  "description": "Echo the input text (for testing).",
54
  "parameters": {"text": "string"},
55
  "func": "do_echo"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  }
57
  }
58
 
59
-
60
  # ----------------------------------------------------------------------
61
- # Tool Implementations
62
-
63
 
64
  def do_list_tools():
65
- """Return the list of tools with metadata."""
66
-
67
  tools_list = []
68
-
69
  for name, info in TOOLS.items():
70
-
71
  tools_list.append({
72
  "name": name,
73
  "description": info["description"],
74
  "parameters": info["parameters"]
75
  })
76
-
77
  return {"tools": tools_list}
78
 
79
-
80
  def do_sys_info():
81
  """Return system information."""
82
-
83
  return {
84
  "os": platform.system(),
85
  "os_release": platform.release(),
@@ -87,124 +118,207 @@ def do_sys_info():
87
  "hostname": platform.node()
88
  }
89
 
90
-
91
  def do_log_error(msg, trace=""):
92
- """Append an error to the error log."""
93
-
94
  entry = {
95
  "timestamp": time.time(),
96
  "msg": msg,
97
  "trace": trace
98
  }
99
-
100
  try:
101
-
102
  if ERROR_LOG.exists():
103
-
104
  with open(ERROR_LOG) as f:
105
  log = json.load(f)
106
-
107
  else:
108
-
109
  log = []
110
-
111
  log.append(entry)
112
-
113
  if len(log) > MAX_LOG_ENTRIES:
114
  log = log[-MAX_LOG_ENTRIES:]
115
-
116
- with open(ERROR_LOG, "w") as f:
117
  json.dump(log, f, indent=2)
118
-
119
  return {"status": "logged"}
120
-
121
  except Exception as e:
122
-
123
  return {"error": f"Failed to write log: {e}"}
124
 
125
-
126
  def do_echo(text):
127
- """Echo input."""
128
-
129
  return {"echo": text}
130
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
 
132
- # ----------------------------------------------------------------------
133
- # INTERNAL TOOL EXECUTOR (used by PygmyClaw agent)
134
-
135
- def run_tool(action, **params):
136
  """
137
- Run tool programmatically.
138
-
139
- Example:
140
- run_tool("sys_info")
141
- run_tool("echo", text="hello")
142
  """
143
-
144
- tool = TOOLS.get(action)
145
-
146
- if not tool:
147
- return {"error": f"Unknown tool '{action}'"}
148
-
149
- func_name = tool["func"]
150
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  try:
152
-
153
- if func_name == "do_list_tools":
154
- return do_list_tools()
155
-
156
- elif func_name == "do_sys_info":
157
- return do_sys_info()
158
-
159
- elif func_name == "do_log_error":
160
- return do_log_error(
161
- params.get("msg"),
162
- params.get("trace", "")
163
- )
164
-
165
- elif func_name == "do_echo":
166
- return do_echo(params.get("text"))
167
-
168
- else:
169
- return {"error": f"Unknown function '{func_name}'"}
170
-
171
  except Exception as e:
 
172
 
173
- return {"error": f"Tool execution failed: {e}"}
 
 
 
 
 
 
 
 
 
174
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
 
176
  # ----------------------------------------------------------------------
177
- # CLI Dispatcher (existing behavior preserved)
 
 
 
 
 
 
 
 
 
 
 
 
178
 
 
 
179
  def main():
180
-
181
  try:
182
-
183
- raw = sys.stdin.read()
184
-
185
- if not raw:
186
- print(json.dumps({"error": "No input"}))
187
- return
188
-
189
- data = json.loads(raw)
190
-
191
  action = data.get("action")
192
-
193
  if not action:
194
-
195
  print(json.dumps({"error": "No action specified"}))
196
  return
197
 
198
- params = {k: v for k, v in data.items() if k != "action"}
 
 
 
199
 
200
- result = run_tool(action, **params)
 
 
 
 
201
 
 
 
 
 
 
 
 
 
 
 
 
 
202
  print(json.dumps(result))
203
-
204
  except Exception as e:
205
-
206
  print(json.dumps({"error": f"Multitool exception: {e}"}))
207
 
208
-
209
  if __name__ == "__main__":
210
  main()
 
1
  #!/usr/bin/env python3
2
  """
3
  PygmyClaw Multitool – Contains the actual tool implementations.
4
+ Now with generic dispatcher, heartbeat, file I/O, and scheduler tools.
 
 
5
  """
 
6
  import json
7
  import sys
8
  import os
 
9
  import time
10
+ import inspect
11
+ import platform
12
  from pathlib import Path
13
 
14
+ # Optional dependencies
15
+ try:
16
+ import psutil
17
+ PSUTIL_AVAILABLE = True
18
+ except ImportError:
19
+ PSUTIL_AVAILABLE = False
20
 
21
+ SCRIPT_DIR = Path(__file__).parent.resolve()
22
  ERROR_LOG = SCRIPT_DIR / "error_log.json"
 
23
  MAX_LOG_ENTRIES = 1000
24
+ SCHEDULED_JOBS_FILE = SCRIPT_DIR / "scheduled_jobs.json"
25
 
26
  # ----------------------------------------------------------------------
27
  # Tool definitions
 
28
  TOOLS = {
29
  "list_tools_detailed": {
30
  "name": "list_tools_detailed",
 
32
  "parameters": {},
33
  "func": "do_list_tools"
34
  },
 
35
  "sys_info": {
36
  "name": "sys_info",
37
  "description": "Get system information (OS, Python version, etc.).",
38
  "parameters": {},
39
  "func": "do_sys_info"
40
  },
 
41
  "log_error": {
42
  "name": "log_error",
43
  "description": "Log an error message to the error log.",
 
47
  },
48
  "func": "do_log_error"
49
  },
 
50
  "echo": {
51
  "name": "echo",
52
  "description": "Echo the input text (for testing).",
53
  "parameters": {"text": "string"},
54
  "func": "do_echo"
55
+ },
56
+ "heartbeat": {
57
+ "name": "heartbeat",
58
+ "description": "Get system health info: CPU, memory, disk, uptime.",
59
+ "parameters": {},
60
+ "func": "do_heartbeat"
61
+ },
62
+ "file_read": {
63
+ "name": "file_read",
64
+ "description": "Read a file from the workspace.",
65
+ "parameters": {"path": "string"},
66
+ "func": "do_file_read"
67
+ },
68
+ "file_write": {
69
+ "name": "file_write",
70
+ "description": "Write content to a file (mode: 'w' overwrite, 'a' append).",
71
+ "parameters": {"path": "string", "content": "string", "mode": "string (optional)"},
72
+ "func": "do_file_write"
73
+ },
74
+ "schedule_task": {
75
+ "name": "schedule_task",
76
+ "description": "Schedule a command to run at a specific time or interval. Time format: 'in 5 minutes', 'every day at 10:00', etc. (uses dateparser if installed, otherwise simple timestamps).",
77
+ "parameters": {
78
+ "command": "string",
79
+ "time_spec": "string",
80
+ "job_id": "string (optional)"
81
+ },
82
+ "func": "do_schedule_task"
83
+ },
84
+ "list_scheduled": {
85
+ "name": "list_scheduled",
86
+ "description": "List all scheduled jobs.",
87
+ "parameters": {},
88
+ "func": "do_list_scheduled"
89
+ },
90
+ "remove_scheduled": {
91
+ "name": "remove_scheduled",
92
+ "description": "Remove a scheduled job by its ID.",
93
+ "parameters": {"job_id": "string"},
94
+ "func": "do_remove_scheduled"
95
  }
96
  }
97
 
 
98
  # ----------------------------------------------------------------------
99
+ # Tool implementations
 
100
 
101
  def do_list_tools():
102
+ """Return the list of tools with their metadata."""
 
103
  tools_list = []
 
104
  for name, info in TOOLS.items():
 
105
  tools_list.append({
106
  "name": name,
107
  "description": info["description"],
108
  "parameters": info["parameters"]
109
  })
 
110
  return {"tools": tools_list}
111
 
 
112
  def do_sys_info():
113
  """Return system information."""
 
114
  return {
115
  "os": platform.system(),
116
  "os_release": platform.release(),
 
118
  "hostname": platform.node()
119
  }
120
 
 
121
  def do_log_error(msg, trace=""):
122
+ """Append an error to the error log file."""
 
123
  entry = {
124
  "timestamp": time.time(),
125
  "msg": msg,
126
  "trace": trace
127
  }
 
128
  try:
 
129
  if ERROR_LOG.exists():
 
130
  with open(ERROR_LOG) as f:
131
  log = json.load(f)
 
132
  else:
 
133
  log = []
 
134
  log.append(entry)
 
135
  if len(log) > MAX_LOG_ENTRIES:
136
  log = log[-MAX_LOG_ENTRIES:]
137
+ with open(ERROR_LOG, 'w') as f:
 
138
  json.dump(log, f, indent=2)
 
139
  return {"status": "logged"}
 
140
  except Exception as e:
 
141
  return {"error": f"Failed to write log: {e}"}
142
 
 
143
  def do_echo(text):
144
+ """Echo the input."""
 
145
  return {"echo": text}
146
 
147
+ def do_heartbeat():
148
+ """Return system load, memory, disk usage, and uptime."""
149
+ info = {}
150
+ if PSUTIL_AVAILABLE:
151
+ try:
152
+ info["cpu_percent"] = psutil.cpu_percent(interval=1)
153
+ mem = psutil.virtual_memory()
154
+ info["memory"] = {
155
+ "total": mem.total,
156
+ "available": mem.available,
157
+ "percent": mem.percent
158
+ }
159
+ disk = psutil.disk_usage('/')
160
+ info["disk"] = {
161
+ "total": disk.total,
162
+ "used": disk.used,
163
+ "free": disk.free,
164
+ "percent": disk.percent
165
+ }
166
+ info["uptime_seconds"] = time.time() - psutil.boot_time()
167
+ except Exception as e:
168
+ info["error"] = f"psutil error: {e}"
169
+ else:
170
+ info["error"] = "psutil not installed – install for detailed stats"
171
+ info["platform"] = platform.platform()
172
+ return info
173
+
174
+ def _safe_path(path):
175
+ """Resolve path relative to SCRIPT_DIR and ensure it stays inside."""
176
+ target = (SCRIPT_DIR / path).resolve()
177
+ try:
178
+ target.relative_to(SCRIPT_DIR)
179
+ return target
180
+ except ValueError:
181
+ return None
182
+
183
+ def do_file_read(path):
184
+ """Read and return contents of a file (must be inside workspace)."""
185
+ safe = _safe_path(path)
186
+ if not safe:
187
+ return {"error": "Path not allowed (outside workspace)"}
188
+ try:
189
+ with open(safe, 'r', encoding='utf-8') as f:
190
+ content = f.read()
191
+ return {"content": content, "path": str(safe)}
192
+ except Exception as e:
193
+ return {"error": str(e)}
194
+
195
+ def do_file_write(path, content, mode="w"):
196
+ """Write content to a file (modes: w = overwrite, a = append)."""
197
+ safe = _safe_path(path)
198
+ if not safe:
199
+ return {"error": "Path not allowed (outside workspace)"}
200
+ if mode not in ("w", "a"):
201
+ return {"error": f"Invalid mode '{mode}'; use 'w' or 'a'"}
202
+ try:
203
+ with open(safe, mode, encoding='utf-8') as f:
204
+ f.write(content)
205
+ return {"status": "written", "path": str(safe), "mode": mode}
206
+ except Exception as e:
207
+ return {"error": str(e)}
208
 
209
+ def do_schedule_task(command, time_spec, job_id=None):
 
 
 
210
  """
211
+ Add a scheduled job. Simple implementation: store in a JSON file.
212
+ The agent's scheduler will read this file and execute commands when due.
 
 
 
213
  """
214
+ jobs = []
215
+ if SCHEDULED_JOBS_FILE.exists():
216
+ try:
217
+ with open(SCHEDULED_JOBS_FILE) as f:
218
+ jobs = json.load(f)
219
+ except Exception:
220
+ jobs = []
221
+
222
+ if job_id is None:
223
+ job_id = f"job_{int(time.time())}_{len(jobs)}"
224
+
225
+ # Parse time_spec – we just store it; the agent's scheduler will interpret.
226
+ # For simplicity, we support:
227
+ # - "in X minutes/hours/days" -> compute timestamp
228
+ # - "every day at HH:MM" -> store as cron-like?
229
+ # We'll store raw and let the agent handle it.
230
+ job = {
231
+ "id": job_id,
232
+ "command": command,
233
+ "time_spec": time_spec,
234
+ "created": time.time()
235
+ }
236
+ jobs.append(job)
237
  try:
238
+ with open(SCHEDULED_JOBS_FILE, 'w') as f:
239
+ json.dump(jobs, f, indent=2)
240
+ return {"status": "scheduled", "job_id": job_id}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
  except Exception as e:
242
+ return {"error": f"Failed to write jobs file: {e}"}
243
 
244
+ def do_list_scheduled():
245
+ """List all scheduled jobs."""
246
+ if not SCHEDULED_JOBS_FILE.exists():
247
+ return {"jobs": []}
248
+ try:
249
+ with open(SCHEDULED_JOBS_FILE) as f:
250
+ jobs = json.load(f)
251
+ return {"jobs": jobs}
252
+ except Exception as e:
253
+ return {"error": f"Failed to read jobs: {e}"}
254
 
255
+ def do_remove_scheduled(job_id):
256
+ """Remove a scheduled job by ID."""
257
+ if not SCHEDULED_JOBS_FILE.exists():
258
+ return {"error": "No jobs file"}
259
+ try:
260
+ with open(SCHEDULED_JOBS_FILE) as f:
261
+ jobs = json.load(f)
262
+ new_jobs = [j for j in jobs if j.get("id") != job_id]
263
+ if len(new_jobs) == len(jobs):
264
+ return {"error": f"Job ID '{job_id}' not found"}
265
+ with open(SCHEDULED_JOBS_FILE, 'w') as f:
266
+ json.dump(new_jobs, f, indent=2)
267
+ return {"status": "removed", "job_id": job_id}
268
+ except Exception as e:
269
+ return {"error": f"Failed to remove job: {e}"}
270
 
271
  # ----------------------------------------------------------------------
272
+ # Map function names to actual functions
273
+ FUNC_MAP = {
274
+ "do_list_tools": do_list_tools,
275
+ "do_sys_info": do_sys_info,
276
+ "do_log_error": do_log_error,
277
+ "do_echo": do_echo,
278
+ "do_heartbeat": do_heartbeat,
279
+ "do_file_read": do_file_read,
280
+ "do_file_write": do_file_write,
281
+ "do_schedule_task": do_schedule_task,
282
+ "do_list_scheduled": do_list_scheduled,
283
+ "do_remove_scheduled": do_remove_scheduled,
284
+ }
285
 
286
+ # ----------------------------------------------------------------------
287
+ # Generic dispatcher using inspect
288
  def main():
 
289
  try:
290
+ data = json.loads(sys.stdin.read())
 
 
 
 
 
 
 
 
291
  action = data.get("action")
 
292
  if not action:
 
293
  print(json.dumps({"error": "No action specified"}))
294
  return
295
 
296
+ tool_info = TOOLS.get(action)
297
+ if not tool_info:
298
+ print(json.dumps({"error": f"Unknown action '{action}'"}))
299
+ return
300
 
301
+ func_name = tool_info["func"]
302
+ func = FUNC_MAP.get(func_name)
303
+ if not func:
304
+ print(json.dumps({"error": f"Internal error: unknown function {func_name}"}))
305
+ return
306
 
307
+ # Extract parameters expected by the function
308
+ sig = inspect.signature(func)
309
+ kwargs = {}
310
+ for param in sig.parameters.values():
311
+ if param.name in data:
312
+ kwargs[param.name] = data[param.name]
313
+ elif param.default is param.empty:
314
+ # Required parameter missing
315
+ print(json.dumps({"error": f"Missing required parameter '{param.name}'"}))
316
+ return
317
+
318
+ result = func(**kwargs)
319
  print(json.dumps(result))
 
320
  except Exception as e:
 
321
  print(json.dumps({"error": f"Multitool exception: {e}"}))
322
 
 
323
  if __name__ == "__main__":
324
  main()