copaw-worker 0.1.0__tar.gz
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.
- copaw_worker-0.1.0/PKG-INFO +29 -0
- copaw_worker-0.1.0/README.md +17 -0
- copaw_worker-0.1.0/pyproject.toml +25 -0
- copaw_worker-0.1.0/setup.cfg +4 -0
- copaw_worker-0.1.0/src/copaw_worker/__init__.py +1 -0
- copaw_worker-0.1.0/src/copaw_worker/bridge.py +226 -0
- copaw_worker-0.1.0/src/copaw_worker/cli.py +63 -0
- copaw_worker-0.1.0/src/copaw_worker/config.py +28 -0
- copaw_worker-0.1.0/src/copaw_worker/matrix_channel.py +376 -0
- copaw_worker-0.1.0/src/copaw_worker/sync.py +202 -0
- copaw_worker-0.1.0/src/copaw_worker/worker.py +346 -0
- copaw_worker-0.1.0/src/copaw_worker.egg-info/PKG-INFO +29 -0
- copaw_worker-0.1.0/src/copaw_worker.egg-info/SOURCES.txt +15 -0
- copaw_worker-0.1.0/src/copaw_worker.egg-info/dependency_links.txt +1 -0
- copaw_worker-0.1.0/src/copaw_worker.egg-info/entry_points.txt +2 -0
- copaw_worker-0.1.0/src/copaw_worker.egg-info/requires.txt +2 -0
- copaw_worker-0.1.0/src/copaw_worker.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: copaw-worker
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lightweight HiClaw Worker runtime based on CoPaw
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Project-URL: Homepage, https://github.com/higress-group/hiclaw
|
|
7
|
+
Project-URL: Repository, https://github.com/higress-group/hiclaw
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: copaw>=0.0.5
|
|
11
|
+
Requires-Dist: matrix-nio>=0.24.0
|
|
12
|
+
|
|
13
|
+
# copaw-worker
|
|
14
|
+
|
|
15
|
+
Lightweight [HiClaw](https://github.com/higress-group/hiclaw) Worker runtime based on [CoPaw](https://github.com/agentscope-ai/CoPaw).
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install copaw-worker
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
copaw-worker --name <worker-name> --fs <minio-endpoint> --fs-key <access-key> --fs-secret <secret-key>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
See [HiClaw worker guide](https://github.com/higress-group/hiclaw/blob/main/docs/worker-guide.md) for full setup instructions.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# copaw-worker
|
|
2
|
+
|
|
3
|
+
Lightweight [HiClaw](https://github.com/higress-group/hiclaw) Worker runtime based on [CoPaw](https://github.com/agentscope-ai/CoPaw).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install copaw-worker
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
copaw-worker --name <worker-name> --fs <minio-endpoint> --fs-key <access-key> --fs-secret <secret-key>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
See [HiClaw worker guide](https://github.com/higress-group/hiclaw/blob/main/docs/worker-guide.md) for full setup instructions.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "copaw-worker"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Lightweight HiClaw Worker runtime based on CoPaw"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "Apache-2.0" }
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"copaw>=0.0.5",
|
|
14
|
+
"matrix-nio>=0.24.0",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.urls]
|
|
18
|
+
Homepage = "https://github.com/higress-group/hiclaw"
|
|
19
|
+
Repository = "https://github.com/higress-group/hiclaw"
|
|
20
|
+
|
|
21
|
+
[project.scripts]
|
|
22
|
+
copaw-worker = "copaw_worker.cli:main"
|
|
23
|
+
|
|
24
|
+
[tool.setuptools.packages.find]
|
|
25
|
+
where = ["src"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# copaw-worker: HiClaw Worker runtime based on CoPaw
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Bridge: translate openclaw.json (HiClaw Worker config) into CoPaw's
|
|
3
|
+
config.json + providers.json, then set COPAW_WORKING_DIR so CoPaw
|
|
4
|
+
picks up the right workspace.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import shutil
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _port_remap(url: str, is_container: bool) -> str:
|
|
16
|
+
"""Remap container-internal :8080 to host-exposed :18080 when needed."""
|
|
17
|
+
if not is_container and url and ":8080" in url:
|
|
18
|
+
return url.replace(":8080", ":18080")
|
|
19
|
+
return url
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _is_in_container() -> bool:
|
|
23
|
+
return Path("/.dockerenv").exists()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _secret_dir(working_dir: Path) -> Path:
|
|
27
|
+
"""Return the secret dir path that copaw uses alongside working_dir."""
|
|
28
|
+
return Path(str(working_dir) + ".secret")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _patch_copaw_paths(working_dir: Path) -> None:
|
|
32
|
+
"""Patch copaw's module-level path constants to point at working_dir.
|
|
33
|
+
|
|
34
|
+
copaw.constant captures WORKING_DIR / SECRET_DIR at import time from
|
|
35
|
+
env vars, so setting COPAW_WORKING_DIR after import has no effect.
|
|
36
|
+
We must update the live module objects directly.
|
|
37
|
+
"""
|
|
38
|
+
secret_dir = _secret_dir(working_dir)
|
|
39
|
+
secret_dir.mkdir(parents=True, exist_ok=True)
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
import copaw.constant as _const
|
|
43
|
+
_const.WORKING_DIR = working_dir
|
|
44
|
+
_const.SECRET_DIR = secret_dir
|
|
45
|
+
_const.ACTIVE_SKILLS_DIR = working_dir / "active_skills"
|
|
46
|
+
_const.CUSTOMIZED_SKILLS_DIR = working_dir / "customized_skills"
|
|
47
|
+
_const.MEMORY_DIR = working_dir / "memory"
|
|
48
|
+
_const.CUSTOM_CHANNELS_DIR = working_dir / "custom_channels"
|
|
49
|
+
_const.MODELS_DIR = working_dir / "models"
|
|
50
|
+
except ImportError:
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
import copaw.providers.store as _store
|
|
55
|
+
_store._PROVIDERS_JSON = secret_dir / "providers.json"
|
|
56
|
+
_store._LEGACY_PROVIDERS_JSON_CANDIDATES = (
|
|
57
|
+
Path(__file__).resolve().parent / "providers.json",
|
|
58
|
+
working_dir / "providers.json",
|
|
59
|
+
)
|
|
60
|
+
except ImportError:
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
import copaw.envs.store as _envs
|
|
65
|
+
_envs._BOOTSTRAP_WORKING_DIR = working_dir
|
|
66
|
+
_envs._BOOTSTRAP_SECRET_DIR = secret_dir
|
|
67
|
+
_envs._ENVS_JSON = secret_dir / "envs.json"
|
|
68
|
+
_envs._LEGACY_ENVS_JSON_CANDIDATES = (working_dir / "envs.json",)
|
|
69
|
+
except (ImportError, AttributeError):
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def bridge_openclaw_to_copaw(
|
|
74
|
+
openclaw_cfg: dict[str, Any],
|
|
75
|
+
working_dir: Path,
|
|
76
|
+
) -> None:
|
|
77
|
+
"""
|
|
78
|
+
Read openclaw_cfg (parsed openclaw.json) and write:
|
|
79
|
+
- <working_dir>/config.json (channels + agents)
|
|
80
|
+
- <working_dir>/providers.json (LLM credentials, for reference)
|
|
81
|
+
- <working_dir>.secret/providers.json (where copaw actually reads from)
|
|
82
|
+
|
|
83
|
+
Also sets COPAW_WORKING_DIR env var and patches copaw's module-level
|
|
84
|
+
path constants so the running process uses the correct directory.
|
|
85
|
+
"""
|
|
86
|
+
working_dir.mkdir(parents=True, exist_ok=True)
|
|
87
|
+
in_container = _is_in_container()
|
|
88
|
+
|
|
89
|
+
_write_config_json(openclaw_cfg, working_dir, in_container)
|
|
90
|
+
_write_providers_json(openclaw_cfg, working_dir, in_container)
|
|
91
|
+
|
|
92
|
+
os.environ["COPAW_WORKING_DIR"] = str(working_dir)
|
|
93
|
+
|
|
94
|
+
# Patch module-level constants (import-time values won't reflect env change)
|
|
95
|
+
_patch_copaw_paths(working_dir)
|
|
96
|
+
|
|
97
|
+
# Copy providers.json into secret_dir — that's where copaw actually reads it
|
|
98
|
+
secret_dir = _secret_dir(working_dir)
|
|
99
|
+
providers_src = working_dir / "providers.json"
|
|
100
|
+
if providers_src.exists():
|
|
101
|
+
shutil.copy2(providers_src, secret_dir / "providers.json")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# ---------------------------------------------------------------------------
|
|
105
|
+
# config.json
|
|
106
|
+
# ---------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
def _write_config_json(
|
|
109
|
+
cfg: dict[str, Any],
|
|
110
|
+
working_dir: Path,
|
|
111
|
+
in_container: bool,
|
|
112
|
+
) -> None:
|
|
113
|
+
matrix_raw = cfg.get("channels", {}).get("matrix", {})
|
|
114
|
+
homeserver = _port_remap(
|
|
115
|
+
matrix_raw.get("homeserver", ""), in_container
|
|
116
|
+
)
|
|
117
|
+
access_token = matrix_raw.get("accessToken", "")
|
|
118
|
+
|
|
119
|
+
# DM allowlist
|
|
120
|
+
dm_cfg = matrix_raw.get("dm", {})
|
|
121
|
+
dm_policy = dm_cfg.get("policy", "allowlist")
|
|
122
|
+
dm_allow_from: list[str] = dm_cfg.get("allowFrom", [])
|
|
123
|
+
|
|
124
|
+
# Group allowlist
|
|
125
|
+
group_policy = matrix_raw.get("groupPolicy", "allowlist")
|
|
126
|
+
group_allow_from: list[str] = matrix_raw.get("groupAllowFrom", [])
|
|
127
|
+
|
|
128
|
+
# Per-room/group config (pass through as-is for MatrixChannel to use)
|
|
129
|
+
groups = matrix_raw.get("groups", {})
|
|
130
|
+
|
|
131
|
+
matrix_channel_cfg = {
|
|
132
|
+
"enabled": matrix_raw.get("enabled", True),
|
|
133
|
+
"homeserver": homeserver,
|
|
134
|
+
"access_token": access_token,
|
|
135
|
+
"dm_policy": dm_policy,
|
|
136
|
+
"allow_from": dm_allow_from,
|
|
137
|
+
"group_policy": group_policy,
|
|
138
|
+
"group_allow_from": group_allow_from,
|
|
139
|
+
"groups": groups,
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
config_path = working_dir / "config.json"
|
|
143
|
+
# Merge with existing config to avoid clobbering other settings
|
|
144
|
+
existing: dict[str, Any] = {}
|
|
145
|
+
if config_path.exists():
|
|
146
|
+
with open(config_path) as f:
|
|
147
|
+
existing = json.load(f)
|
|
148
|
+
|
|
149
|
+
existing.setdefault("channels", {})["matrix"] = matrix_channel_cfg
|
|
150
|
+
# Disable console channel (we use Matrix)
|
|
151
|
+
existing["channels"].setdefault("console", {})["enabled"] = False
|
|
152
|
+
|
|
153
|
+
with open(config_path, "w") as f:
|
|
154
|
+
json.dump(existing, f, indent=2, ensure_ascii=False)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
158
|
+
# providers.json
|
|
159
|
+
# ---------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
def _write_providers_json(
|
|
162
|
+
cfg: dict[str, Any],
|
|
163
|
+
working_dir: Path,
|
|
164
|
+
in_container: bool,
|
|
165
|
+
) -> None:
|
|
166
|
+
providers_raw = cfg.get("models", {}).get("providers", {})
|
|
167
|
+
|
|
168
|
+
custom_providers: dict[str, Any] = {}
|
|
169
|
+
active_provider_id = ""
|
|
170
|
+
active_model = ""
|
|
171
|
+
|
|
172
|
+
for provider_id, provider_cfg in providers_raw.items():
|
|
173
|
+
base_url = _port_remap(
|
|
174
|
+
provider_cfg.get("baseUrl", ""), in_container
|
|
175
|
+
)
|
|
176
|
+
api_key = provider_cfg.get("apiKey", "")
|
|
177
|
+
|
|
178
|
+
models_raw = provider_cfg.get("models", [])
|
|
179
|
+
models = [
|
|
180
|
+
{"id": m["id"], "name": m.get("name", m["id"])}
|
|
181
|
+
for m in models_raw
|
|
182
|
+
if m.get("id")
|
|
183
|
+
]
|
|
184
|
+
|
|
185
|
+
custom_providers[provider_id] = {
|
|
186
|
+
"id": provider_id,
|
|
187
|
+
"name": provider_id,
|
|
188
|
+
"default_base_url": base_url,
|
|
189
|
+
"api_key_prefix": "",
|
|
190
|
+
"models": models,
|
|
191
|
+
"base_url": base_url,
|
|
192
|
+
"api_key": api_key,
|
|
193
|
+
"chat_model": "OpenAIChatModel",
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
# Use first provider + first model as active LLM
|
|
197
|
+
if not active_provider_id and models:
|
|
198
|
+
active_provider_id = provider_id
|
|
199
|
+
active_model = models[0]["id"]
|
|
200
|
+
|
|
201
|
+
# Resolve active model from agents.defaults.model.primary
|
|
202
|
+
# Format: "provider_id/model_id"
|
|
203
|
+
primary = (
|
|
204
|
+
cfg.get("agents", {})
|
|
205
|
+
.get("defaults", {})
|
|
206
|
+
.get("model", {})
|
|
207
|
+
.get("primary", "")
|
|
208
|
+
)
|
|
209
|
+
if primary and "/" in primary:
|
|
210
|
+
pid, mid = primary.split("/", 1)
|
|
211
|
+
if pid in custom_providers:
|
|
212
|
+
active_provider_id = pid
|
|
213
|
+
active_model = mid
|
|
214
|
+
|
|
215
|
+
providers_data: dict[str, Any] = {
|
|
216
|
+
"providers": {},
|
|
217
|
+
"custom_providers": custom_providers,
|
|
218
|
+
"active_llm": {
|
|
219
|
+
"provider_id": active_provider_id,
|
|
220
|
+
"model": active_model,
|
|
221
|
+
},
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
providers_path = working_dir / "providers.json"
|
|
225
|
+
with open(providers_path, "w") as f:
|
|
226
|
+
json.dump(providers_data, f, indent=2, ensure_ascii=False)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""CLI entry point: copaw-worker"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import logging
|
|
6
|
+
import signal
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
|
|
12
|
+
from copaw_worker.config import WorkerConfig
|
|
13
|
+
from copaw_worker.worker import Worker
|
|
14
|
+
|
|
15
|
+
logging.basicConfig(
|
|
16
|
+
level=logging.INFO,
|
|
17
|
+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def main() -> None:
|
|
22
|
+
"""Entry point registered in pyproject.toml."""
|
|
23
|
+
|
|
24
|
+
def _run(
|
|
25
|
+
name: str = typer.Option(..., "--name", help="Worker name"),
|
|
26
|
+
fs: str = typer.Option(..., "--fs", help="MinIO endpoint"),
|
|
27
|
+
fs_key: str = typer.Option(..., "--fs-key", help="MinIO access key"),
|
|
28
|
+
fs_secret: str = typer.Option(..., "--fs-secret", help="MinIO secret key"),
|
|
29
|
+
fs_bucket: str = typer.Option("hiclaw-storage", "--fs-bucket", help="MinIO bucket"),
|
|
30
|
+
sync_interval: int = typer.Option(300, "--sync-interval", help="Sync interval (seconds)"),
|
|
31
|
+
install_dir: Optional[str] = typer.Option(None, "--install-dir", help="Base install dir"),
|
|
32
|
+
console_port: Optional[int] = typer.Option(None, "--console-port", help="Enable web console on this port (e.g. 8088, costs ~500MB extra RAM)"),
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Start the CoPaw Worker and connect to Matrix."""
|
|
35
|
+
config = WorkerConfig(
|
|
36
|
+
worker_name=name,
|
|
37
|
+
minio_endpoint=fs,
|
|
38
|
+
minio_access_key=fs_key,
|
|
39
|
+
minio_secret_key=fs_secret,
|
|
40
|
+
minio_bucket=fs_bucket,
|
|
41
|
+
sync_interval=sync_interval,
|
|
42
|
+
install_dir=Path(install_dir) if install_dir else None,
|
|
43
|
+
console_port=console_port,
|
|
44
|
+
)
|
|
45
|
+
worker = Worker(config)
|
|
46
|
+
|
|
47
|
+
async def _async_run() -> None:
|
|
48
|
+
loop = asyncio.get_running_loop()
|
|
49
|
+
|
|
50
|
+
def _shutdown() -> None:
|
|
51
|
+
asyncio.create_task(worker.stop())
|
|
52
|
+
|
|
53
|
+
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
54
|
+
loop.add_signal_handler(sig, _shutdown)
|
|
55
|
+
|
|
56
|
+
await worker.run()
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
asyncio.run(_async_run())
|
|
60
|
+
except KeyboardInterrupt:
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
typer.run(_run)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""WorkerConfig: parsed from CLI args / env vars."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class WorkerConfig:
|
|
8
|
+
def __init__(
|
|
9
|
+
self,
|
|
10
|
+
worker_name: str,
|
|
11
|
+
minio_endpoint: str,
|
|
12
|
+
minio_access_key: str,
|
|
13
|
+
minio_secret_key: str,
|
|
14
|
+
minio_bucket: str = "hiclaw-storage",
|
|
15
|
+
minio_secure: bool = False,
|
|
16
|
+
sync_interval: int = 300,
|
|
17
|
+
install_dir: Path | None = None,
|
|
18
|
+
console_port: int | None = None,
|
|
19
|
+
) -> None:
|
|
20
|
+
self.worker_name = worker_name
|
|
21
|
+
self.minio_endpoint = minio_endpoint
|
|
22
|
+
self.minio_access_key = minio_access_key
|
|
23
|
+
self.minio_secret_key = minio_secret_key
|
|
24
|
+
self.minio_bucket = minio_bucket
|
|
25
|
+
self.minio_secure = minio_secure
|
|
26
|
+
self.sync_interval = sync_interval
|
|
27
|
+
self.install_dir = install_dir or Path.home() / ".copaw-worker"
|
|
28
|
+
self.console_port = console_port
|