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.
@@ -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
+ ]