guychuk commited on
Commit
13ed231
·
verified ·
1 Parent(s): 6314bb2

feat: add ExecutionEncoder source

Browse files
Files changed (1) hide show
  1. source/execution_encoder.py +406 -0
source/execution_encoder.py ADDED
@@ -0,0 +1,406 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ExecutionEncoder: Graph-Based Transformer for Execution Plan Encoding
3
+
4
+ This module implements the transformer-based encoder that maps
5
+ (plan_graph, provenance_metadata) → z_e ∈ R^1024.
6
+
7
+ The ExecutionEncoder is the complementary half of the JEPA dual-encoder architecture,
8
+ encoding proposed execution plans into the same latent space as governance policies
9
+ to enable energy-based security validation.
10
+
11
+ Architecture:
12
+ - Graph Neural Network for tool-call dependency encoding
13
+ - Provenance-aware attention mechanism
14
+ - Scope metadata integration
15
+ - Differentiable for end-to-end training with energy functions
16
+
17
+ References:
18
+ - Graph Attention Networks: https://arxiv.org/abs/1710.10903
19
+ - Relational Graph Convolutional Networks: https://arxiv.org/abs/1703.06103
20
+ """
21
+
22
+ from enum import IntEnum
23
+ from typing import Any
24
+
25
+ import torch
26
+ import torch.nn as nn
27
+ import torch.nn.functional as F
28
+ from pydantic import BaseModel, Field, field_validator
29
+
30
+
31
+ class TrustTier(IntEnum):
32
+ """Trust levels for data provenance (as per Dawn Song's workstream)."""
33
+ INTERNAL = 1 # System instructions, internal databases
34
+ SIGNED_PARTNER = 2 # Verified external sources
35
+ PUBLIC_WEB = 3 # Untrusted retrieval (RAG, web scraping)
36
+
37
+
38
+ class ToolCallNode(BaseModel):
39
+ """A single tool invocation in the execution plan graph."""
40
+ tool_name: str = Field(..., min_length=1, description="Name of the tool being invoked")
41
+ arguments: dict[str, Any] = Field(default_factory=dict, description="Tool arguments")
42
+
43
+ # Provenance metadata
44
+ provenance_tier: TrustTier = Field(default=TrustTier.INTERNAL, description="Trust tier of instruction source")
45
+ provenance_hash: str | None = Field(default=None, description="Cryptographic hash of source")
46
+
47
+ # Scope metadata
48
+ scope_volume: int = Field(default=1, ge=1, description="Data volume (rows, records, files)")
49
+ scope_sensitivity: int = Field(default=1, ge=1, le=5, description="Sensitivity level (1=public, 5=critical)")
50
+
51
+ # Graph metadata
52
+ node_id: str = Field(..., description="Unique node identifier")
53
+
54
+ @field_validator('provenance_tier', mode='before')
55
+ @classmethod
56
+ def parse_trust_tier(cls, v):
57
+ """Parse trust tier from int or TrustTier."""
58
+ if isinstance(v, int):
59
+ return TrustTier(v)
60
+ return v
61
+
62
+
63
+ class ExecutionPlan(BaseModel):
64
+ """Complete execution plan represented as a typed tool-call graph."""
65
+ nodes: list[ToolCallNode] = Field(..., min_length=1, description="Tool invocation nodes")
66
+ edges: list[tuple[str, str]] = Field(default_factory=list, description="Data flow edges (src_id, dst_id)")
67
+
68
+ @field_validator('edges')
69
+ @classmethod
70
+ def validate_edges(cls, v, info):
71
+ """Ensure edge endpoints reference valid nodes."""
72
+ if 'nodes' not in info.data:
73
+ return v
74
+
75
+ node_ids = {node.node_id for node in info.data['nodes']}
76
+ for src, dst in v:
77
+ if src not in node_ids or dst not in node_ids:
78
+ raise ValueError(f"Edge ({src}, {dst}) references non-existent node")
79
+ return v
80
+
81
+
82
+ class ProvenanceEmbedding(nn.Module):
83
+ """Embeds provenance metadata (trust tier + cryptographic hash)."""
84
+
85
+ def __init__(self, hidden_dim: int, num_tiers: int = 3):
86
+ super().__init__()
87
+ self.hidden_dim = hidden_dim
88
+ self.tier_embedding = nn.Embedding(num_tiers + 1, hidden_dim) # +1 for padding
89
+ self.scope_projection = nn.Linear(2, hidden_dim) # volume + sensitivity
90
+ self.fusion = nn.Linear(hidden_dim * 2, hidden_dim)
91
+
92
+ def forward(
93
+ self,
94
+ tier_indices: torch.Tensor,
95
+ scope_volume: torch.Tensor,
96
+ scope_sensitivity: torch.Tensor
97
+ ) -> torch.Tensor:
98
+ """Combine provenance tier and scope metadata."""
99
+ tier_emb = self.tier_embedding(tier_indices)
100
+
101
+ # Log-scale volume to handle wide range (1 to 1M+)
102
+ log_volume = torch.log1p(scope_volume.float()).unsqueeze(-1)
103
+ sensitivity = scope_sensitivity.float().unsqueeze(-1)
104
+ scope_features = torch.cat([log_volume, sensitivity], dim=-1)
105
+ scope_emb = self.scope_projection(scope_features)
106
+
107
+ combined = torch.cat([tier_emb, scope_emb], dim=-1)
108
+ return self.fusion(combined)
109
+
110
+
111
+ class GraphAttention(nn.Module):
112
+ """
113
+ Graph Attention layer for encoding tool-call dependencies.
114
+ Implements message passing with edge-aware attention.
115
+ """
116
+
117
+ def __init__(
118
+ self,
119
+ hidden_dim: int,
120
+ num_heads: int = 8,
121
+ dropout: float = 0.1
122
+ ):
123
+ super().__init__()
124
+ self.hidden_dim = hidden_dim
125
+ self.num_heads = num_heads
126
+ self.head_dim = hidden_dim // num_heads
127
+
128
+ assert hidden_dim % num_heads == 0, "hidden_dim must be divisible by num_heads"
129
+
130
+ self.q_proj = nn.Linear(hidden_dim, hidden_dim)
131
+ self.k_proj = nn.Linear(hidden_dim, hidden_dim)
132
+ self.v_proj = nn.Linear(hidden_dim, hidden_dim)
133
+ self.out_proj = nn.Linear(hidden_dim, hidden_dim)
134
+
135
+ self.dropout = nn.Dropout(dropout)
136
+ self.scale = self.head_dim ** -0.5
137
+
138
+ def forward(
139
+ self,
140
+ x: torch.Tensor,
141
+ adjacency: torch.Tensor
142
+ ) -> torch.Tensor:
143
+ """
144
+ Apply graph attention.
145
+
146
+ Args:
147
+ x: Node features [batch_size, num_nodes, hidden_dim]
148
+ adjacency: Adjacency matrix [batch_size, num_nodes, num_nodes]
149
+ 1 = edge exists, 0 = no edge
150
+ """
151
+ batch_size, num_nodes, _ = x.shape
152
+
153
+ q = self.q_proj(x).view(batch_size, num_nodes, self.num_heads, self.head_dim).transpose(1, 2)
154
+ k = self.k_proj(x).view(batch_size, num_nodes, self.num_heads, self.head_dim).transpose(1, 2)
155
+ v = self.v_proj(x).view(batch_size, num_nodes, self.num_heads, self.head_dim).transpose(1, 2)
156
+
157
+ scores = torch.matmul(q, k.transpose(-2, -1)) * self.scale
158
+
159
+ # Mask attention to respect graph structure
160
+ # Also add self-loops (diagonal) for residual connections
161
+ mask = adjacency.unsqueeze(1) # [batch, 1, nodes, nodes]
162
+ eye = torch.eye(num_nodes, device=x.device).unsqueeze(0).unsqueeze(0)
163
+ mask = torch.maximum(mask, eye) # Add self-loops
164
+
165
+ scores = scores.masked_fill(mask == 0, float('-inf'))
166
+ attn_weights = F.softmax(scores, dim=-1)
167
+ attn_weights = self.dropout(attn_weights)
168
+
169
+ out = torch.matmul(attn_weights, v)
170
+ out = out.transpose(1, 2).contiguous().view(batch_size, num_nodes, self.hidden_dim)
171
+
172
+ return self.out_proj(out)
173
+
174
+
175
+ class GraphTransformerBlock(nn.Module):
176
+ """Transformer block with graph-aware attention."""
177
+
178
+ def __init__(
179
+ self,
180
+ hidden_dim: int,
181
+ num_heads: int,
182
+ dropout: float = 0.1
183
+ ):
184
+ super().__init__()
185
+ self.attention = GraphAttention(hidden_dim, num_heads, dropout)
186
+ self.norm1 = nn.LayerNorm(hidden_dim)
187
+
188
+ self.ffn = nn.Sequential(
189
+ nn.Linear(hidden_dim, hidden_dim * 4),
190
+ nn.GELU(),
191
+ nn.Dropout(dropout),
192
+ nn.Linear(hidden_dim * 4, hidden_dim),
193
+ nn.Dropout(dropout)
194
+ )
195
+ self.norm2 = nn.LayerNorm(hidden_dim)
196
+
197
+ def forward(
198
+ self,
199
+ x: torch.Tensor,
200
+ adjacency: torch.Tensor
201
+ ) -> torch.Tensor:
202
+ """Apply graph transformer block."""
203
+ x = x + self.attention(self.norm1(x), adjacency)
204
+ x = x + self.ffn(self.norm2(x))
205
+ return x
206
+
207
+
208
+ class ExecutionEncoder(nn.Module):
209
+ """
210
+ Graph-based transformer encoder mapping execution plans to z_e ∈ R^1024.
211
+
212
+ Encodes:
213
+ - Tool invocation sequences
214
+ - Data flow dependencies (graph edges)
215
+ - Provenance metadata (trust tiers)
216
+ - Scope metadata (volume + sensitivity)
217
+
218
+ Performance targets:
219
+ - Latency: <100ms on CPU (pairs with GovernanceEncoder's 98ms)
220
+ - Memory: <500MB
221
+ - Differentiable: Yes
222
+ """
223
+
224
+ def __init__(
225
+ self,
226
+ latent_dim: int = 1024,
227
+ hidden_dim: int = 512,
228
+ num_layers: int = 4,
229
+ num_heads: int = 8,
230
+ max_nodes: int = 64,
231
+ dropout: float = 0.1,
232
+ vocab_size: int = 10000
233
+ ):
234
+ super().__init__()
235
+
236
+ self.latent_dim = latent_dim
237
+ self.hidden_dim = hidden_dim
238
+ self.max_nodes = max_nodes
239
+
240
+ # Token embeddings for tool names and arguments
241
+ self.token_embedding = nn.Embedding(vocab_size, hidden_dim)
242
+ self.position_embedding = nn.Embedding(max_nodes, hidden_dim)
243
+
244
+ # Provenance and scope embeddings
245
+ self.provenance_embedding = ProvenanceEmbedding(hidden_dim)
246
+
247
+ # Graph transformer layers
248
+ self.layers = nn.ModuleList([
249
+ GraphTransformerBlock(hidden_dim, num_heads, dropout)
250
+ for _ in range(num_layers)
251
+ ])
252
+
253
+ # Pooling and projection
254
+ self.attention_pool = nn.Linear(hidden_dim, 1)
255
+ self.projection = nn.Sequential(
256
+ nn.Linear(hidden_dim, latent_dim * 2),
257
+ nn.GELU(),
258
+ nn.Dropout(dropout),
259
+ nn.Linear(latent_dim * 2, latent_dim),
260
+ nn.LayerNorm(latent_dim)
261
+ )
262
+
263
+ self.input_norm = nn.LayerNorm(hidden_dim)
264
+
265
+ def _tokenize(self, text: str) -> int:
266
+ """
267
+ Hash-based tokenization (v0.1.0).
268
+
269
+ Future: Replace with BPE tokenizer (v0.2.0) to reduce collisions.
270
+ """
271
+ return hash(text) % 10000
272
+
273
+ def _create_adjacency_matrix(
274
+ self,
275
+ num_nodes: int,
276
+ edges: list[tuple[int, int]],
277
+ device: torch.device
278
+ ) -> torch.Tensor:
279
+ """Build adjacency matrix from edge list."""
280
+ adjacency = torch.zeros(num_nodes, num_nodes, device=device)
281
+ for src, dst in edges:
282
+ if src < num_nodes and dst < num_nodes:
283
+ adjacency[src, dst] = 1
284
+ return adjacency
285
+
286
+ def forward(
287
+ self,
288
+ plan: ExecutionPlan | dict[str, Any]
289
+ ) -> torch.Tensor:
290
+ """
291
+ Encode execution plan into latent vector.
292
+
293
+ Args:
294
+ plan: ExecutionPlan or dict conforming to ExecutionPlan schema
295
+
296
+ Returns:
297
+ z_e: Latent vector [1, latent_dim]
298
+ """
299
+ # Validate and parse input
300
+ if not isinstance(plan, ExecutionPlan):
301
+ plan = ExecutionPlan(**plan)
302
+
303
+ nodes = plan.nodes
304
+ edges = plan.edges
305
+
306
+ # Build node ID mapping
307
+ node_id_to_idx = {node.node_id: i for i, node in enumerate(nodes)}
308
+ edge_indices = [(node_id_to_idx[src], node_id_to_idx[dst]) for src, dst in edges]
309
+
310
+ # Pad or truncate to max_nodes
311
+ num_nodes = min(len(nodes), self.max_nodes)
312
+ nodes = nodes[:num_nodes]
313
+
314
+ # Tokenize tool names and arguments
315
+ tool_tokens = []
316
+ for node in nodes:
317
+ # Combine tool name + serialized args for richer representation
318
+ arg_str = ",".join(f"{k}={v}" for k, v in sorted(node.arguments.items()))
319
+ combined = f"{node.tool_name}({arg_str})"
320
+ tool_tokens.append(self._tokenize(combined))
321
+
322
+ # Pad tokens
323
+ if len(tool_tokens) < self.max_nodes:
324
+ tool_tokens.extend([0] * (self.max_nodes - len(tool_tokens)))
325
+
326
+ # Infer device from model parameters so tensors land on the right device (cpu/mps/cuda)
327
+ device = next(self.parameters()).device
328
+
329
+ # Convert to tensors
330
+ token_ids = torch.tensor(tool_tokens[:self.max_nodes], device=device).unsqueeze(0)
331
+ position_ids = torch.arange(self.max_nodes, device=device).unsqueeze(0)
332
+
333
+ # Provenance and scope metadata
334
+ tier_indices = torch.tensor([node.provenance_tier for node in nodes] + [0] * (self.max_nodes - num_nodes), device=device).unsqueeze(0)
335
+ scope_volume = torch.tensor([node.scope_volume for node in nodes] + [1] * (self.max_nodes - num_nodes), device=device).unsqueeze(0)
336
+ scope_sensitivity = torch.tensor([node.scope_sensitivity for node in nodes] + [1] * (self.max_nodes - num_nodes), device=device).unsqueeze(0)
337
+
338
+ # Build adjacency matrix
339
+ adjacency = self._create_adjacency_matrix(
340
+ self.max_nodes,
341
+ edge_indices,
342
+ device
343
+ ).unsqueeze(0)
344
+
345
+ # Embed tokens
346
+ token_emb = self.token_embedding(token_ids)
347
+ pos_emb = self.position_embedding(position_ids)
348
+ prov_emb = self.provenance_embedding(tier_indices, scope_volume, scope_sensitivity)
349
+
350
+ # Combine embeddings
351
+ x = token_emb + pos_emb + prov_emb
352
+ x = self.input_norm(x)
353
+
354
+ # Apply graph transformer layers
355
+ for layer in self.layers:
356
+ x = layer(x, adjacency)
357
+
358
+ # Attention pooling over nodes
359
+ attn_scores = self.attention_pool(x).squeeze(-1)
360
+ attn_weights = F.softmax(attn_scores, dim=-1).unsqueeze(1)
361
+ pooled = torch.matmul(attn_weights, x).squeeze(1)
362
+
363
+ # Project to latent space
364
+ z_e = self.projection(pooled)
365
+
366
+ return z_e
367
+
368
+ def encode_batch(self, plans: list[ExecutionPlan]) -> torch.Tensor:
369
+ """
370
+ Batch encoding of multiple execution plans.
371
+
372
+ Args:
373
+ plans: List of ExecutionPlan objects
374
+
375
+ Returns:
376
+ z_e: Latent vectors [batch_size, latent_dim]
377
+ """
378
+ latents = [self.forward(plan) for plan in plans]
379
+ return torch.cat(latents, dim=0)
380
+
381
+
382
+ def create_execution_encoder(
383
+ latent_dim: int = 1024,
384
+ checkpoint_path: str | None = None,
385
+ device: str = "cpu"
386
+ ) -> ExecutionEncoder:
387
+ """
388
+ Factory function to create ExecutionEncoder.
389
+
390
+ Args:
391
+ latent_dim: Dimension of output latent vector (must match GovernanceEncoder)
392
+ checkpoint_path: Optional path to pretrained weights
393
+ device: Device to load model on
394
+
395
+ Returns:
396
+ Initialized ExecutionEncoder in inference mode
397
+ """
398
+ model = ExecutionEncoder(latent_dim=latent_dim)
399
+
400
+ if checkpoint_path is not None:
401
+ model.load_state_dict(torch.load(checkpoint_path, map_location=device, weights_only=True))
402
+
403
+ model = model.to(device)
404
+ model.training = False # Set to inference mode
405
+
406
+ return model