conceptkernel 1.1.0__tar.gz → 1.2.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.
Files changed (28) hide show
  1. {conceptkernel-1.1.0 → conceptkernel-1.2.0}/PKG-INFO +1 -1
  2. {conceptkernel-1.1.0 → conceptkernel-1.2.0}/cklib/actions.py +45 -12
  3. {conceptkernel-1.1.0 → conceptkernel-1.2.0}/cklib/dispatch.py +30 -3
  4. conceptkernel-1.2.0/cklib/nats.py +183 -0
  5. {conceptkernel-1.1.0 → conceptkernel-1.2.0}/cklib/processor.py +1 -2
  6. {conceptkernel-1.1.0 → conceptkernel-1.2.0}/conceptkernel.egg-info/PKG-INFO +1 -1
  7. {conceptkernel-1.1.0 → conceptkernel-1.2.0}/conceptkernel.egg-info/SOURCES.txt +1 -0
  8. {conceptkernel-1.1.0 → conceptkernel-1.2.0}/pyproject.toml +1 -1
  9. {conceptkernel-1.1.0 → conceptkernel-1.2.0}/LICENSE +0 -0
  10. {conceptkernel-1.1.0 → conceptkernel-1.2.0}/README.md +0 -0
  11. {conceptkernel-1.1.0 → conceptkernel-1.2.0}/cklib/__init__.py +0 -0
  12. {conceptkernel-1.1.0 → conceptkernel-1.2.0}/cklib/auth.py +0 -0
  13. {conceptkernel-1.1.0 → conceptkernel-1.2.0}/cklib/capacity.py +0 -0
  14. {conceptkernel-1.1.0 → conceptkernel-1.2.0}/cklib/context.py +0 -0
  15. {conceptkernel-1.1.0 → conceptkernel-1.2.0}/cklib/entities.py +0 -0
  16. {conceptkernel-1.1.0 → conceptkernel-1.2.0}/cklib/events.py +0 -0
  17. {conceptkernel-1.1.0 → conceptkernel-1.2.0}/cklib/execution.py +0 -0
  18. {conceptkernel-1.1.0 → conceptkernel-1.2.0}/cklib/instance.py +0 -0
  19. {conceptkernel-1.1.0 → conceptkernel-1.2.0}/cklib/ledger.py +0 -0
  20. {conceptkernel-1.1.0 → conceptkernel-1.2.0}/cklib/prov.py +0 -0
  21. {conceptkernel-1.1.0 → conceptkernel-1.2.0}/cklib/schema.py +0 -0
  22. {conceptkernel-1.1.0 → conceptkernel-1.2.0}/cklib/serve.py +0 -0
  23. {conceptkernel-1.1.0 → conceptkernel-1.2.0}/cklib/urn.py +0 -0
  24. {conceptkernel-1.1.0 → conceptkernel-1.2.0}/conceptkernel.egg-info/dependency_links.txt +0 -0
  25. {conceptkernel-1.1.0 → conceptkernel-1.2.0}/conceptkernel.egg-info/requires.txt +0 -0
  26. {conceptkernel-1.1.0 → conceptkernel-1.2.0}/conceptkernel.egg-info/top_level.txt +0 -0
  27. {conceptkernel-1.1.0 → conceptkernel-1.2.0}/setup.cfg +0 -0
  28. {conceptkernel-1.1.0 → conceptkernel-1.2.0}/tests/test_execution.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: conceptkernel
3
- Version: 1.1.0
3
+ Version: 1.2.0
4
4
  Summary: CKP v3.5 Python runtime — kernel processor, provenance, execution proofs
5
5
  Author-email: Peter Styk <peter@conceptkernel.org>
6
6
  License: MIT
@@ -108,27 +108,51 @@ def _get_actions_from_spec(data):
108
108
 
109
109
 
110
110
  def _find_ck_dir_by_name(target_name, concepts_dir):
111
- """Find a CK directory by its metadata.name."""
111
+ """Find a CK directory by its metadata.name.
112
+
113
+ Searches two levels: direct child (flat layout) and one level deeper
114
+ (GUID subdirectory layout).
115
+ """
112
116
  if not os.path.isdir(concepts_dir):
113
117
  return None
114
118
  for entry in os.listdir(concepts_dir):
115
119
  candidate = os.path.join(concepts_dir, entry)
116
120
  if not os.path.isdir(candidate):
117
121
  continue
122
+ # Level 1: conceptkernel.yaml at kernel root (flat layout)
118
123
  yaml_path = os.path.join(candidate, "conceptkernel.yaml")
119
- if not os.path.isfile(yaml_path):
120
- continue
121
- try:
122
- with open(yaml_path) as f:
123
- d = yaml.safe_load(f)
124
- if isinstance(d, dict) and d.get("metadata", {}).get("name") == target_name:
125
- return candidate
126
- except Exception:
124
+ if os.path.isfile(yaml_path):
125
+ try:
126
+ with open(yaml_path) as f:
127
+ d = yaml.safe_load(f)
128
+ if isinstance(d, dict) and d.get("metadata", {}).get("name") == target_name:
129
+ return candidate
130
+ # Also check kernel_class field
131
+ if isinstance(d, dict) and d.get("kernel_class") == target_name:
132
+ return candidate
133
+ except Exception:
134
+ pass
127
135
  continue
136
+ # Level 2: conceptkernel.yaml inside a subdirectory (GUID layout)
137
+ for sub in os.listdir(candidate):
138
+ sub_path = os.path.join(candidate, sub)
139
+ if not os.path.isdir(sub_path):
140
+ continue
141
+ yaml_path = os.path.join(sub_path, "conceptkernel.yaml")
142
+ if os.path.isfile(yaml_path):
143
+ try:
144
+ with open(yaml_path) as f:
145
+ d = yaml.safe_load(f)
146
+ if isinstance(d, dict) and d.get("metadata", {}).get("name") == target_name:
147
+ return sub_path
148
+ if isinstance(d, dict) and d.get("kernel_class") == target_name:
149
+ return sub_path
150
+ except Exception:
151
+ pass
128
152
  return None
129
153
 
130
154
 
131
- def resolve_composed_actions(ck_dir):
155
+ def resolve_composed_actions(ck_dir, concepts_dir=None):
132
156
  """Walk COMPOSES/EXTENDS edges, collect target kernel actions.
133
157
 
134
158
  Returns dict: {action_name: target_kernel_name}
@@ -139,8 +163,17 @@ def resolve_composed_actions(ck_dir):
139
163
  if err:
140
164
  return {}
141
165
 
142
- # Derive concepts_dir
143
- concepts_dir = os.path.dirname(ck_dir)
166
+ # Derive concepts_dir — walk up until we find a dir containing OTHER kernels
167
+ if concepts_dir is None:
168
+ concepts_dir = os.path.dirname(ck_dir)
169
+ # Check if parent has OTHER kernel directories (not just our own GUID subdir)
170
+ ck_basename = os.path.basename(ck_dir)
171
+ siblings = [d for d in os.listdir(concepts_dir)
172
+ if d != ck_basename
173
+ and os.path.isdir(os.path.join(concepts_dir, d))
174
+ and os.path.isfile(os.path.join(concepts_dir, d, "conceptkernel.yaml"))]
175
+ if not siblings:
176
+ concepts_dir = os.path.dirname(concepts_dir)
144
177
 
145
178
  edges_section = data.get("spec", {}).get("edges", data.get("edges", {}))
146
179
  outbound = edges_section.get("outbound", [])
@@ -27,9 +27,36 @@ CONCEPTS_DIR = None
27
27
  def _init_paths():
28
28
  global PROJECT_ROOT, CONCEPTS_DIR
29
29
  if PROJECT_ROOT is None:
30
- PROJECT_ROOT = os.path.abspath(
31
- os.path.join(os.path.dirname(__file__), "..", "..", "..", ".."))
32
- CONCEPTS_DIR = os.path.join(PROJECT_ROOT, "concepts")
30
+ # Prefer env vars (set by CK.Operator boot or delvinator.sh)
31
+ if os.environ.get("CK_CONCEPTS_DIR"):
32
+ CONCEPTS_DIR = os.environ["CK_CONCEPTS_DIR"]
33
+ PROJECT_ROOT = os.path.dirname(CONCEPTS_DIR)
34
+ elif os.environ.get("PROJECT_ROOT"):
35
+ PROJECT_ROOT = os.environ["PROJECT_ROOT"]
36
+ CONCEPTS_DIR = os.path.join(PROJECT_ROOT, "concepts")
37
+ else:
38
+ # Fallback: walk up from cklib to find concepts/ dir containing kernel dirs
39
+ candidate = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
40
+ for _ in range(5):
41
+ concepts_candidate = os.path.join(candidate, "concepts")
42
+ if os.path.isdir(concepts_candidate):
43
+ # Verify it has actual kernel directories (not just a stray concepts/ folder)
44
+ has_kernels = any(
45
+ os.path.isfile(os.path.join(concepts_candidate, d, "conceptkernel.yaml"))
46
+ or any(os.path.isfile(os.path.join(concepts_candidate, d, sub, "conceptkernel.yaml"))
47
+ for sub in os.listdir(os.path.join(concepts_candidate, d))
48
+ if os.path.isdir(os.path.join(concepts_candidate, d, sub)))
49
+ for d in os.listdir(concepts_candidate)
50
+ if os.path.isdir(os.path.join(concepts_candidate, d))
51
+ )
52
+ if has_kernels:
53
+ PROJECT_ROOT = candidate
54
+ CONCEPTS_DIR = concepts_candidate
55
+ break
56
+ candidate = os.path.dirname(candidate)
57
+ else:
58
+ PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..", ".."))
59
+ CONCEPTS_DIR = os.path.join(PROJECT_ROOT, "concepts")
33
60
 
34
61
 
35
62
  def resolve_kernel(name):
@@ -0,0 +1,183 @@
1
+ """
2
+ nats_kernel.py — shared NATS kernel loop for Concept Kernels.
3
+
4
+ Not a standalone CK — a library that any CK's processor.py imports to become
5
+ a NATS listener following the CK processing cycle:
6
+
7
+ Receive → Validate → Process (primary tool) → Create Instance → Publish Result → Notify
8
+
9
+ Usage:
10
+ from nats_kernel import NatsKernelLoop
11
+
12
+ def handle_message(msg):
13
+ return {"status": "ok", "echo": msg}
14
+
15
+ loop = NatsKernelLoop(CK_DIR, handle_message)
16
+ asyncio.run(loop.run())
17
+ """
18
+ import asyncio
19
+ import json
20
+ import os
21
+ import time
22
+ import uuid
23
+
24
+ import yaml
25
+ import nats
26
+
27
+
28
+ class NatsKernelLoop:
29
+ """NATS listener that implements the CK processing cycle."""
30
+
31
+ # nats:// — native TCP (server processors in cluster)
32
+ # wss:// — WebSocket (Python CLI from local machine, needs aiohttp)
33
+ # Set NATS_URL env var to override
34
+ DEFAULT_ENDPOINT = "nats://localhost:4222"
35
+
36
+ def __init__(self, ck_dir, handler_fn):
37
+ self.ck_dir = os.path.abspath(ck_dir)
38
+ self.handler_fn = handler_fn
39
+ self.instance_dir = os.path.join(self.ck_dir, "storage", "instances")
40
+
41
+ # NATS endpoint: env var > .ckproject > default
42
+ self.endpoint = os.environ.get("NATS_URL", self.DEFAULT_ENDPOINT)
43
+
44
+ # Load identity from conceptkernel.yaml
45
+ ck_yaml_path = os.path.join(self.ck_dir, "conceptkernel.yaml")
46
+ with open(ck_yaml_path) as f:
47
+ self.ck = yaml.safe_load(f)
48
+
49
+ meta = self.ck["metadata"]
50
+ spec = self.ck["spec"]
51
+ nats_cfg = spec.get("nats", {})
52
+
53
+ self.kernel_name = meta["name"]
54
+ self.kernel_urn = meta["urn"]
55
+ self.input_topic = nats_cfg.get("input_topic", f"input.{self.kernel_name}")
56
+ self.result_topic = nats_cfg.get("result_topic", f"result.{self.kernel_name}")
57
+ self.event_topic = nats_cfg.get("event_topic", f"event.{self.kernel_name}")
58
+
59
+ self.nc = None
60
+
61
+ async def run(self):
62
+ """Connect to NATS and listen on input topic."""
63
+ print(f"[nats] connecting to {self.endpoint}...")
64
+ self.nc = await nats.connect(self.endpoint)
65
+ print(f"[nats] connected as {self.kernel_urn}")
66
+ print(f"[sub] {self.input_topic}")
67
+ print(f"[ready] Listening on {self.input_topic}...")
68
+ print()
69
+
70
+ sub = await self.nc.subscribe(self.input_topic)
71
+
72
+ try:
73
+ async for msg in sub.messages:
74
+ await self._handle(msg)
75
+ except asyncio.CancelledError:
76
+ pass
77
+ finally:
78
+ await self.nc.drain()
79
+
80
+ async def _handle(self, msg):
81
+ """Process a single incoming message through the CK cycle."""
82
+ ts = int(time.time())
83
+
84
+ # Parse headers from NATS-level headers
85
+ headers = {}
86
+ if msg.headers:
87
+ for key in ("Trace-Id", "Nats-Msg-Id", "X-Kernel-ID", "X-User-ID", "X-Anonymous"):
88
+ val = msg.headers.get(key)
89
+ if val:
90
+ headers[key] = val
91
+
92
+ trace_id = headers.get("Trace-Id", f"tx-{uuid.uuid4().hex[:6]}")
93
+ sender_kernel = headers.get("X-Kernel-ID", "unknown")
94
+ user_id = headers.get("X-User-ID", "anonymous")
95
+
96
+ # Parse body (pure data — no control attributes)
97
+ try:
98
+ body = json.loads(msg.data.decode()) if msg.data else {}
99
+ except (json.JSONDecodeError, UnicodeDecodeError):
100
+ body = {"raw": msg.data.decode("utf-8", errors="replace")}
101
+
102
+ print(f"[rx] {trace_id} {msg.subject} {json.dumps(body, separators=(',', ':'))}")
103
+
104
+ # Process via primary tool (handler_fn)
105
+ # Pass nc + trace_id for streaming. Supports both sync and async handlers.
106
+ try:
107
+ import inspect
108
+ try:
109
+ raw = self.handler_fn(body, nc=self.nc, trace_id=trace_id)
110
+ except TypeError:
111
+ raw = self.handler_fn(body)
112
+
113
+ # If handler returned a coroutine, await it
114
+ if inspect.isawaitable(raw):
115
+ result = await raw
116
+ else:
117
+ result = raw
118
+ status = "ok"
119
+ except Exception as e:
120
+ result = {"error": str(e)}
121
+ status = "error"
122
+
123
+ # Create instance record
124
+ instance_id = f"i-{trace_id}-{ts}"
125
+ instance_path = os.path.join(self.instance_dir, instance_id)
126
+ os.makedirs(instance_path, exist_ok=True)
127
+
128
+ record = {
129
+ "instance_id": instance_id,
130
+ "kernel_urn": self.kernel_urn,
131
+ "trace_id": trace_id,
132
+ "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(ts)),
133
+ "input": {
134
+ "subject": msg.subject,
135
+ "headers": headers,
136
+ "body": body,
137
+ },
138
+ "output": result,
139
+ "status": status,
140
+ "sender": sender_kernel,
141
+ "user_id": user_id,
142
+ }
143
+
144
+ record_path = os.path.join(instance_path, "message.json")
145
+ with open(record_path, "w") as f:
146
+ json.dump(record, f, indent=2)
147
+
148
+ # Publish result
149
+ result_envelope = {
150
+ "trace_id": trace_id,
151
+ "kernel_urn": self.kernel_urn,
152
+ "timestamp": record["timestamp"],
153
+ "status": status,
154
+ "data": result,
155
+ }
156
+
157
+ result_headers = {
158
+ "Trace-Id": trace_id,
159
+ "X-Kernel-ID": self.kernel_urn,
160
+ "Nats-Msg-Id": str(uuid.uuid4()),
161
+ }
162
+
163
+ await self.nc.publish(
164
+ self.result_topic,
165
+ json.dumps(result_envelope).encode(),
166
+ headers=result_headers,
167
+ )
168
+ print(f"[tx] {trace_id} {self.result_topic}")
169
+
170
+ # Publish event notification
171
+ event = {
172
+ "type": "instance_created",
173
+ "instance_id": instance_id,
174
+ "trace_id": trace_id,
175
+ "kernel_urn": self.kernel_urn,
176
+ "timestamp": record["timestamp"],
177
+ }
178
+
179
+ await self.nc.publish(
180
+ self.event_topic,
181
+ json.dumps(event).encode(),
182
+ headers={"Trace-Id": trace_id, "X-Kernel-ID": self.kernel_urn},
183
+ )
@@ -168,8 +168,7 @@ class KernelProcessor:
168
168
 
169
169
  def listen(self):
170
170
  """Start NATS listener using NatsKernelLoop."""
171
- sys.path.insert(0, os.path.join(self.concepts_dir, "CK.Lib", "tool"))
172
- from nats_kernel import NatsKernelLoop
171
+ from cklib.nats import NatsKernelLoop
173
172
  loop = NatsKernelLoop(self.ck_dir, self.handle_message)
174
173
  asyncio.run(loop.run())
175
174
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: conceptkernel
3
- Version: 1.1.0
3
+ Version: 1.2.0
4
4
  Summary: CKP v3.5 Python runtime — kernel processor, provenance, execution proofs
5
5
  Author-email: Peter Styk <peter@conceptkernel.org>
6
6
  License: MIT
@@ -12,6 +12,7 @@ cklib/events.py
12
12
  cklib/execution.py
13
13
  cklib/instance.py
14
14
  cklib/ledger.py
15
+ cklib/nats.py
15
16
  cklib/processor.py
16
17
  cklib/prov.py
17
18
  cklib/schema.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "conceptkernel"
7
- version = "1.1.0"
7
+ version = "1.2.0"
8
8
  description = "CKP v3.5 Python runtime — kernel processor, provenance, execution proofs"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
File without changes
File without changes
File without changes