feat: add ExecutionEncoder source
Browse files- 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
|