ryanyen22 commited on
Commit
4839aff
·
verified ·
1 Parent(s): c133ebf

feat: add reason_first_program/steering.py

Browse files
Files changed (1) hide show
  1. reason_first_program/steering.py +438 -0
reason_first_program/steering.py ADDED
@@ -0,0 +1,438 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Stage 4: Query Language and Steering Interface
3
+
4
+ Provides a formal query language for navigating the program space by composing
5
+ concepts. Users express preferences as concept coordinates, and the system
6
+ steers LLM generation accordingly.
7
+
8
+ The query language supports:
9
+ - Single concept selection: steer("recursive", strength=0.8)
10
+ - Concept composition: steer(recursive=0.8, space_efficient=0.6)
11
+ - Concept negation: steer(mutation=-0.5) (avoid mutation)
12
+ - Region queries: select(region_where(recursive > 0.5, fast_execution > 0.7))
13
+ - Lattice navigation: refine(current, add_concept="memoization")
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import logging
19
+ import re
20
+ from dataclasses import dataclass, field
21
+ from typing import Any, Optional, Union
22
+
23
+ import numpy as np
24
+
25
+ from reason_first_program.program_space import Program, ProgramSpace
26
+ from reason_first_program.concepts import Concept, ConceptSet
27
+ from reason_first_program.embeddings import (
28
+ ConceptEmbeddingSpace,
29
+ GCAVEmbedding,
30
+ MSRSSteering,
31
+ )
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+
36
+ @dataclass
37
+ class ConceptQuery:
38
+ """
39
+ A query in the concept space.
40
+
41
+ A query is a weighted combination of concepts that defines a target
42
+ region in the program space. The system either:
43
+ 1. Selects existing programs nearest to this region, or
44
+ 2. Steers generation toward this region.
45
+
46
+ Formally: q = Σ_i w_i · v_i where v_i is concept i's activation vector
47
+ """
48
+
49
+ weights: dict[str, float] = field(default_factory=dict)
50
+ constraints: dict[str, tuple[str, float]] = field(default_factory=dict)
51
+ # constraints: {concept_name: (operator, threshold)} e.g., {"recursive": (">", 0.5)}
52
+ metadata: dict[str, Any] = field(default_factory=dict)
53
+
54
+ def __repr__(self) -> str:
55
+ parts = []
56
+ for name, weight in sorted(self.weights.items(), key=lambda x: -abs(x[1])):
57
+ if weight > 0:
58
+ parts.append(f"+{weight:.1f}·{name}")
59
+ else:
60
+ parts.append(f"{weight:.1f}·{name}")
61
+ for name, (op, val) in self.constraints.items():
62
+ parts.append(f"{name}{op}{val:.1f}")
63
+ return f"Query({', '.join(parts)})"
64
+
65
+ @property
66
+ def concept_vector(self) -> dict[str, float]:
67
+ """The query as a concept-space direction vector."""
68
+ return self.weights.copy()
69
+
70
+ def matches(self, concept_scores: dict[str, float]) -> bool:
71
+ """Check if a program's concept scores satisfy the query constraints."""
72
+ for name, (op, threshold) in self.constraints.items():
73
+ score = concept_scores.get(name, 0.0)
74
+ if op == ">" and not (score > threshold):
75
+ return False
76
+ elif op == ">=" and not (score >= threshold):
77
+ return False
78
+ elif op == "<" and not (score < threshold):
79
+ return False
80
+ elif op == "<=" and not (score <= threshold):
81
+ return False
82
+ elif op == "==" and not (abs(score - threshold) < 0.05):
83
+ return False
84
+ return True
85
+
86
+ def distance_to(self, concept_scores: dict[str, float]) -> float:
87
+ """
88
+ Compute distance from a program's concept scores to this query.
89
+ Lower = more aligned with query.
90
+ """
91
+ total = 0.0
92
+ for name, target_weight in self.weights.items():
93
+ actual = concept_scores.get(name, 0.0)
94
+ # Distance weighted by how strongly we care about this concept
95
+ total += abs(target_weight) * (actual - (1.0 if target_weight > 0 else 0.0)) ** 2
96
+ return total
97
+
98
+
99
+ class QueryLanguage:
100
+ """
101
+ Parser and builder for concept queries.
102
+
103
+ Supports a simple DSL:
104
+ "recursive > 0.5 AND fast_execution > 0.7"
105
+ "recursive=0.8, space_efficient=0.6, mutation=-0.3"
106
+ "LIKE program_id_abc123" (find programs similar to a reference)
107
+ "NOT mutation" (avoid mutation)
108
+ """
109
+
110
+ def __init__(self, concept_set: ConceptSet):
111
+ self.concept_set = concept_set
112
+
113
+ def parse(self, query_str: str) -> ConceptQuery:
114
+ """Parse a query string into a ConceptQuery."""
115
+ query = ConceptQuery()
116
+
117
+ # Handle comma-separated weight assignments: "recursive=0.8, mutation=-0.3"
118
+ weight_pattern = r"(\w+)\s*=\s*(-?\d+\.?\d*)"
119
+ for match in re.finditer(weight_pattern, query_str):
120
+ name = match.group(1)
121
+ weight = float(match.group(2))
122
+ if self.concept_set.get_by_name(name):
123
+ query.weights[name] = weight
124
+
125
+ # Handle constraint expressions: "recursive > 0.5"
126
+ constraint_pattern = r"(\w+)\s*(>=|<=|>|<|==)\s*(-?\d+\.?\d*)"
127
+ for match in re.finditer(constraint_pattern, query_str):
128
+ name = match.group(1)
129
+ op = match.group(2)
130
+ threshold = float(match.group(3))
131
+ if name not in query.weights: # Don't double-count
132
+ if self.concept_set.get_by_name(name):
133
+ query.constraints[name] = (op, threshold)
134
+
135
+ # Handle NOT: "NOT mutation"
136
+ not_pattern = r"NOT\s+(\w+)"
137
+ for match in re.finditer(not_pattern, query_str, re.IGNORECASE):
138
+ name = match.group(1)
139
+ if self.concept_set.get_by_name(name):
140
+ query.weights[name] = query.weights.get(name, -1.0)
141
+
142
+ return query
143
+
144
+ def build(self, **concept_weights: float) -> ConceptQuery:
145
+ """Build a query from keyword arguments."""
146
+ validated = {}
147
+ for name, weight in concept_weights.items():
148
+ if self.concept_set.get_by_name(name):
149
+ validated[name] = weight
150
+ else:
151
+ logger.warning(f"Unknown concept: {name}")
152
+ return ConceptQuery(weights=validated)
153
+
154
+ def constrain(self, **constraints: str) -> ConceptQuery:
155
+ """
156
+ Build a constraint query.
157
+ Example: constrain(recursive=">0.5", fast_execution=">=0.7")
158
+ """
159
+ query = ConceptQuery()
160
+ for name, expr in constraints.items():
161
+ if not self.concept_set.get_by_name(name):
162
+ logger.warning(f"Unknown concept: {name}")
163
+ continue
164
+ match = re.match(r"(>=|<=|>|<|==)?\s*(-?\d+\.?\d*)", expr)
165
+ if match:
166
+ op = match.group(1) or ">"
167
+ threshold = float(match.group(2))
168
+ query.constraints[name] = (op, threshold)
169
+ return query
170
+
171
+
172
+ class SteeringEngine:
173
+ """
174
+ Engine for steering program generation using concept queries.
175
+
176
+ Two modes:
177
+ 1. Selection: Given a ProgramSpace and a query, rank/filter programs
178
+ 2. Generation: Given a query, steer LLM hidden states during generation
179
+
180
+ Selection uses concept scores directly.
181
+ Generation uses GCAV vectors (e' = e + ε·v) or MSRS orthogonal steering.
182
+ """
183
+
184
+ def __init__(
185
+ self,
186
+ concept_set: ConceptSet,
187
+ embedding_space: Optional[ConceptEmbeddingSpace] = None,
188
+ gcav: Optional[GCAVEmbedding] = None,
189
+ msrs: Optional[MSRSSteering] = None,
190
+ ):
191
+ self.concept_set = concept_set
192
+ self.embedding_space = embedding_space
193
+ self.gcav = gcav
194
+ self.msrs = msrs
195
+ self.query_language = QueryLanguage(concept_set)
196
+
197
+ # ---- Selection Mode ----
198
+
199
+ def select(
200
+ self,
201
+ space: ProgramSpace,
202
+ query: Union[ConceptQuery, str],
203
+ top_k: int = 10,
204
+ ) -> list[tuple[Program, float]]:
205
+ """
206
+ Select programs from the space that best match the query.
207
+
208
+ Returns list of (program, relevance_score) tuples, sorted by relevance.
209
+ """
210
+ if isinstance(query, str):
211
+ query = self.query_language.parse(query)
212
+
213
+ scored: list[tuple[Program, float]] = []
214
+
215
+ for program in space.valid_programs:
216
+ concept_scores = self.concept_set.score_program(program)
217
+
218
+ # Check hard constraints
219
+ if not query.matches(concept_scores):
220
+ continue
221
+
222
+ # Compute soft relevance score
223
+ relevance = self._compute_relevance(concept_scores, query)
224
+ scored.append((program, relevance))
225
+
226
+ # Sort by relevance (higher = better match)
227
+ scored.sort(key=lambda x: x[1], reverse=True)
228
+ return scored[:top_k]
229
+
230
+ def _compute_relevance(
231
+ self,
232
+ concept_scores: dict[str, float],
233
+ query: ConceptQuery,
234
+ ) -> float:
235
+ """
236
+ Compute relevance of a program to a query.
237
+
238
+ For positive weights: reward high concept scores
239
+ For negative weights: reward low concept scores
240
+ """
241
+ relevance = 0.0
242
+ total_weight = 0.0
243
+
244
+ for name, target_weight in query.weights.items():
245
+ actual = concept_scores.get(name, 0.0)
246
+ if target_weight > 0:
247
+ relevance += target_weight * actual
248
+ else:
249
+ relevance += abs(target_weight) * (1.0 - actual)
250
+ total_weight += abs(target_weight)
251
+
252
+ if total_weight > 0:
253
+ relevance /= total_weight
254
+
255
+ return relevance
256
+
257
+ def filter(
258
+ self,
259
+ space: ProgramSpace,
260
+ query: Union[ConceptQuery, str],
261
+ ) -> ProgramSpace:
262
+ """Filter a ProgramSpace to programs matching the query."""
263
+ if isinstance(query, str):
264
+ query = self.query_language.parse(query)
265
+
266
+ filtered = ProgramSpace(space.stub)
267
+ for program in space.valid_programs:
268
+ concept_scores = self.concept_set.score_program(program)
269
+ if query.matches(concept_scores):
270
+ filtered.add(program)
271
+ return filtered
272
+
273
+ # ---- Generation Steering Mode ----
274
+
275
+ def build_steering_vector(
276
+ self,
277
+ query: Union[ConceptQuery, str],
278
+ method: str = "additive",
279
+ ) -> Optional[np.ndarray]:
280
+ """
281
+ Build a steering vector from a concept query.
282
+
283
+ Args:
284
+ query: The concept query
285
+ method: 'additive' (GCAV sum) or 'msrs' (orthogonal subspaces)
286
+
287
+ Returns:
288
+ Steering vector in activation space, or None if no GCAV available
289
+ """
290
+ if isinstance(query, str):
291
+ query = self.query_language.parse(query)
292
+
293
+ if method == "additive" and self.gcav is not None:
294
+ # Simple additive: v_steer = Σ w_i · v_i
295
+ steer = np.zeros_like(
296
+ next(iter(self.gcav.concept_vectors.values()))
297
+ )
298
+ for name, weight in query.weights.items():
299
+ if name in self.gcav.concept_vectors:
300
+ steer += weight * self.gcav.concept_vectors[name]
301
+ return steer
302
+
303
+ elif method == "msrs" and self.msrs is not None:
304
+ # Use MSRS orthogonal steering
305
+ base = np.zeros(self.msrs.S_align.shape[1])
306
+ return self.msrs.steer(base, query.weights) - base
307
+
308
+ return None
309
+
310
+ def steer_prompt(
311
+ self,
312
+ query: Union[ConceptQuery, str],
313
+ base_prompt: str,
314
+ ) -> str:
315
+ """
316
+ Augment a generation prompt with concept-steering instructions.
317
+
318
+ This is a lightweight steering approach that works with any LLM API
319
+ (no hidden state access needed). For stronger steering, use
320
+ build_steering_vector with activation-level intervention.
321
+ """
322
+ if isinstance(query, str):
323
+ query = self.query_language.parse(query)
324
+
325
+ concept_instructions = []
326
+ for name, weight in sorted(
327
+ query.weights.items(), key=lambda x: -abs(x[1])
328
+ ):
329
+ concept = self.concept_set.get_by_name(name)
330
+ if concept is None:
331
+ continue
332
+
333
+ if weight > 0.5:
334
+ concept_instructions.append(
335
+ f"STRONGLY PREFER: {concept.description}"
336
+ )
337
+ elif weight > 0:
338
+ concept_instructions.append(
339
+ f"PREFER: {concept.description}"
340
+ )
341
+ elif weight < -0.5:
342
+ concept_instructions.append(
343
+ f"STRONGLY AVOID: {concept.description}"
344
+ )
345
+ elif weight < 0:
346
+ concept_instructions.append(
347
+ f"AVOID: {concept.description}"
348
+ )
349
+
350
+ for name, (op, threshold) in query.constraints.items():
351
+ concept = self.concept_set.get_by_name(name)
352
+ if concept:
353
+ concept_instructions.append(
354
+ f"CONSTRAINT: {concept.description} ({op} {threshold})"
355
+ )
356
+
357
+ if not concept_instructions:
358
+ return base_prompt
359
+
360
+ steering_block = "\n".join(
361
+ f" - {inst}" for inst in concept_instructions
362
+ )
363
+ return (
364
+ f"{base_prompt}\n\n"
365
+ f"CONCEPT STEERING INSTRUCTIONS:\n{steering_block}\n\n"
366
+ f"Follow the above concept preferences when implementing."
367
+ )
368
+
369
+ # ---- Exploration Mode ----
370
+
371
+ def explore_neighbors(
372
+ self,
373
+ program: Program,
374
+ space: ProgramSpace,
375
+ n_neighbors: int = 5,
376
+ ) -> list[tuple[Program, float, dict[str, float]]]:
377
+ """
378
+ Find programs in the space that are conceptually nearby.
379
+
380
+ Returns list of (program, distance, concept_diff) tuples.
381
+ concept_diff shows which concepts differ most.
382
+ """
383
+ ref_scores = self.concept_set.score_program(program)
384
+
385
+ neighbors: list[tuple[Program, float, dict[str, float]]] = []
386
+ for other in space.valid_programs:
387
+ if other.program_id == program.program_id:
388
+ continue
389
+
390
+ other_scores = self.concept_set.score_program(other)
391
+
392
+ # Euclidean distance in concept space
393
+ diff = {}
394
+ dist_sq = 0.0
395
+ for name in set(ref_scores) | set(other_scores):
396
+ d = ref_scores.get(name, 0.0) - other_scores.get(name, 0.0)
397
+ if abs(d) > 0.01:
398
+ diff[name] = d
399
+ dist_sq += d ** 2
400
+
401
+ neighbors.append((other, dist_sq ** 0.5, diff))
402
+
403
+ neighbors.sort(key=lambda x: x[1])
404
+ return neighbors[:n_neighbors]
405
+
406
+ def concept_boundary_programs(
407
+ self,
408
+ concept_name: str,
409
+ space: ProgramSpace,
410
+ n_per_side: int = 3,
411
+ ) -> dict[str, list[Program]]:
412
+ """
413
+ Find programs at the boundary of a concept.
414
+ Returns programs just inside and just outside the concept region.
415
+ """
416
+ concept = self.concept_set.get_by_name(concept_name)
417
+ if concept is None:
418
+ return {"inside": [], "outside": []}
419
+
420
+ inside: list[tuple[Program, float]] = []
421
+ outside: list[tuple[Program, float]] = []
422
+
423
+ for program in space.valid_programs:
424
+ score = concept.score(program)
425
+ if score > 0.5:
426
+ inside.append((program, score))
427
+ else:
428
+ outside.append((program, score))
429
+
430
+ # Sort inside by score ascending (closest to boundary)
431
+ inside.sort(key=lambda x: x[1])
432
+ # Sort outside by score descending (closest to boundary)
433
+ outside.sort(key=lambda x: x[1], reverse=True)
434
+
435
+ return {
436
+ "inside": [p for p, _ in inside[:n_per_side]],
437
+ "outside": [p for p, _ in outside[:n_per_side]],
438
+ }