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.
- pythonclaw/__init__.py +17 -0
- pythonclaw/__main__.py +6 -0
- pythonclaw/channels/discord_bot.py +231 -0
- pythonclaw/channels/telegram_bot.py +236 -0
- pythonclaw/config.py +190 -0
- pythonclaw/core/__init__.py +25 -0
- pythonclaw/core/agent.py +773 -0
- pythonclaw/core/compaction.py +220 -0
- pythonclaw/core/knowledge/rag.py +93 -0
- pythonclaw/core/llm/anthropic_client.py +107 -0
- pythonclaw/core/llm/base.py +26 -0
- pythonclaw/core/llm/gemini_client.py +139 -0
- pythonclaw/core/llm/openai_compatible.py +39 -0
- pythonclaw/core/llm/response.py +57 -0
- pythonclaw/core/memory/manager.py +120 -0
- pythonclaw/core/memory/storage.py +164 -0
- pythonclaw/core/persistent_agent.py +103 -0
- pythonclaw/core/retrieval/__init__.py +6 -0
- pythonclaw/core/retrieval/chunker.py +78 -0
- pythonclaw/core/retrieval/dense.py +152 -0
- pythonclaw/core/retrieval/fusion.py +51 -0
- pythonclaw/core/retrieval/reranker.py +112 -0
- pythonclaw/core/retrieval/retriever.py +166 -0
- pythonclaw/core/retrieval/sparse.py +69 -0
- pythonclaw/core/session_store.py +269 -0
- pythonclaw/core/skill_loader.py +322 -0
- pythonclaw/core/skillhub.py +290 -0
- pythonclaw/core/tools.py +622 -0
- pythonclaw/core/utils.py +64 -0
- pythonclaw/daemon.py +221 -0
- pythonclaw/init.py +61 -0
- pythonclaw/main.py +489 -0
- pythonclaw/onboard.py +290 -0
- pythonclaw/scheduler/cron.py +310 -0
- pythonclaw/scheduler/heartbeat.py +178 -0
- pythonclaw/server.py +145 -0
- pythonclaw/session_manager.py +104 -0
- pythonclaw/templates/persona/demo_persona.md +2 -0
- pythonclaw/templates/skills/communication/CATEGORY.md +4 -0
- pythonclaw/templates/skills/communication/email/SKILL.md +54 -0
- pythonclaw/templates/skills/communication/email/__pycache__/send_email.cpython-311.pyc +0 -0
- pythonclaw/templates/skills/communication/email/send_email.py +88 -0
- pythonclaw/templates/skills/data/CATEGORY.md +4 -0
- pythonclaw/templates/skills/data/csv_analyzer/SKILL.md +51 -0
- pythonclaw/templates/skills/data/csv_analyzer/__pycache__/analyze.cpython-311.pyc +0 -0
- pythonclaw/templates/skills/data/csv_analyzer/analyze.py +138 -0
- pythonclaw/templates/skills/data/finance/SKILL.md +41 -0
- pythonclaw/templates/skills/data/finance/__pycache__/fetch_quote.cpython-311.pyc +0 -0
- pythonclaw/templates/skills/data/finance/fetch_quote.py +118 -0
- pythonclaw/templates/skills/data/news/SKILL.md +39 -0
- pythonclaw/templates/skills/data/news/__pycache__/search_news.cpython-311.pyc +0 -0
- pythonclaw/templates/skills/data/news/search_news.py +57 -0
- pythonclaw/templates/skills/data/pdf_reader/SKILL.md +40 -0
- pythonclaw/templates/skills/data/pdf_reader/__pycache__/read_pdf.cpython-311.pyc +0 -0
- pythonclaw/templates/skills/data/pdf_reader/read_pdf.py +113 -0
- pythonclaw/templates/skills/data/scraper/SKILL.md +39 -0
- pythonclaw/templates/skills/data/scraper/__pycache__/scrape.cpython-311.pyc +0 -0
- pythonclaw/templates/skills/data/scraper/scrape.py +92 -0
- pythonclaw/templates/skills/data/weather/SKILL.md +42 -0
- pythonclaw/templates/skills/data/weather/__pycache__/weather.cpython-311.pyc +0 -0
- pythonclaw/templates/skills/data/weather/weather.py +142 -0
- pythonclaw/templates/skills/data/youtube/SKILL.md +43 -0
- pythonclaw/templates/skills/data/youtube/__pycache__/youtube_info.cpython-311.pyc +0 -0
- pythonclaw/templates/skills/data/youtube/youtube_info.py +167 -0
- pythonclaw/templates/skills/dev/CATEGORY.md +4 -0
- pythonclaw/templates/skills/dev/code_runner/SKILL.md +46 -0
- pythonclaw/templates/skills/dev/code_runner/__pycache__/run_code.cpython-311.pyc +0 -0
- pythonclaw/templates/skills/dev/code_runner/run_code.py +117 -0
- pythonclaw/templates/skills/dev/github/SKILL.md +52 -0
- pythonclaw/templates/skills/dev/github/__pycache__/gh.cpython-311.pyc +0 -0
- pythonclaw/templates/skills/dev/github/gh.py +165 -0
- pythonclaw/templates/skills/dev/http_request/SKILL.md +40 -0
- pythonclaw/templates/skills/dev/http_request/__pycache__/request.cpython-311.pyc +0 -0
- pythonclaw/templates/skills/dev/http_request/request.py +90 -0
- pythonclaw/templates/skills/google/CATEGORY.md +4 -0
- pythonclaw/templates/skills/google/workspace/SKILL.md +98 -0
- pythonclaw/templates/skills/google/workspace/check_setup.sh +52 -0
- pythonclaw/templates/skills/meta/CATEGORY.md +4 -0
- pythonclaw/templates/skills/meta/skill_creator/SKILL.md +151 -0
- pythonclaw/templates/skills/system/CATEGORY.md +4 -0
- pythonclaw/templates/skills/system/change_persona/SKILL.md +41 -0
- pythonclaw/templates/skills/system/change_setting/SKILL.md +65 -0
- pythonclaw/templates/skills/system/change_setting/__pycache__/update_config.cpython-311.pyc +0 -0
- pythonclaw/templates/skills/system/change_setting/update_config.py +129 -0
- pythonclaw/templates/skills/system/change_soul/SKILL.md +41 -0
- pythonclaw/templates/skills/system/onboarding/SKILL.md +63 -0
- pythonclaw/templates/skills/system/onboarding/__pycache__/write_identity.cpython-311.pyc +0 -0
- pythonclaw/templates/skills/system/onboarding/write_identity.py +218 -0
- pythonclaw/templates/skills/system/random/SKILL.md +33 -0
- pythonclaw/templates/skills/system/random/__pycache__/random_util.cpython-311.pyc +0 -0
- pythonclaw/templates/skills/system/random/random_util.py +45 -0
- pythonclaw/templates/skills/system/time/SKILL.md +33 -0
- pythonclaw/templates/skills/system/time/__pycache__/time_util.cpython-311.pyc +0 -0
- pythonclaw/templates/skills/system/time/time_util.py +81 -0
- pythonclaw/templates/skills/text/CATEGORY.md +4 -0
- pythonclaw/templates/skills/text/translator/SKILL.md +47 -0
- pythonclaw/templates/skills/text/translator/__pycache__/translate.cpython-311.pyc +0 -0
- pythonclaw/templates/skills/text/translator/translate.py +66 -0
- pythonclaw/templates/skills/web/CATEGORY.md +4 -0
- pythonclaw/templates/skills/web/tavily/SKILL.md +61 -0
- pythonclaw/templates/soul/SOUL.md +54 -0
- pythonclaw/web/__init__.py +1 -0
- pythonclaw/web/app.py +585 -0
- pythonclaw/web/static/favicon.png +0 -0
- pythonclaw/web/static/index.html +1318 -0
- pythonclaw/web/static/logo.png +0 -0
- pythonclaw-0.2.0.dist-info/METADATA +410 -0
- pythonclaw-0.2.0.dist-info/RECORD +112 -0
- pythonclaw-0.2.0.dist-info/WHEEL +5 -0
- pythonclaw-0.2.0.dist-info/entry_points.txt +2 -0
- pythonclaw-0.2.0.dist-info/licenses/LICENSE +21 -0
- 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.")
|