pythonclaw 0.2.0__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.
Files changed (112) hide show
  1. pythonclaw/__init__.py +17 -0
  2. pythonclaw/__main__.py +6 -0
  3. pythonclaw/channels/discord_bot.py +231 -0
  4. pythonclaw/channels/telegram_bot.py +236 -0
  5. pythonclaw/config.py +190 -0
  6. pythonclaw/core/__init__.py +25 -0
  7. pythonclaw/core/agent.py +773 -0
  8. pythonclaw/core/compaction.py +220 -0
  9. pythonclaw/core/knowledge/rag.py +93 -0
  10. pythonclaw/core/llm/anthropic_client.py +107 -0
  11. pythonclaw/core/llm/base.py +26 -0
  12. pythonclaw/core/llm/gemini_client.py +139 -0
  13. pythonclaw/core/llm/openai_compatible.py +39 -0
  14. pythonclaw/core/llm/response.py +57 -0
  15. pythonclaw/core/memory/manager.py +120 -0
  16. pythonclaw/core/memory/storage.py +164 -0
  17. pythonclaw/core/persistent_agent.py +103 -0
  18. pythonclaw/core/retrieval/__init__.py +6 -0
  19. pythonclaw/core/retrieval/chunker.py +78 -0
  20. pythonclaw/core/retrieval/dense.py +152 -0
  21. pythonclaw/core/retrieval/fusion.py +51 -0
  22. pythonclaw/core/retrieval/reranker.py +112 -0
  23. pythonclaw/core/retrieval/retriever.py +166 -0
  24. pythonclaw/core/retrieval/sparse.py +69 -0
  25. pythonclaw/core/session_store.py +269 -0
  26. pythonclaw/core/skill_loader.py +322 -0
  27. pythonclaw/core/skillhub.py +290 -0
  28. pythonclaw/core/tools.py +622 -0
  29. pythonclaw/core/utils.py +64 -0
  30. pythonclaw/daemon.py +221 -0
  31. pythonclaw/init.py +61 -0
  32. pythonclaw/main.py +489 -0
  33. pythonclaw/onboard.py +290 -0
  34. pythonclaw/scheduler/cron.py +310 -0
  35. pythonclaw/scheduler/heartbeat.py +178 -0
  36. pythonclaw/server.py +145 -0
  37. pythonclaw/session_manager.py +104 -0
  38. pythonclaw/templates/persona/demo_persona.md +2 -0
  39. pythonclaw/templates/skills/communication/CATEGORY.md +4 -0
  40. pythonclaw/templates/skills/communication/email/SKILL.md +54 -0
  41. pythonclaw/templates/skills/communication/email/__pycache__/send_email.cpython-311.pyc +0 -0
  42. pythonclaw/templates/skills/communication/email/send_email.py +88 -0
  43. pythonclaw/templates/skills/data/CATEGORY.md +4 -0
  44. pythonclaw/templates/skills/data/csv_analyzer/SKILL.md +51 -0
  45. pythonclaw/templates/skills/data/csv_analyzer/__pycache__/analyze.cpython-311.pyc +0 -0
  46. pythonclaw/templates/skills/data/csv_analyzer/analyze.py +138 -0
  47. pythonclaw/templates/skills/data/finance/SKILL.md +41 -0
  48. pythonclaw/templates/skills/data/finance/__pycache__/fetch_quote.cpython-311.pyc +0 -0
  49. pythonclaw/templates/skills/data/finance/fetch_quote.py +118 -0
  50. pythonclaw/templates/skills/data/news/SKILL.md +39 -0
  51. pythonclaw/templates/skills/data/news/__pycache__/search_news.cpython-311.pyc +0 -0
  52. pythonclaw/templates/skills/data/news/search_news.py +57 -0
  53. pythonclaw/templates/skills/data/pdf_reader/SKILL.md +40 -0
  54. pythonclaw/templates/skills/data/pdf_reader/__pycache__/read_pdf.cpython-311.pyc +0 -0
  55. pythonclaw/templates/skills/data/pdf_reader/read_pdf.py +113 -0
  56. pythonclaw/templates/skills/data/scraper/SKILL.md +39 -0
  57. pythonclaw/templates/skills/data/scraper/__pycache__/scrape.cpython-311.pyc +0 -0
  58. pythonclaw/templates/skills/data/scraper/scrape.py +92 -0
  59. pythonclaw/templates/skills/data/weather/SKILL.md +42 -0
  60. pythonclaw/templates/skills/data/weather/__pycache__/weather.cpython-311.pyc +0 -0
  61. pythonclaw/templates/skills/data/weather/weather.py +142 -0
  62. pythonclaw/templates/skills/data/youtube/SKILL.md +43 -0
  63. pythonclaw/templates/skills/data/youtube/__pycache__/youtube_info.cpython-311.pyc +0 -0
  64. pythonclaw/templates/skills/data/youtube/youtube_info.py +167 -0
  65. pythonclaw/templates/skills/dev/CATEGORY.md +4 -0
  66. pythonclaw/templates/skills/dev/code_runner/SKILL.md +46 -0
  67. pythonclaw/templates/skills/dev/code_runner/__pycache__/run_code.cpython-311.pyc +0 -0
  68. pythonclaw/templates/skills/dev/code_runner/run_code.py +117 -0
  69. pythonclaw/templates/skills/dev/github/SKILL.md +52 -0
  70. pythonclaw/templates/skills/dev/github/__pycache__/gh.cpython-311.pyc +0 -0
  71. pythonclaw/templates/skills/dev/github/gh.py +165 -0
  72. pythonclaw/templates/skills/dev/http_request/SKILL.md +40 -0
  73. pythonclaw/templates/skills/dev/http_request/__pycache__/request.cpython-311.pyc +0 -0
  74. pythonclaw/templates/skills/dev/http_request/request.py +90 -0
  75. pythonclaw/templates/skills/google/CATEGORY.md +4 -0
  76. pythonclaw/templates/skills/google/workspace/SKILL.md +98 -0
  77. pythonclaw/templates/skills/google/workspace/check_setup.sh +52 -0
  78. pythonclaw/templates/skills/meta/CATEGORY.md +4 -0
  79. pythonclaw/templates/skills/meta/skill_creator/SKILL.md +151 -0
  80. pythonclaw/templates/skills/system/CATEGORY.md +4 -0
  81. pythonclaw/templates/skills/system/change_persona/SKILL.md +41 -0
  82. pythonclaw/templates/skills/system/change_setting/SKILL.md +65 -0
  83. pythonclaw/templates/skills/system/change_setting/__pycache__/update_config.cpython-311.pyc +0 -0
  84. pythonclaw/templates/skills/system/change_setting/update_config.py +129 -0
  85. pythonclaw/templates/skills/system/change_soul/SKILL.md +41 -0
  86. pythonclaw/templates/skills/system/onboarding/SKILL.md +63 -0
  87. pythonclaw/templates/skills/system/onboarding/__pycache__/write_identity.cpython-311.pyc +0 -0
  88. pythonclaw/templates/skills/system/onboarding/write_identity.py +218 -0
  89. pythonclaw/templates/skills/system/random/SKILL.md +33 -0
  90. pythonclaw/templates/skills/system/random/__pycache__/random_util.cpython-311.pyc +0 -0
  91. pythonclaw/templates/skills/system/random/random_util.py +45 -0
  92. pythonclaw/templates/skills/system/time/SKILL.md +33 -0
  93. pythonclaw/templates/skills/system/time/__pycache__/time_util.cpython-311.pyc +0 -0
  94. pythonclaw/templates/skills/system/time/time_util.py +81 -0
  95. pythonclaw/templates/skills/text/CATEGORY.md +4 -0
  96. pythonclaw/templates/skills/text/translator/SKILL.md +47 -0
  97. pythonclaw/templates/skills/text/translator/__pycache__/translate.cpython-311.pyc +0 -0
  98. pythonclaw/templates/skills/text/translator/translate.py +66 -0
  99. pythonclaw/templates/skills/web/CATEGORY.md +4 -0
  100. pythonclaw/templates/skills/web/tavily/SKILL.md +61 -0
  101. pythonclaw/templates/soul/SOUL.md +54 -0
  102. pythonclaw/web/__init__.py +1 -0
  103. pythonclaw/web/app.py +585 -0
  104. pythonclaw/web/static/favicon.png +0 -0
  105. pythonclaw/web/static/index.html +1318 -0
  106. pythonclaw/web/static/logo.png +0 -0
  107. pythonclaw-0.2.0.dist-info/METADATA +410 -0
  108. pythonclaw-0.2.0.dist-info/RECORD +112 -0
  109. pythonclaw-0.2.0.dist-info/WHEEL +5 -0
  110. pythonclaw-0.2.0.dist-info/entry_points.txt +2 -0
  111. pythonclaw-0.2.0.dist-info/licenses/LICENSE +21 -0
  112. pythonclaw-0.2.0.dist-info/top_level.txt +1 -0
pythonclaw/daemon.py ADDED
@@ -0,0 +1,221 @@
1
+ """
2
+ Daemon process lifecycle manager for PythonClaw.
3
+
4
+ Handles starting the agent as a background process, writing a PID file,
5
+ stopping via SIGTERM, and querying status.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import os
12
+ import signal
13
+ import subprocess
14
+ import sys
15
+ import time
16
+ from pathlib import Path
17
+
18
+ from . import config
19
+
20
+ # ── Paths ────────────────────────────────────────────────────────────────────
21
+
22
+ PYTHONCLAW_DIR = Path.home() / ".pythonclaw"
23
+ PID_FILE = PYTHONCLAW_DIR / "pythonclaw.pid"
24
+ LOG_FILE = PYTHONCLAW_DIR / "daemon.log"
25
+ META_FILE = PYTHONCLAW_DIR / "daemon.meta.json"
26
+
27
+
28
+ def _ensure_dir() -> None:
29
+ PYTHONCLAW_DIR.mkdir(parents=True, exist_ok=True)
30
+
31
+
32
+ # ── Public API ───────────────────────────────────────────────────────────────
33
+
34
+ def start_daemon(
35
+ channels: list[str] | None = None,
36
+ config_path: str | None = None,
37
+ ) -> None:
38
+ """Start PythonClaw as a background daemon.
39
+
40
+ Spawns a detached subprocess running ``pythonclaw start --foreground``
41
+ and writes its PID to ``~/.pythonclaw/pythonclaw.pid``.
42
+ """
43
+ pid = read_pid()
44
+ if pid and _is_alive(pid):
45
+ port = _read_meta().get("port", 7788)
46
+ print(f"[PythonClaw] Already running (PID {pid}).")
47
+ print(f"[PythonClaw] Dashboard: http://localhost:{port}")
48
+ return
49
+
50
+ _ensure_dir()
51
+
52
+ cmd = [sys.executable, "-m", "pythonclaw", "start", "--foreground"]
53
+ if config_path:
54
+ cmd += ["--config", config_path]
55
+ if channels:
56
+ cmd += ["--channels"] + channels
57
+
58
+ log_handle = open(LOG_FILE, "a", encoding="utf-8")
59
+ log_handle.write(f"\n{'='*60}\n")
60
+ log_handle.write(f"Starting PythonClaw daemon at {time.strftime('%Y-%m-%d %H:%M:%S')}\n")
61
+ log_handle.write(f"Command: {' '.join(cmd)}\n")
62
+ log_handle.write(f"{'='*60}\n")
63
+ log_handle.flush()
64
+
65
+ proc = subprocess.Popen(
66
+ cmd,
67
+ stdout=log_handle,
68
+ stderr=subprocess.STDOUT,
69
+ start_new_session=True,
70
+ cwd=os.getcwd(),
71
+ )
72
+
73
+ _write_pid(proc.pid)
74
+
75
+ port = config.get_int("web", "port", default=7788)
76
+ _write_meta({
77
+ "pid": proc.pid,
78
+ "port": port,
79
+ "cwd": os.getcwd(),
80
+ "started_at": time.strftime("%Y-%m-%d %H:%M:%S"),
81
+ "channels": channels or [],
82
+ "config_path": config_path,
83
+ })
84
+
85
+ # Brief pause to check the process didn't immediately crash
86
+ time.sleep(1.0)
87
+ if proc.poll() is not None:
88
+ print(f"[PythonClaw] Daemon failed to start (exit code {proc.returncode}).")
89
+ print(f"[PythonClaw] Check logs: {LOG_FILE}")
90
+ _cleanup_pid()
91
+ return
92
+
93
+ print(f"[PythonClaw] Daemon started (PID {proc.pid}).")
94
+ print(f"[PythonClaw] Dashboard: http://localhost:{port}")
95
+ print(f"[PythonClaw] Logs: {LOG_FILE}")
96
+
97
+
98
+ def stop_daemon() -> None:
99
+ """Stop the running PythonClaw daemon."""
100
+ pid = read_pid()
101
+ if not pid:
102
+ print("[PythonClaw] No daemon is running.")
103
+ return
104
+
105
+ if not _is_alive(pid):
106
+ print(f"[PythonClaw] Stale PID file (process {pid} not found). Cleaning up.")
107
+ _cleanup_pid()
108
+ return
109
+
110
+ print(f"[PythonClaw] Stopping daemon (PID {pid})...", end=" ", flush=True)
111
+ try:
112
+ os.kill(pid, signal.SIGTERM)
113
+ except ProcessLookupError:
114
+ print("already stopped.")
115
+ _cleanup_pid()
116
+ return
117
+
118
+ # Wait up to 10 seconds for graceful shutdown
119
+ for _ in range(20):
120
+ time.sleep(0.5)
121
+ if not _is_alive(pid):
122
+ break
123
+
124
+ if _is_alive(pid):
125
+ print("forcing...", end=" ", flush=True)
126
+ try:
127
+ os.kill(pid, signal.SIGKILL)
128
+ except ProcessLookupError:
129
+ pass
130
+ time.sleep(0.5)
131
+
132
+ _cleanup_pid()
133
+ print("stopped.")
134
+
135
+
136
+ def daemon_status() -> dict | None:
137
+ """Check daemon status. Returns metadata dict or None."""
138
+ pid = read_pid()
139
+ if not pid:
140
+ return None
141
+
142
+ if not _is_alive(pid):
143
+ _cleanup_pid()
144
+ return None
145
+
146
+ meta = _read_meta()
147
+ meta["pid"] = pid
148
+ meta["alive"] = True
149
+
150
+ started = meta.get("started_at", "")
151
+ if started:
152
+ try:
153
+ from datetime import datetime
154
+ start_dt = datetime.strptime(started, "%Y-%m-%d %H:%M:%S")
155
+ uptime_sec = int((datetime.now() - start_dt).total_seconds())
156
+ hours, remainder = divmod(uptime_sec, 3600)
157
+ minutes, seconds = divmod(remainder, 60)
158
+ meta["uptime"] = f"{hours}h {minutes}m {seconds}s"
159
+ except Exception:
160
+ meta["uptime"] = "unknown"
161
+
162
+ return meta
163
+
164
+
165
+ def print_status() -> None:
166
+ """Print daemon status to stdout."""
167
+ status = daemon_status()
168
+ if not status:
169
+ print("[PythonClaw] No daemon is running.")
170
+ return
171
+
172
+ print("[PythonClaw] Daemon Status")
173
+ print(f" PID : {status['pid']}")
174
+ print(f" Uptime : {status.get('uptime', 'unknown')}")
175
+ print(f" Port : {status.get('port', '?')}")
176
+ print(f" CWD : {status.get('cwd', '?')}")
177
+ print(f" Started : {status.get('started_at', '?')}")
178
+ channels = status.get("channels", [])
179
+ if channels:
180
+ print(f" Channels : {', '.join(channels)}")
181
+ print(f" Dashboard: http://localhost:{status.get('port', 7788)}")
182
+ print(f" Logs : {LOG_FILE}")
183
+
184
+
185
+ # ── Internal helpers ─────────────────────────────────────────────────────────
186
+
187
+ def read_pid() -> int | None:
188
+ try:
189
+ return int(PID_FILE.read_text().strip())
190
+ except (FileNotFoundError, ValueError):
191
+ return None
192
+
193
+
194
+ def _write_pid(pid: int) -> None:
195
+ _ensure_dir()
196
+ PID_FILE.write_text(str(pid) + "\n")
197
+
198
+
199
+ def _cleanup_pid() -> None:
200
+ PID_FILE.unlink(missing_ok=True)
201
+ META_FILE.unlink(missing_ok=True)
202
+
203
+
204
+ def _write_meta(meta: dict) -> None:
205
+ _ensure_dir()
206
+ META_FILE.write_text(json.dumps(meta, indent=2) + "\n", encoding="utf-8")
207
+
208
+
209
+ def _read_meta() -> dict:
210
+ try:
211
+ return json.loads(META_FILE.read_text(encoding="utf-8"))
212
+ except (FileNotFoundError, json.JSONDecodeError):
213
+ return {}
214
+
215
+
216
+ def _is_alive(pid: int) -> bool:
217
+ try:
218
+ os.kill(pid, 0)
219
+ return True
220
+ except (ProcessLookupError, PermissionError):
221
+ return False
pythonclaw/init.py ADDED
@@ -0,0 +1,61 @@
1
+ """
2
+ Project initialiser for pythonclaw.
3
+
4
+ Creates a `context/` directory with the required sub-directories and copies
5
+ template files so the agent has a ready-to-use starting point.
6
+
7
+ Sub-directories created
8
+ -----------------------
9
+ context/memory/ — MEMORY.md + daily YYYY-MM-DD.md logs
10
+ context/knowledge/ — .txt / .md knowledge-base files for RAG
11
+ context/skills/ — skill directories (copied from templates)
12
+ context/persona/ — persona .md files
13
+ context/soul/ — SOUL.md identity document
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import os
19
+ import shutil
20
+
21
+
22
+ def init(project_path: str = ".") -> None:
23
+ """
24
+ Initialise a new PythonClaw project at *project_path*.
25
+
26
+ Copies template files into `<project_path>/context/` only for
27
+ directories that do not already exist (safe to re-run).
28
+ """
29
+ application_dir = os.path.abspath(project_path)
30
+ context_dir = os.path.join(application_dir, "context")
31
+ pkg_dir = os.path.dirname(os.path.abspath(__file__))
32
+ templates_dir = os.path.join(pkg_dir, "templates")
33
+
34
+ print(f"Initialising PythonClaw in: {application_dir}")
35
+
36
+ if not os.path.exists(templates_dir):
37
+ print("Warning: templates directory not found — using minimal fallbacks.")
38
+
39
+ for component in ("memory", "knowledge", "skills", "persona", "soul"):
40
+ target = os.path.join(context_dir, component)
41
+ source = os.path.join(templates_dir, component)
42
+
43
+ if os.path.exists(target):
44
+ print(f" - {target} already exists, skipping.")
45
+ continue
46
+
47
+ if os.path.isdir(source):
48
+ shutil.copytree(source, target)
49
+ print(f" - Created {target} (from template)")
50
+ else:
51
+ os.makedirs(target, exist_ok=True)
52
+ print(f" - Created {target} (empty)")
53
+ # Minimal fallback content for directories with no template
54
+ if component == "memory":
55
+ with open(os.path.join(target, "MEMORY.md"), "w") as f:
56
+ f.write("# Long-Term Memory\n")
57
+ elif component == "knowledge":
58
+ with open(os.path.join(target, "README.txt"), "w") as f:
59
+ f.write("Add your knowledge-base .txt files here.\n")
60
+
61
+ print("\nInitialisation complete. Start your agent with this context directory.")