Rohan03 commited on
Commit
1fa0c29
·
verified ·
1 Parent(s): 9b02e81

V2 merge: purpose_agent/compiler.py

Browse files
Files changed (1) hide show
  1. purpose_agent/compiler.py +160 -0
purpose_agent/compiler.py ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ compiler.py — Prompt compiler with token budget enforcement and credit assignment.
3
+
4
+ The PromptCompiler selects which memories to include in the prompt based on:
5
+ 1. Relevance to the current task
6
+ 2. Trust score (immune-scanned, tested)
7
+ 3. Utility score (has this memory actually helped before?)
8
+ 4. Scope match (right agent, right tools, right task category)
9
+ 5. Diversity (avoid redundant memories)
10
+ 6. Token cost (fit within budget)
11
+
12
+ Returns included_memory_ids so the orchestrator can do credit assignment
13
+ after the step: update utility scores for memories that were in context.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import logging
18
+ from dataclasses import dataclass, field
19
+ from typing import Any
20
+
21
+ from purpose_agent.memory import MemoryCard, MemoryKind, MemoryStatus, MemoryStore
22
+ from purpose_agent.v2_types import MemoryScope
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ @dataclass
28
+ class CompiledPrompt:
29
+ """Result of prompt compilation."""
30
+ system_sections: list[str] = field(default_factory=list)
31
+ included_memory_ids: list[str] = field(default_factory=list)
32
+ total_tokens_estimated: int = 0
33
+ budget_remaining: int = 0
34
+ memories_considered: int = 0
35
+ memories_included: int = 0
36
+
37
+ @property
38
+ def system_prompt(self) -> str:
39
+ return "\n\n".join(self.system_sections)
40
+
41
+
42
+ class PromptCompiler:
43
+ """
44
+ Compiles a prompt by selecting the best memories under a token budget.
45
+
46
+ The key invariant: only promoted memories are included.
47
+ Candidate/quarantined/rejected memories are never exposed to the LLM.
48
+ """
49
+
50
+ def __init__(
51
+ self,
52
+ memory_store: MemoryStore,
53
+ token_budget: int = 4096,
54
+ chars_per_token: int = 4,
55
+ ):
56
+ self.store = memory_store
57
+ self.token_budget = token_budget
58
+ self.chars_per_token = chars_per_token
59
+
60
+ def compile(
61
+ self,
62
+ task: str,
63
+ base_prompt: str,
64
+ scope: MemoryScope | None = None,
65
+ max_memories: int = 15,
66
+ ) -> CompiledPrompt:
67
+ """
68
+ Compile a prompt: base_prompt + best memories under token budget.
69
+
70
+ Returns CompiledPrompt with included_memory_ids for credit assignment.
71
+ """
72
+ result = CompiledPrompt()
73
+ result.system_sections.append(base_prompt)
74
+
75
+ base_tokens = len(base_prompt) // self.chars_per_token
76
+ remaining = self.token_budget - base_tokens
77
+ result.budget_remaining = remaining
78
+
79
+ if remaining <= 100:
80
+ result.total_tokens_estimated = base_tokens
81
+ return result
82
+
83
+ # Retrieve candidate memories (only PROMOTED)
84
+ candidates = self.store.retrieve(
85
+ query_text=task,
86
+ scope=scope,
87
+ statuses=[MemoryStatus.PROMOTED],
88
+ top_k=max_memories * 2, # over-fetch for diversity filtering
89
+ )
90
+ result.memories_considered = len(candidates)
91
+
92
+ # Deduplicate by content similarity
93
+ selected = self._diverse_select(candidates, max_memories)
94
+
95
+ # Fill prompt under budget
96
+ memory_sections = []
97
+ for card in selected:
98
+ text = self._format_memory(card)
99
+ token_cost = len(text) // self.chars_per_token
100
+
101
+ if token_cost > remaining:
102
+ continue
103
+
104
+ memory_sections.append(text)
105
+ result.included_memory_ids.append(card.id)
106
+ remaining -= token_cost
107
+ card.times_retrieved += 1
108
+
109
+ if memory_sections:
110
+ result.system_sections.append(
111
+ "## Learned Knowledge\n" + "\n".join(memory_sections)
112
+ )
113
+
114
+ result.memories_included = len(result.included_memory_ids)
115
+ result.budget_remaining = remaining
116
+ result.total_tokens_estimated = (self.token_budget - remaining)
117
+
118
+ return result
119
+
120
+ def _format_memory(self, card: MemoryCard) -> str:
121
+ """Format a single memory card for prompt inclusion."""
122
+ if card.kind == MemoryKind.SKILL_CARD:
123
+ text = f"- Skill: When {card.pattern}, do: {card.strategy}"
124
+ if card.steps:
125
+ text += " Steps: " + "; ".join(card.steps[:3])
126
+ elif card.kind == MemoryKind.USER_PREFERENCE:
127
+ text = f"- User preference: {card.content or card.strategy}"
128
+ elif card.kind == MemoryKind.FAILURE_PATTERN:
129
+ text = f"- Avoid: {card.pattern} — {card.strategy}"
130
+ elif card.kind == MemoryKind.TOOL_POLICY:
131
+ text = f"- Tool tip ({', '.join(card.scope.tool_names)}): {card.strategy}"
132
+ elif card.kind == MemoryKind.PURPOSE_CONTRACT:
133
+ text = f"- Goal constraint: {card.content or card.strategy}"
134
+ elif card.kind == MemoryKind.CRITIC_CALIBRATION:
135
+ text = f"- Scoring note: {card.content or card.strategy}"
136
+ else:
137
+ text = f"- [{card.kind.value}] {card.content or card.strategy}"
138
+ return text
139
+
140
+ def _diverse_select(
141
+ self, candidates: list[MemoryCard], max_n: int
142
+ ) -> list[MemoryCard]:
143
+ """Select diverse memories — avoid near-duplicates."""
144
+ if len(candidates) <= max_n:
145
+ return candidates
146
+
147
+ selected: list[MemoryCard] = []
148
+ seen_patterns: set[str] = set()
149
+
150
+ for card in candidates:
151
+ # Rough dedup by pattern prefix
152
+ key = (card.pattern or card.content or "")[:50].lower().strip()
153
+ if key in seen_patterns:
154
+ continue
155
+ seen_patterns.add(key)
156
+ selected.append(card)
157
+ if len(selected) >= max_n:
158
+ break
159
+
160
+ return selected