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.
@@ -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."""