hb-cortex-memory 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.
- cortex_memory/__init__.py +126 -0
- cortex_memory/_textutil.py +83 -0
- cortex_memory/assembly.py +335 -0
- cortex_memory/db.py +25 -0
- cortex_memory/domains.py +158 -0
- cortex_memory/dreaming.py +568 -0
- cortex_memory/dreaming_prompts.py +55 -0
- cortex_memory/dtos.py +260 -0
- cortex_memory/embedding.py +58 -0
- cortex_memory/enums.py +78 -0
- cortex_memory/episodic_tree.py +466 -0
- cortex_memory/experience_tree.py +230 -0
- cortex_memory/graph.py +409 -0
- cortex_memory/ingestion.py +224 -0
- cortex_memory/intelligence_tree.py +275 -0
- cortex_memory/knowledge_tree.py +483 -0
- cortex_memory/models.py +240 -0
- cortex_memory/prompts.py +21 -0
- cortex_memory/providers.py +156 -0
- cortex_memory/providers_reference.py +121 -0
- cortex_memory/py.typed +0 -0
- cortex_memory/schema.py +43 -0
- cortex_memory/scope_policy.py +53 -0
- cortex_memory/service.py +1196 -0
- hb_cortex_memory-0.1.0.dist-info/METADATA +168 -0
- hb_cortex_memory-0.1.0.dist-info/RECORD +29 -0
- hb_cortex_memory-0.1.0.dist-info/WHEEL +5 -0
- hb_cortex_memory-0.1.0.dist-info/licenses/LICENSE +201 -0
- hb_cortex_memory-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
"""
|
|
2
|
+
dreaming_engine.py — The Learning Engine (Phase D)
|
|
3
|
+
|
|
4
|
+
Background learning pipeline that extracts patterns from episodic history
|
|
5
|
+
and distills them into actionable intelligence. Runs as a background
|
|
6
|
+
worker task, triggered after execution completion.
|
|
7
|
+
|
|
8
|
+
Three-phase pipeline:
|
|
9
|
+
Phase 1: Observation Extraction (Episodic → Experience.Observations)
|
|
10
|
+
Phase 2: Pattern Recognition (Observations → Experience.Patterns)
|
|
11
|
+
Phase 3: Intelligence Distillation (Patterns → Intelligence.Rules)
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
engine = DreamingEngine(db, company_id)
|
|
15
|
+
result = await engine.dream(entity_id, force=True)
|
|
16
|
+
# result = {"observations_created": 3, "patterns_created": 1, "rules_created": 1}
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import logging
|
|
22
|
+
from datetime import datetime, timedelta
|
|
23
|
+
from typing import Any, Dict, List, Optional
|
|
24
|
+
from uuid import UUID, uuid4
|
|
25
|
+
|
|
26
|
+
from sqlalchemy import select, text
|
|
27
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
28
|
+
|
|
29
|
+
from cortex_memory.models import (
|
|
30
|
+
CortexTree, CortexNode, CortexEdge,
|
|
31
|
+
CortexTreeStatus, CortexNodeType, CortexNodeStatus,
|
|
32
|
+
MemoryDomain, ScopeLevel,
|
|
33
|
+
)
|
|
34
|
+
from cortex_memory.dreaming_prompts import (
|
|
35
|
+
OBSERVATION_EXTRACTION_PROMPT,
|
|
36
|
+
PATTERN_RECOGNITION_PROMPT,
|
|
37
|
+
INTELLIGENCE_DISTILLATION_PROMPT,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
logger = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class _Resp:
|
|
44
|
+
"""Minimal LLMResponse-shaped object the dreaming phases read (.output)."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, output: str = "", model_name: str = "",
|
|
47
|
+
prompt_tokens: int = 0, completion_tokens: int = 0) -> None:
|
|
48
|
+
self.output = output
|
|
49
|
+
self.model_name = model_name
|
|
50
|
+
self.prompt_tokens = prompt_tokens
|
|
51
|
+
self.completion_tokens = completion_tokens
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class _ProviderLLMAdapter:
|
|
55
|
+
"""Adapts a cortex_memory.LLMProvider to the ``call_llm(...)`` interface."""
|
|
56
|
+
|
|
57
|
+
def __init__(self, provider: Any) -> None:
|
|
58
|
+
self._p = provider
|
|
59
|
+
|
|
60
|
+
async def call_llm(self, *, task_type: str = "text_generation",
|
|
61
|
+
system_prompt: str = "", user_prompt: str = "",
|
|
62
|
+
temperature: float = 0.7, max_tokens: Any = None,
|
|
63
|
+
**_kw: Any) -> _Resp:
|
|
64
|
+
if self._p is None:
|
|
65
|
+
return _Resp("")
|
|
66
|
+
r = await self._p.complete(
|
|
67
|
+
system=system_prompt, user=user_prompt, task_type=task_type,
|
|
68
|
+
temperature=temperature, max_tokens=max_tokens,
|
|
69
|
+
)
|
|
70
|
+
return _Resp(r.text or "", r.model, r.input_tokens, r.output_tokens)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class DreamingEngine:
|
|
74
|
+
"""
|
|
75
|
+
Background learning engine — the "dreaming" process.
|
|
76
|
+
|
|
77
|
+
Extracts observations from episodes, identifies patterns across
|
|
78
|
+
observations, and distills validated patterns into rules.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
# Configuration (overridable via constants.py defaults)
|
|
82
|
+
MIN_EPISODES_FOR_DREAMING = 5
|
|
83
|
+
MIN_OBSERVATIONS_FOR_PATTERNS = 3
|
|
84
|
+
MIN_PATTERNS_FOR_DISTILLATION = 2
|
|
85
|
+
BATCH_SIZE = 20
|
|
86
|
+
CONSOLIDATION_INTERVAL_HOURS = 24
|
|
87
|
+
OBSERVATION_CONFIDENCE_THRESHOLD = 0.5
|
|
88
|
+
PATTERN_STRENGTH_THRESHOLD = 0.7
|
|
89
|
+
|
|
90
|
+
def __init__(
|
|
91
|
+
self,
|
|
92
|
+
db: AsyncSession,
|
|
93
|
+
company_id: UUID,
|
|
94
|
+
*,
|
|
95
|
+
llm: Any = None,
|
|
96
|
+
embedding: Any = None,
|
|
97
|
+
) -> None:
|
|
98
|
+
self.db = db
|
|
99
|
+
self.company_id = company_id
|
|
100
|
+
# Injected cortex_memory providers. Both optional; dreaming degrades to
|
|
101
|
+
# a no-op when absent (standalone use).
|
|
102
|
+
self._llm = llm
|
|
103
|
+
self._embedding = embedding
|
|
104
|
+
|
|
105
|
+
def _get_llm(self) -> Any:
|
|
106
|
+
"""Adapt the injected LLMProvider to the call_llm(...) interface the
|
|
107
|
+
dreaming phases use."""
|
|
108
|
+
return _ProviderLLMAdapter(self._llm)
|
|
109
|
+
|
|
110
|
+
async def _log_dreaming_usage(self, response: Any) -> None:
|
|
111
|
+
# The injected provider's host adapter self-meters; nothing to do.
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
# ===================================================================
|
|
115
|
+
# Main Entry Point
|
|
116
|
+
# ===================================================================
|
|
117
|
+
|
|
118
|
+
async def dream(
|
|
119
|
+
self,
|
|
120
|
+
entity_id: UUID,
|
|
121
|
+
force: bool = False,
|
|
122
|
+
) -> Dict[str, int]:
|
|
123
|
+
"""
|
|
124
|
+
Run the full dreaming pipeline for an entity.
|
|
125
|
+
|
|
126
|
+
Returns counts of created nodes per phase.
|
|
127
|
+
"""
|
|
128
|
+
if not force:
|
|
129
|
+
should_run = await self._should_run(entity_id)
|
|
130
|
+
if not should_run:
|
|
131
|
+
return {"observations_created": 0, "patterns_created": 0, "rules_created": 0}
|
|
132
|
+
|
|
133
|
+
logger.info(f"Dreaming engine starting for entity {entity_id}")
|
|
134
|
+
|
|
135
|
+
# Extract Observations
|
|
136
|
+
obs_ids = await self._extract_observations(entity_id)
|
|
137
|
+
|
|
138
|
+
# Pattern Recognition
|
|
139
|
+
pat_ids = await self._recognize_patterns(entity_id)
|
|
140
|
+
|
|
141
|
+
# Intelligence Distillation
|
|
142
|
+
rule_ids = await self._distill_intelligence(entity_id)
|
|
143
|
+
|
|
144
|
+
# Update consolidation timestamps
|
|
145
|
+
await self._update_consolidation_timestamp(entity_id)
|
|
146
|
+
|
|
147
|
+
result = {
|
|
148
|
+
"observations_created": len(obs_ids),
|
|
149
|
+
"patterns_created": len(pat_ids),
|
|
150
|
+
"rules_created": len(rule_ids),
|
|
151
|
+
}
|
|
152
|
+
logger.info(f"Dreaming engine completed for entity {entity_id}: {result}")
|
|
153
|
+
return result
|
|
154
|
+
|
|
155
|
+
# ===================================================================
|
|
156
|
+
# Observation Extraction
|
|
157
|
+
# ===================================================================
|
|
158
|
+
|
|
159
|
+
async def _extract_observations(self, entity_id: UUID) -> List[UUID]:
|
|
160
|
+
"""
|
|
161
|
+
Analyze recent episode nodes and extract observations via LLM.
|
|
162
|
+
"""
|
|
163
|
+
from cortex_memory.episodic_tree import EpisodicTreeService
|
|
164
|
+
from cortex_memory.experience_tree import ExperienceTreeService
|
|
165
|
+
|
|
166
|
+
episodic_svc = EpisodicTreeService(self.db, self.company_id, embedding=self._embedding)
|
|
167
|
+
experience_svc = ExperienceTreeService(self.db, self.company_id)
|
|
168
|
+
|
|
169
|
+
experience_tree = await experience_svc.get_or_create_experience_tree(entity_id)
|
|
170
|
+
last_consolidated = experience_tree.last_consolidated_at or datetime.min
|
|
171
|
+
|
|
172
|
+
# Get unprocessed episodes
|
|
173
|
+
episodes = await episodic_svc.query_by_time(
|
|
174
|
+
entity_id=entity_id,
|
|
175
|
+
start_date=last_consolidated,
|
|
176
|
+
end_date=datetime.utcnow(),
|
|
177
|
+
limit=self.BATCH_SIZE,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
if len(episodes) < self.MIN_EPISODES_FOR_DREAMING:
|
|
181
|
+
logger.debug(
|
|
182
|
+
f"Skipping observation extraction: {len(episodes)} episodes "
|
|
183
|
+
f"(min {self.MIN_EPISODES_FOR_DREAMING})"
|
|
184
|
+
)
|
|
185
|
+
return []
|
|
186
|
+
|
|
187
|
+
# Build episode summaries for LLM
|
|
188
|
+
episode_summaries = []
|
|
189
|
+
for ep in episodes:
|
|
190
|
+
meta = ep.get("metadata", {}) or {}
|
|
191
|
+
episode_summaries.append({
|
|
192
|
+
"id": ep.get("node_id", ""),
|
|
193
|
+
"task": ep.get("summary", ""),
|
|
194
|
+
"status": meta.get("status", "unknown"),
|
|
195
|
+
"tools_used": meta.get("tools_used", []),
|
|
196
|
+
"cost_usd": meta.get("cost_usd", 0),
|
|
197
|
+
"execution_time_ms": meta.get("execution_time_ms", 0),
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
llm = self._get_llm()
|
|
202
|
+
response = await llm.call_llm(
|
|
203
|
+
task_type="text_generation",
|
|
204
|
+
system_prompt=OBSERVATION_EXTRACTION_PROMPT,
|
|
205
|
+
user_prompt=json.dumps(episode_summaries),
|
|
206
|
+
temperature=0.2,
|
|
207
|
+
max_tokens=2000,
|
|
208
|
+
)
|
|
209
|
+
await self._log_dreaming_usage(response)
|
|
210
|
+
except Exception as e:
|
|
211
|
+
logger.warning(f"LLM call failed in observation extraction: {e}")
|
|
212
|
+
return []
|
|
213
|
+
|
|
214
|
+
from cortex_memory._textutil import parse_json_array
|
|
215
|
+
observations = parse_json_array(response.output)
|
|
216
|
+
if not observations:
|
|
217
|
+
return []
|
|
218
|
+
|
|
219
|
+
# Write observation nodes
|
|
220
|
+
obs_root = await experience_svc.get_observations_root(entity_id)
|
|
221
|
+
created_ids = []
|
|
222
|
+
|
|
223
|
+
from cortex_memory.embedding import embed_node
|
|
224
|
+
|
|
225
|
+
for obs in observations:
|
|
226
|
+
if obs.get("confidence", 0) < self.OBSERVATION_CONFIDENCE_THRESHOLD:
|
|
227
|
+
continue
|
|
228
|
+
|
|
229
|
+
node = CortexNode(
|
|
230
|
+
id=uuid4(),
|
|
231
|
+
tree_id=experience_tree.id,
|
|
232
|
+
parent_id=obs_root,
|
|
233
|
+
node_type=CortexNodeType.OBSERVATION,
|
|
234
|
+
title=f"🔍 {obs.get('title', 'Observation')[:100]}",
|
|
235
|
+
summary=obs.get("description", "")[:500],
|
|
236
|
+
content=json.dumps(obs),
|
|
237
|
+
status=CortexNodeStatus.COMPLETE,
|
|
238
|
+
depth=2,
|
|
239
|
+
sibling_order=await self._next_sibling_order(obs_root),
|
|
240
|
+
metadata_extra={
|
|
241
|
+
"source_episodes": obs.get("source_episodes", []),
|
|
242
|
+
"confidence": obs.get("confidence", 0.5),
|
|
243
|
+
"first_observed": datetime.utcnow().isoformat(),
|
|
244
|
+
"observation_count": 1,
|
|
245
|
+
},
|
|
246
|
+
importance_score=obs.get("confidence", 0.5),
|
|
247
|
+
)
|
|
248
|
+
self.db.add(node)
|
|
249
|
+
await self.db.flush()
|
|
250
|
+
|
|
251
|
+
# Embed the observation
|
|
252
|
+
await embed_node(self._embedding, node)
|
|
253
|
+
created_ids.append(node.id)
|
|
254
|
+
|
|
255
|
+
experience_tree.total_nodes = (experience_tree.total_nodes or 0) + len(created_ids)
|
|
256
|
+
await self.db.flush()
|
|
257
|
+
|
|
258
|
+
logger.info(f"Phase 1: {len(created_ids)} observations extracted for entity {entity_id}")
|
|
259
|
+
return created_ids
|
|
260
|
+
|
|
261
|
+
# ===================================================================
|
|
262
|
+
# Pattern Recognition
|
|
263
|
+
# ===================================================================
|
|
264
|
+
|
|
265
|
+
async def _recognize_patterns(self, entity_id: UUID) -> List[UUID]:
|
|
266
|
+
"""
|
|
267
|
+
Cluster observations by embedding similarity and synthesize patterns.
|
|
268
|
+
"""
|
|
269
|
+
from cortex_memory.experience_tree import ExperienceTreeService
|
|
270
|
+
experience_svc = ExperienceTreeService(self.db, self.company_id)
|
|
271
|
+
|
|
272
|
+
observations = await experience_svc.get_observations(entity_id)
|
|
273
|
+
if len(observations) < self.MIN_OBSERVATIONS_FOR_PATTERNS:
|
|
274
|
+
logger.debug(f"Skipping pattern recognition: {len(observations)} observations")
|
|
275
|
+
return []
|
|
276
|
+
|
|
277
|
+
# Cluster by embedding similarity
|
|
278
|
+
clusters = self._cluster_observations(observations)
|
|
279
|
+
|
|
280
|
+
patterns_root = await experience_svc.get_patterns_root(entity_id)
|
|
281
|
+
experience_tree = await experience_svc.get_or_create_experience_tree(entity_id)
|
|
282
|
+
created_ids = []
|
|
283
|
+
|
|
284
|
+
from cortex_memory.embedding import embed_node
|
|
285
|
+
|
|
286
|
+
for cluster in clusters:
|
|
287
|
+
if len(cluster) < 2:
|
|
288
|
+
continue
|
|
289
|
+
|
|
290
|
+
# LLM synthesis
|
|
291
|
+
cluster_texts = [obs.summary for obs in cluster]
|
|
292
|
+
try:
|
|
293
|
+
llm = self._get_llm()
|
|
294
|
+
response = await llm.call_llm(
|
|
295
|
+
task_type="text_generation",
|
|
296
|
+
system_prompt=PATTERN_RECOGNITION_PROMPT,
|
|
297
|
+
user_prompt=json.dumps(cluster_texts),
|
|
298
|
+
temperature=0.2,
|
|
299
|
+
max_tokens=500,
|
|
300
|
+
)
|
|
301
|
+
await self._log_dreaming_usage(response)
|
|
302
|
+
except Exception as e:
|
|
303
|
+
logger.warning(f"LLM call failed in pattern recognition: {e}")
|
|
304
|
+
continue
|
|
305
|
+
|
|
306
|
+
from cortex_memory._textutil import parse_json_object
|
|
307
|
+
pattern = parse_json_object(response.output)
|
|
308
|
+
if not pattern:
|
|
309
|
+
continue
|
|
310
|
+
|
|
311
|
+
node = CortexNode(
|
|
312
|
+
id=uuid4(),
|
|
313
|
+
tree_id=experience_tree.id,
|
|
314
|
+
parent_id=patterns_root,
|
|
315
|
+
node_type=CortexNodeType.PATTERN,
|
|
316
|
+
title=f"🔄 {pattern.get('title', 'Pattern')[:100]}",
|
|
317
|
+
summary=pattern.get("description", "")[:500],
|
|
318
|
+
content=json.dumps(pattern),
|
|
319
|
+
status=CortexNodeStatus.COMPLETE,
|
|
320
|
+
depth=2,
|
|
321
|
+
sibling_order=await self._next_sibling_order(patterns_root),
|
|
322
|
+
metadata_extra={
|
|
323
|
+
"source_observations": [str(obs.id) for obs in cluster],
|
|
324
|
+
"pattern_strength": pattern.get("strength", 0.5),
|
|
325
|
+
"recurrence_count": len(cluster),
|
|
326
|
+
"success_correlation": pattern.get("success_correlation", 0.5),
|
|
327
|
+
},
|
|
328
|
+
)
|
|
329
|
+
self.db.add(node)
|
|
330
|
+
await self.db.flush()
|
|
331
|
+
|
|
332
|
+
# Embed the pattern
|
|
333
|
+
await embed_node(self._embedding, node)
|
|
334
|
+
|
|
335
|
+
# Create cortex_edges linking pattern → source observations
|
|
336
|
+
for obs in cluster:
|
|
337
|
+
edge = CortexEdge(
|
|
338
|
+
source_node_id=node.id,
|
|
339
|
+
target_node_id=obs.id,
|
|
340
|
+
edge_type="derived_from",
|
|
341
|
+
weight=1.0 / len(cluster),
|
|
342
|
+
created_by="dreaming_engine",
|
|
343
|
+
)
|
|
344
|
+
self.db.add(edge)
|
|
345
|
+
|
|
346
|
+
created_ids.append(node.id)
|
|
347
|
+
|
|
348
|
+
experience_tree.total_nodes = (experience_tree.total_nodes or 0) + len(created_ids)
|
|
349
|
+
await self.db.flush()
|
|
350
|
+
|
|
351
|
+
logger.info(f"Phase 2: {len(created_ids)} patterns recognized for entity {entity_id}")
|
|
352
|
+
return created_ids
|
|
353
|
+
|
|
354
|
+
# ===================================================================
|
|
355
|
+
# Intelligence Distillation
|
|
356
|
+
# ===================================================================
|
|
357
|
+
|
|
358
|
+
async def _distill_intelligence(self, entity_id: UUID) -> List[UUID]:
|
|
359
|
+
"""
|
|
360
|
+
Distill validated patterns into actionable Intelligence rules.
|
|
361
|
+
"""
|
|
362
|
+
from cortex_memory.experience_tree import ExperienceTreeService
|
|
363
|
+
from cortex_memory.intelligence_tree import IntelligenceTreeService
|
|
364
|
+
|
|
365
|
+
experience_svc = ExperienceTreeService(self.db, self.company_id)
|
|
366
|
+
intelligence_svc = IntelligenceTreeService(self.db, self.company_id, embedding=self._embedding)
|
|
367
|
+
|
|
368
|
+
# Get strong patterns
|
|
369
|
+
patterns = await experience_svc.get_strong_patterns(
|
|
370
|
+
entity_id,
|
|
371
|
+
min_strength=self.PATTERN_STRENGTH_THRESHOLD,
|
|
372
|
+
min_recurrence=self.MIN_PATTERNS_FOR_DISTILLATION,
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
if not patterns:
|
|
376
|
+
logger.debug("Skipping distillation: no strong patterns")
|
|
377
|
+
return []
|
|
378
|
+
|
|
379
|
+
# Get existing rules to avoid duplicates
|
|
380
|
+
existing_rules = await intelligence_svc.get_all_rules(entity_id)
|
|
381
|
+
existing_summaries = [r.summary for r in existing_rules]
|
|
382
|
+
|
|
383
|
+
try:
|
|
384
|
+
llm = self._get_llm()
|
|
385
|
+
response = await llm.call_llm(
|
|
386
|
+
task_type="text_generation",
|
|
387
|
+
system_prompt=INTELLIGENCE_DISTILLATION_PROMPT,
|
|
388
|
+
user_prompt=json.dumps({
|
|
389
|
+
"patterns": [
|
|
390
|
+
{
|
|
391
|
+
"summary": p.summary,
|
|
392
|
+
"strength": (p.metadata_extra or {}).get("pattern_strength", 0.5),
|
|
393
|
+
}
|
|
394
|
+
for p in patterns
|
|
395
|
+
],
|
|
396
|
+
"existing_rules": existing_summaries,
|
|
397
|
+
}),
|
|
398
|
+
temperature=0.1,
|
|
399
|
+
max_tokens=2000,
|
|
400
|
+
)
|
|
401
|
+
await self._log_dreaming_usage(response)
|
|
402
|
+
except Exception as e:
|
|
403
|
+
logger.warning(f"LLM call failed in intelligence distillation: {e}")
|
|
404
|
+
return []
|
|
405
|
+
|
|
406
|
+
from cortex_memory._textutil import parse_json_array
|
|
407
|
+
rules = parse_json_array(response.output)
|
|
408
|
+
if not rules:
|
|
409
|
+
return []
|
|
410
|
+
|
|
411
|
+
intelligence_tree = await intelligence_svc.get_or_create_intelligence_tree(entity_id)
|
|
412
|
+
created_ids = []
|
|
413
|
+
|
|
414
|
+
from cortex_memory.embedding import embed_node
|
|
415
|
+
|
|
416
|
+
NODE_TYPE_MAP = {
|
|
417
|
+
"instruction": CortexNodeType.INSTRUCTION,
|
|
418
|
+
"strategy": CortexNodeType.STRATEGY,
|
|
419
|
+
"preference": CortexNodeType.PREFERENCE,
|
|
420
|
+
}
|
|
421
|
+
EMOJI_MAP = {
|
|
422
|
+
"instruction": "📏",
|
|
423
|
+
"strategy": "🎯",
|
|
424
|
+
"preference": "❤️",
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
for rule in rules:
|
|
428
|
+
rule_type = rule.get("type", "instruction")
|
|
429
|
+
node_type = NODE_TYPE_MAP.get(rule_type, CortexNodeType.INSTRUCTION)
|
|
430
|
+
emoji = EMOJI_MAP.get(rule_type, "📏")
|
|
431
|
+
|
|
432
|
+
parent_id = await intelligence_svc.get_section_root(entity_id, rule_type)
|
|
433
|
+
|
|
434
|
+
node = CortexNode(
|
|
435
|
+
id=uuid4(),
|
|
436
|
+
tree_id=intelligence_tree.id,
|
|
437
|
+
parent_id=parent_id,
|
|
438
|
+
node_type=node_type,
|
|
439
|
+
title=f"{emoji} {rule.get('title', 'Rule')[:100]}",
|
|
440
|
+
summary=rule.get("description", "")[:500],
|
|
441
|
+
content=json.dumps(rule),
|
|
442
|
+
status=CortexNodeStatus.COMPLETE,
|
|
443
|
+
depth=2,
|
|
444
|
+
sibling_order=await self._next_sibling_order(parent_id),
|
|
445
|
+
metadata_extra={
|
|
446
|
+
"rule_type": rule_type,
|
|
447
|
+
"source_patterns": rule.get("source_patterns", []),
|
|
448
|
+
"confidence": rule.get("confidence", 0.5),
|
|
449
|
+
"success_rate": rule.get("success_rate", 0.0),
|
|
450
|
+
"applicability_conditions": rule.get("applicability_conditions", []),
|
|
451
|
+
"generation": (intelligence_tree.consolidation_generation or 0) + 1,
|
|
452
|
+
},
|
|
453
|
+
)
|
|
454
|
+
self.db.add(node)
|
|
455
|
+
await self.db.flush()
|
|
456
|
+
|
|
457
|
+
# Embed for semantic retrieval
|
|
458
|
+
await embed_node(self._embedding, node)
|
|
459
|
+
created_ids.append(node.id)
|
|
460
|
+
|
|
461
|
+
# Update generation counter
|
|
462
|
+
intelligence_tree.consolidation_generation = (
|
|
463
|
+
(intelligence_tree.consolidation_generation or 0) + 1
|
|
464
|
+
)
|
|
465
|
+
intelligence_tree.last_consolidated_at = datetime.utcnow()
|
|
466
|
+
intelligence_tree.total_nodes = (intelligence_tree.total_nodes or 0) + len(created_ids)
|
|
467
|
+
await self.db.flush()
|
|
468
|
+
|
|
469
|
+
logger.info(f"Phase 3: {len(created_ids)} rules distilled for entity {entity_id}")
|
|
470
|
+
return created_ids
|
|
471
|
+
|
|
472
|
+
# ===================================================================
|
|
473
|
+
# Scheduling Helpers
|
|
474
|
+
# ===================================================================
|
|
475
|
+
|
|
476
|
+
async def _should_run(self, entity_id: UUID) -> bool:
|
|
477
|
+
"""Check if enough time has passed since the last consolidation."""
|
|
478
|
+
from cortex_memory.experience_tree import ExperienceTreeService
|
|
479
|
+
experience_svc = ExperienceTreeService(self.db, self.company_id)
|
|
480
|
+
|
|
481
|
+
try:
|
|
482
|
+
tree = await experience_svc.get_or_create_experience_tree(entity_id)
|
|
483
|
+
except Exception:
|
|
484
|
+
return True # No tree yet — first run
|
|
485
|
+
|
|
486
|
+
if not tree.last_consolidated_at:
|
|
487
|
+
return True
|
|
488
|
+
|
|
489
|
+
hours_since = (datetime.utcnow() - tree.last_consolidated_at).total_seconds() / 3600
|
|
490
|
+
return hours_since >= self.CONSOLIDATION_INTERVAL_HOURS
|
|
491
|
+
|
|
492
|
+
async def _update_consolidation_timestamp(self, entity_id: UUID) -> None:
|
|
493
|
+
"""Update the last consolidation timestamp on the Experience Tree."""
|
|
494
|
+
from cortex_memory.experience_tree import ExperienceTreeService
|
|
495
|
+
experience_svc = ExperienceTreeService(self.db, self.company_id)
|
|
496
|
+
tree = await experience_svc.get_or_create_experience_tree(entity_id)
|
|
497
|
+
tree.last_consolidated_at = datetime.utcnow()
|
|
498
|
+
tree.consolidation_generation = (tree.consolidation_generation or 0) + 1
|
|
499
|
+
await self.db.flush()
|
|
500
|
+
|
|
501
|
+
# ===================================================================
|
|
502
|
+
# Observation Clustering
|
|
503
|
+
# ===================================================================
|
|
504
|
+
|
|
505
|
+
def _cluster_observations(
|
|
506
|
+
self, observations: List[CortexNode],
|
|
507
|
+
) -> List[List[CortexNode]]:
|
|
508
|
+
"""
|
|
509
|
+
Simple greedy clustering by embedding cosine similarity.
|
|
510
|
+
Groups observations with embeddings > 0.75 similarity.
|
|
511
|
+
"""
|
|
512
|
+
if not observations:
|
|
513
|
+
return []
|
|
514
|
+
|
|
515
|
+
# Filter to observations with embeddings
|
|
516
|
+
embedded = [o for o in observations if o.embedding is not None]
|
|
517
|
+
if len(embedded) < 2:
|
|
518
|
+
return [embedded] if embedded else []
|
|
519
|
+
|
|
520
|
+
# Greedy clustering
|
|
521
|
+
used = set()
|
|
522
|
+
clusters: List[List[CortexNode]] = []
|
|
523
|
+
|
|
524
|
+
for i, obs_a in enumerate(embedded):
|
|
525
|
+
if i in used:
|
|
526
|
+
continue
|
|
527
|
+
cluster = [obs_a]
|
|
528
|
+
used.add(i)
|
|
529
|
+
|
|
530
|
+
for j, obs_b in enumerate(embedded):
|
|
531
|
+
if j in used or j <= i:
|
|
532
|
+
continue
|
|
533
|
+
sim = self._cosine_similarity(obs_a.embedding, obs_b.embedding)
|
|
534
|
+
if sim > 0.75:
|
|
535
|
+
cluster.append(obs_b)
|
|
536
|
+
used.add(j)
|
|
537
|
+
|
|
538
|
+
clusters.append(cluster)
|
|
539
|
+
|
|
540
|
+
return clusters
|
|
541
|
+
|
|
542
|
+
@staticmethod
|
|
543
|
+
def _cosine_similarity(a: Any, b: Any) -> float:
|
|
544
|
+
"""Compute cosine similarity between two vectors.
|
|
545
|
+
|
|
546
|
+
Accepts plain lists or numpy/pgvector arrays. We must avoid bare
|
|
547
|
+
truthiness checks (``if not a``) because pgvector loads embeddings
|
|
548
|
+
as numpy ndarrays, for which truthiness raises
|
|
549
|
+
``ValueError: The truth value of an array ... is ambiguous``.
|
|
550
|
+
"""
|
|
551
|
+
if a is None or b is None or len(a) == 0 or len(b) == 0 or len(a) != len(b):
|
|
552
|
+
return 0.0
|
|
553
|
+
dot = sum(x * y for x, y in zip(a, b))
|
|
554
|
+
norm_a = sum(x * x for x in a) ** 0.5
|
|
555
|
+
norm_b = sum(x * x for x in b) ** 0.5
|
|
556
|
+
if norm_a == 0 or norm_b == 0:
|
|
557
|
+
return 0.0
|
|
558
|
+
return float(dot / (norm_a * norm_b))
|
|
559
|
+
|
|
560
|
+
async def _next_sibling_order(self, parent_id: UUID) -> int:
|
|
561
|
+
"""Get next sibling order for children of a node."""
|
|
562
|
+
from sqlalchemy import func
|
|
563
|
+
result = await self.db.execute(
|
|
564
|
+
select(func.coalesce(func.max(CortexNode.sibling_order), -1))
|
|
565
|
+
.where(CortexNode.parent_id == parent_id)
|
|
566
|
+
)
|
|
567
|
+
return int(result.scalar() or -1) + 1
|
|
568
|
+
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""
|
|
2
|
+
dreaming_prompts.py — LLM Prompt Templates for the Dreaming Engine
|
|
3
|
+
|
|
4
|
+
Three prompts for the three-phase learning pipeline:
|
|
5
|
+
Phase 1: Observation extraction from episodes
|
|
6
|
+
Phase 2: Pattern recognition across observations
|
|
7
|
+
Phase 3: Intelligence distillation from patterns
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
OBSERVATION_EXTRACTION_PROMPT = """You are analyzing execution history for an AI agent.
|
|
11
|
+
Given a batch of recent execution episodes, extract concrete OBSERVATIONS about:
|
|
12
|
+
|
|
13
|
+
1. TOOL PATTERNS: Which tools are used, in what order, and their effectiveness
|
|
14
|
+
2. SUCCESS FACTORS: What conditions correlate with successful outcomes
|
|
15
|
+
3. FAILURE PATTERNS: What conditions or sequences lead to failures
|
|
16
|
+
4. COST PATTERNS: What drives cost up or down
|
|
17
|
+
5. TIME PATTERNS: What affects execution time
|
|
18
|
+
|
|
19
|
+
For each observation, provide:
|
|
20
|
+
- title: A concise name (max 100 chars)
|
|
21
|
+
- description: A detailed description (max 500 chars)
|
|
22
|
+
- confidence: 0.0 to 1.0 indicating how confident this observation is
|
|
23
|
+
- source_episodes: List of episode IDs that support this observation
|
|
24
|
+
|
|
25
|
+
Return as JSON array: [{"title": "...", "description": "...", "confidence": 0.8, "source_episodes": ["..."]}]
|
|
26
|
+
Only return the JSON array, no other text."""
|
|
27
|
+
|
|
28
|
+
PATTERN_RECOGNITION_PROMPT = """You are identifying patterns from multiple observations.
|
|
29
|
+
Given a cluster of related observations, synthesize them into a PATTERN:
|
|
30
|
+
|
|
31
|
+
A pattern is a recurring behavior or correlation that appears across multiple observations.
|
|
32
|
+
It should be generalizable and actionable.
|
|
33
|
+
|
|
34
|
+
Return as JSON: {
|
|
35
|
+
"title": "...",
|
|
36
|
+
"description": "...",
|
|
37
|
+
"strength": 0.0 to 1.0,
|
|
38
|
+
"success_correlation": 0.0 to 1.0,
|
|
39
|
+
"actionability": "The pattern suggests..."
|
|
40
|
+
}
|
|
41
|
+
Only return the JSON object, no other text."""
|
|
42
|
+
|
|
43
|
+
INTELLIGENCE_DISTILLATION_PROMPT = """You are distilling patterns into actionable intelligence rules.
|
|
44
|
+
Given strong, validated patterns, create RULES the agent should follow:
|
|
45
|
+
|
|
46
|
+
Types of rules:
|
|
47
|
+
- instruction: A specific, concrete action to take or avoid
|
|
48
|
+
- strategy: A high-level approach or workflow template
|
|
49
|
+
- preference: A learned user/context preference
|
|
50
|
+
|
|
51
|
+
IMPORTANT: Do NOT duplicate existing rules. Check the provided list of existing rules.
|
|
52
|
+
|
|
53
|
+
Return as JSON array: [{"type": "instruction|strategy|preference", "title": "...",
|
|
54
|
+
"description": "...", "confidence": 0.0-1.0, "applicability_conditions": ["..."]}]
|
|
55
|
+
Only return the JSON array, no other text."""
|