chaoscypher-neuron 0.1.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.
- chaoscypher_neuron/__init__.py +49 -0
- chaoscypher_neuron/config.py +327 -0
- chaoscypher_neuron/handlers/__init__.py +40 -0
- chaoscypher_neuron/handlers/chat_completion.py +931 -0
- chaoscypher_neuron/handlers/quality_scores.py +176 -0
- chaoscypher_neuron/handlers/template_embedding.py +147 -0
- chaoscypher_neuron/py.typed +0 -0
- chaoscypher_neuron/recovery/__init__.py +18 -0
- chaoscypher_neuron/recovery/extraction.py +226 -0
- chaoscypher_neuron/recovery/sources.py +96 -0
- chaoscypher_neuron/search_sweep.py +318 -0
- chaoscypher_neuron/settings_sync.py +337 -0
- chaoscypher_neuron/setup/__init__.py +20 -0
- chaoscypher_neuron/setup/llm_handlers.py +113 -0
- chaoscypher_neuron/setup/ops_handlers.py +373 -0
- chaoscypher_neuron/setup/shared.py +146 -0
- chaoscypher_neuron/types.py +58 -0
- chaoscypher_neuron/worker.py +1041 -0
- chaoscypher_neuron-0.1.0.dist-info/METADATA +185 -0
- chaoscypher_neuron-0.1.0.dist-info/RECORD +23 -0
- chaoscypher_neuron-0.1.0.dist-info/WHEEL +5 -0
- chaoscypher_neuron-0.1.0.dist-info/entry_points.txt +2 -0
- chaoscypher_neuron-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Copyright (C) 2024-2026 Chaos Cypher, Inc.
|
|
2
|
+
# SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
|
|
4
|
+
"""Chaos Cypher Neuron - Background Task Processing Workers.
|
|
5
|
+
|
|
6
|
+
Background task processing capabilities with a unified worker that handles
|
|
7
|
+
both LLM and Operations queues concurrently. Part of the Neural Architecture.
|
|
8
|
+
|
|
9
|
+
Queue Configuration:
|
|
10
|
+
- LLM queue: Serialized AI operations (default 1 concurrent task)
|
|
11
|
+
(chat, embeddings, tool calls, chunk extraction)
|
|
12
|
+
- Operations queue: Parallel processing (default 8 concurrent tasks)
|
|
13
|
+
(source processing, exports, workflows, bulk operations)
|
|
14
|
+
|
|
15
|
+
Components:
|
|
16
|
+
- worker: Unified entry point (cc-neuron) running both queues
|
|
17
|
+
- config: Worker configuration management
|
|
18
|
+
|
|
19
|
+
Example:
|
|
20
|
+
Start the unified worker via CLI entry point::
|
|
21
|
+
|
|
22
|
+
# Start unified worker (both LLM and Operations queues)
|
|
23
|
+
cc-neuron
|
|
24
|
+
|
|
25
|
+
For programmatic access to configuration::
|
|
26
|
+
|
|
27
|
+
from chaoscypher_neuron.config import load_worker_config
|
|
28
|
+
|
|
29
|
+
config = load_worker_config("llm_worker")
|
|
30
|
+
llm_timeout = config["timeout"]
|
|
31
|
+
|
|
32
|
+
Note:
|
|
33
|
+
Workers require Valkey connection. Configure via environment:
|
|
34
|
+
- QUEUE_HOST: Valkey hostname (default: valkey)
|
|
35
|
+
- QUEUE_PORT: Valkey port (default: 6379)
|
|
36
|
+
|
|
37
|
+
See Also:
|
|
38
|
+
- packages/docker/multi-container/docker-compose.dev.yml for service orchestration
|
|
39
|
+
- packages/cortex/src/chaoscypher_cortex/shared/config for timeout/retry settings
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
__version__ = "0.1.0"
|
|
43
|
+
|
|
44
|
+
# Export config functions and types for external use
|
|
45
|
+
from chaoscypher_neuron.config import load_worker_config
|
|
46
|
+
from chaoscypher_neuron.types import WorkerContext
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
__all__ = ["WorkerContext", "load_worker_config"]
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
# Copyright (C) 2024-2026 Chaos Cypher, Inc.
|
|
2
|
+
# SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
|
|
4
|
+
"""Worker Configuration - Defaults with Optional User Override.
|
|
5
|
+
|
|
6
|
+
This module provides worker configuration with sensible defaults baked in.
|
|
7
|
+
Users can optionally create /data/workers.yaml to override any settings.
|
|
8
|
+
|
|
9
|
+
If the config file doesn't exist, defaults are used automatically (no file created).
|
|
10
|
+
|
|
11
|
+
NOTE: Default timeout and retry values are synchronized with backend/shared/config/__init__.py
|
|
12
|
+
(TimeoutSettings and RetrySettings) to maintain consistency.
|
|
13
|
+
|
|
14
|
+
Config file format (/data/workers.yaml)::
|
|
15
|
+
|
|
16
|
+
llm_worker:
|
|
17
|
+
max_concurrent: 2
|
|
18
|
+
timeout: 600
|
|
19
|
+
|
|
20
|
+
operations_worker:
|
|
21
|
+
max_concurrent: 16
|
|
22
|
+
timeout: 7200
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import copy
|
|
26
|
+
import functools
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
import structlog
|
|
31
|
+
import yaml
|
|
32
|
+
from pydantic import BaseModel, Field
|
|
33
|
+
|
|
34
|
+
from chaoscypher_core import policy
|
|
35
|
+
from chaoscypher_core.constants import QUEUE_LLM, QUEUE_OPERATIONS
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
logger = structlog.get_logger(__name__)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ============================================================================
|
|
42
|
+
# Safety clamps for worker config — chosen to stay within Valkey + supervisor
|
|
43
|
+
# tolerances. Not operator-tunable (they encode infrastructure constraints).
|
|
44
|
+
# Expand only after verifying the higher bound has been load-tested.
|
|
45
|
+
# ============================================================================
|
|
46
|
+
_MAX_CONCURRENT_HARD_CAP = 64
|
|
47
|
+
_MAX_TIMEOUT_HARD_CAP_SECONDS = policy.SECONDS_PER_DAY # 86400
|
|
48
|
+
_MAX_TRIES_HARD_CAP = 20
|
|
49
|
+
_MIN_TIMEOUT_FLOOR_SECONDS = 60
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ============================================================================
|
|
53
|
+
# Neuron-Specific Settings
|
|
54
|
+
# ============================================================================
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class NeuronSettings(BaseModel):
|
|
58
|
+
"""Neuron-specific configuration values.
|
|
59
|
+
|
|
60
|
+
Centralises constants that were previously hardcoded across the
|
|
61
|
+
neuron package (settings sync, quality-score handler, etc.).
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
settings_sync_reconnect_delay: int = Field(
|
|
65
|
+
default=5,
|
|
66
|
+
ge=1,
|
|
67
|
+
description="Initial reconnect delay (seconds) for the settings pub/sub listener",
|
|
68
|
+
)
|
|
69
|
+
settings_sync_max_reconnect_delay: int = Field(
|
|
70
|
+
default=60,
|
|
71
|
+
ge=1,
|
|
72
|
+
description="Maximum reconnect delay (seconds) for the settings pub/sub listener",
|
|
73
|
+
)
|
|
74
|
+
max_quality_score_batch: int = Field(
|
|
75
|
+
default=500,
|
|
76
|
+
ge=1,
|
|
77
|
+
description="Maximum source IDs accepted in a single quality-score recalculation request",
|
|
78
|
+
)
|
|
79
|
+
run_worker_max_consecutive_failures: int = Field(
|
|
80
|
+
default=10,
|
|
81
|
+
ge=1,
|
|
82
|
+
description=(
|
|
83
|
+
"Maximum consecutive run_worker() failures the circuit-breaker will absorb "
|
|
84
|
+
"before re-raising and letting the container restart take over. Guards "
|
|
85
|
+
"against a poison-pill crash producing a CPU-burning restart loop."
|
|
86
|
+
),
|
|
87
|
+
)
|
|
88
|
+
run_worker_initial_backoff_seconds: float = Field(
|
|
89
|
+
default=5.0,
|
|
90
|
+
gt=0.0,
|
|
91
|
+
description=(
|
|
92
|
+
"Initial backoff (seconds) the run_worker() circuit-breaker sleeps after "
|
|
93
|
+
"the first failure. Doubles each subsequent failure up to "
|
|
94
|
+
"run_worker_max_backoff_seconds."
|
|
95
|
+
),
|
|
96
|
+
)
|
|
97
|
+
run_worker_max_backoff_seconds: float = Field(
|
|
98
|
+
default=300.0,
|
|
99
|
+
gt=0.0,
|
|
100
|
+
description=(
|
|
101
|
+
"Upper bound (seconds) on a single backoff sleep between run_worker() "
|
|
102
|
+
"restart attempts. Caps the exponential growth so the breaker never "
|
|
103
|
+
"stalls longer than this between attempts."
|
|
104
|
+
),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@functools.cache
|
|
109
|
+
def get_neuron_settings() -> NeuronSettings:
|
|
110
|
+
"""Return the singleton NeuronSettings instance.
|
|
111
|
+
|
|
112
|
+
Cached so import-time construction is deferred and subsequent calls
|
|
113
|
+
are free.
|
|
114
|
+
"""
|
|
115
|
+
return NeuronSettings()
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# ============================================================================
|
|
119
|
+
# DEFAULT CONFIGURATION (Synchronized with Settings)
|
|
120
|
+
# ============================================================================
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _get_default_config() -> dict[str, Any]:
|
|
124
|
+
"""Get default worker configuration synchronized with centralized Settings.
|
|
125
|
+
|
|
126
|
+
Returns defaults from TimeoutSettings and RetrySettings to prevent drift.
|
|
127
|
+
Falls back to hardcoded values if Settings cannot be loaded.
|
|
128
|
+
|
|
129
|
+
For LLM worker:
|
|
130
|
+
- If multiple Ollama instances are configured, max_concurrent is set to the
|
|
131
|
+
number of instances to allow parallel processing across instances.
|
|
132
|
+
- The load balancer handles per-instance concurrency control.
|
|
133
|
+
"""
|
|
134
|
+
try:
|
|
135
|
+
# Import at function level to avoid circular imports
|
|
136
|
+
from chaoscypher_core.app_config import (
|
|
137
|
+
RetrySettings,
|
|
138
|
+
TimeoutSettings,
|
|
139
|
+
WorkerSettings,
|
|
140
|
+
get_settings,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Get defaults from centralized Settings
|
|
144
|
+
timeouts = TimeoutSettings()
|
|
145
|
+
retries = RetrySettings()
|
|
146
|
+
workers = WorkerSettings()
|
|
147
|
+
|
|
148
|
+
# Calculate LLM worker max_concurrent based on Ollama instances
|
|
149
|
+
llm_max_concurrent = 1 # Default: single instance / non-Ollama
|
|
150
|
+
try:
|
|
151
|
+
settings = get_settings()
|
|
152
|
+
workers = settings.workers
|
|
153
|
+
if settings.llm.chat_provider == "ollama":
|
|
154
|
+
instances = settings.llm.ollama_instances or []
|
|
155
|
+
if instances:
|
|
156
|
+
enabled_count = sum(1 for i in instances if getattr(i, "enabled", True))
|
|
157
|
+
llm_max_concurrent = max(enabled_count, 1)
|
|
158
|
+
logger.info(
|
|
159
|
+
"llm_worker_max_concurrent_from_instances",
|
|
160
|
+
instance_count=len(instances),
|
|
161
|
+
enabled_count=enabled_count,
|
|
162
|
+
max_concurrent=llm_max_concurrent,
|
|
163
|
+
)
|
|
164
|
+
except (ImportError, AttributeError, ValueError) as e:
|
|
165
|
+
logger.debug(
|
|
166
|
+
"could_not_determine_instance_count",
|
|
167
|
+
error=str(e),
|
|
168
|
+
fallback_max_concurrent=llm_max_concurrent,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
"llm_worker": {
|
|
173
|
+
"max_concurrent": llm_max_concurrent,
|
|
174
|
+
"queue_name": QUEUE_LLM,
|
|
175
|
+
"timeout": timeouts.llm_worker_default, # 3600 = 1 hour
|
|
176
|
+
"max_tries": retries.llm_worker_max_tries, # 5
|
|
177
|
+
},
|
|
178
|
+
"operations_worker": {
|
|
179
|
+
"max_concurrent": workers.operations_max_concurrent,
|
|
180
|
+
"queue_name": QUEUE_OPERATIONS,
|
|
181
|
+
"timeout": timeouts.operations_worker_default, # 3600 = 1 hour
|
|
182
|
+
"max_tries": retries.operations_worker_max_tries, # 5
|
|
183
|
+
},
|
|
184
|
+
}
|
|
185
|
+
except (ImportError, KeyError, ValueError, FileNotFoundError, OSError) as e:
|
|
186
|
+
logger.warning(
|
|
187
|
+
"settings_load_failed_using_fallbacks",
|
|
188
|
+
error=str(e),
|
|
189
|
+
fallback_source="hardcoded_defaults",
|
|
190
|
+
)
|
|
191
|
+
return {
|
|
192
|
+
"llm_worker": {
|
|
193
|
+
"max_concurrent": 1,
|
|
194
|
+
"queue_name": QUEUE_LLM,
|
|
195
|
+
"timeout": 3600,
|
|
196
|
+
"max_tries": 5,
|
|
197
|
+
},
|
|
198
|
+
"operations_worker": {
|
|
199
|
+
"max_concurrent": 8, # Must match WorkerSettings.operations_max_concurrent default
|
|
200
|
+
"queue_name": QUEUE_OPERATIONS,
|
|
201
|
+
"timeout": 3600,
|
|
202
|
+
"max_tries": 5,
|
|
203
|
+
},
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@functools.cache # Startup-only; not invalidated on hot-reload (safe: load_worker_config called once)
|
|
208
|
+
def _get_defaults() -> dict[str, Any]:
|
|
209
|
+
"""Return the default config, building it on first call.
|
|
210
|
+
|
|
211
|
+
Uses functools.cache to lazy-build on first access, avoiding
|
|
212
|
+
calls to get_settings() at import time (before structlog is configured).
|
|
213
|
+
"""
|
|
214
|
+
return _get_default_config()
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# ============================================================================
|
|
218
|
+
# Configuration Loading
|
|
219
|
+
# ============================================================================
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def load_worker_config(worker_type: str) -> dict[str, Any]:
|
|
223
|
+
"""Load worker configuration for the specified worker type.
|
|
224
|
+
|
|
225
|
+
Looks for /data/workers.yaml. If found, overlays user settings on
|
|
226
|
+
top of defaults. If not found, uses defaults (no file created).
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
worker_type: Either "llm_worker" or "operations_worker"
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
Configuration dictionary for the worker
|
|
233
|
+
|
|
234
|
+
"""
|
|
235
|
+
defaults = _get_defaults()
|
|
236
|
+
if worker_type not in defaults:
|
|
237
|
+
msg = f"Unknown worker type: {worker_type}"
|
|
238
|
+
raise ValueError(msg)
|
|
239
|
+
|
|
240
|
+
# Start with defaults
|
|
241
|
+
config = copy.deepcopy(defaults[worker_type])
|
|
242
|
+
|
|
243
|
+
# Check for user override file
|
|
244
|
+
try:
|
|
245
|
+
from chaoscypher_core.app_config import PathSettings
|
|
246
|
+
|
|
247
|
+
path_settings = PathSettings()
|
|
248
|
+
user_config_path = Path(path_settings.data_dir) / path_settings.workers_config_filename
|
|
249
|
+
except Exception:
|
|
250
|
+
logger.debug("pathsettings_import_failed_using_fallback")
|
|
251
|
+
from chaoscypher_core.settings import PathSettings as CorePathSettings
|
|
252
|
+
|
|
253
|
+
_core_paths = CorePathSettings()
|
|
254
|
+
user_config_path = Path(_core_paths.data_dir) / _core_paths.workers_config_filename
|
|
255
|
+
|
|
256
|
+
if user_config_path.exists():
|
|
257
|
+
logger.info(
|
|
258
|
+
"user_config_found",
|
|
259
|
+
config_path=str(user_config_path),
|
|
260
|
+
worker_type=worker_type,
|
|
261
|
+
)
|
|
262
|
+
try:
|
|
263
|
+
with open(user_config_path) as f:
|
|
264
|
+
user_config = yaml.safe_load(f)
|
|
265
|
+
|
|
266
|
+
if user_config and worker_type in user_config:
|
|
267
|
+
user_overrides = user_config[worker_type]
|
|
268
|
+
allowed_keys = {"max_concurrent", "timeout", "max_tries"}
|
|
269
|
+
filtered = {k: v for k, v in user_overrides.items() if k in allowed_keys}
|
|
270
|
+
rejected = set(user_overrides) - allowed_keys
|
|
271
|
+
if rejected:
|
|
272
|
+
logger.warning(
|
|
273
|
+
"user_config_unknown_keys_ignored",
|
|
274
|
+
worker_type=worker_type,
|
|
275
|
+
rejected_keys=sorted(rejected),
|
|
276
|
+
)
|
|
277
|
+
logger.info(
|
|
278
|
+
"user_config_overrides_applied",
|
|
279
|
+
worker_type=worker_type,
|
|
280
|
+
override_keys=list(filtered.keys()),
|
|
281
|
+
)
|
|
282
|
+
config.update(filtered)
|
|
283
|
+
except Exception as e:
|
|
284
|
+
logger.exception(
|
|
285
|
+
"user_config_load_failed",
|
|
286
|
+
worker_type=worker_type,
|
|
287
|
+
error=str(e),
|
|
288
|
+
)
|
|
289
|
+
logger.warning(
|
|
290
|
+
"using_default_configuration",
|
|
291
|
+
worker_type=worker_type,
|
|
292
|
+
reason="user_config_load_error",
|
|
293
|
+
)
|
|
294
|
+
else:
|
|
295
|
+
logger.info(
|
|
296
|
+
"user_config_not_found_using_defaults",
|
|
297
|
+
config_path=str(user_config_path),
|
|
298
|
+
worker_type=worker_type,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
# Validate numeric types before clamping (YAML booleans silently coerce: True == 1)
|
|
302
|
+
numeric_keys = ("max_concurrent", "timeout", "max_tries")
|
|
303
|
+
for key in numeric_keys:
|
|
304
|
+
val = config.get(key)
|
|
305
|
+
if val is not None and (not isinstance(val, (int, float)) or isinstance(val, bool)):
|
|
306
|
+
logger.warning("worker_config_invalid_type", key=key, value=val, expected="numeric")
|
|
307
|
+
del config[key] # Let it fall back to default from base config
|
|
308
|
+
|
|
309
|
+
# Clamp values to safe ranges
|
|
310
|
+
config["max_concurrent"] = max(
|
|
311
|
+
1, min(config.get("max_concurrent", 1), _MAX_CONCURRENT_HARD_CAP)
|
|
312
|
+
)
|
|
313
|
+
config["timeout"] = max(
|
|
314
|
+
_MIN_TIMEOUT_FLOOR_SECONDS, min(config.get("timeout", 3600), _MAX_TIMEOUT_HARD_CAP_SECONDS)
|
|
315
|
+
)
|
|
316
|
+
config["max_tries"] = max(1, min(config.get("max_tries", 5), _MAX_TRIES_HARD_CAP))
|
|
317
|
+
|
|
318
|
+
logger.info(
|
|
319
|
+
"worker_config_loaded",
|
|
320
|
+
worker_type=worker_type,
|
|
321
|
+
queue_name=config["queue_name"],
|
|
322
|
+
max_concurrent=config["max_concurrent"],
|
|
323
|
+
timeout_seconds=config["timeout"],
|
|
324
|
+
max_tries=config["max_tries"],
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
return dict(config)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Copyright (C) 2024-2026 Chaos Cypher, Inc.
|
|
2
|
+
# SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
|
|
4
|
+
"""Queue handler registration modules for the Neuron worker.
|
|
5
|
+
|
|
6
|
+
Provides individual handler registration functions for specialized
|
|
7
|
+
queue tasks that are not part of a larger operations service.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
from .chat_completion import register_chat_completion_handler
|
|
13
|
+
from .quality_scores import register_quality_score_handler
|
|
14
|
+
from .template_embedding import register_template_embedding_handler
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
_SAFE_DB_NAME = re.compile(r"^[a-zA-Z0-9_-]+$")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def validate_database_name(name: str | None, fallback: str) -> str:
|
|
21
|
+
"""Validate a database name from a queue payload.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
name: The database name from the queue task data.
|
|
25
|
+
fallback: Default database name to use if validation fails.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
The validated database name, or the fallback if invalid.
|
|
29
|
+
"""
|
|
30
|
+
if name and _SAFE_DB_NAME.match(name):
|
|
31
|
+
return name
|
|
32
|
+
return fallback
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"register_chat_completion_handler",
|
|
37
|
+
"register_quality_score_handler",
|
|
38
|
+
"register_template_embedding_handler",
|
|
39
|
+
"validate_database_name",
|
|
40
|
+
]
|