Download code/graph/types.py from frontal-labs/graph-reasoner-typed-compositional: direct link, hf CLI and curl.
- Browser
- Download file 15 kB
-
https://huggingface.co/frontal-labs/graph-reasoner-typed-compositional/resolve/main/code/graph/types.py
- Command line
-
hf download hf://frontal-labs/graph-reasoner-typed-compositional/code/graph/types.py
-
curl -L -o types.py https://huggingface.co/frontal-labs/graph-reasoner-typed-compositional/resolve/main/code/graph/types.py
15 kB
| """Typed contracts shared by the GRM model, search, verifier, and API.""" | |
| from __future__ import annotations | |
| from collections.abc import Mapping | |
| from dataclasses import dataclass, field | |
| from enum import StrEnum | |
| from typing import Any | |
| import torch | |
| from torch import Tensor | |
| class ReasoningStatus(StrEnum): | |
| SUPPORTED = "supported" | |
| UNSUPPORTED = "unsupported" | |
| CONTRADICTED = "contradicted" | |
| INSUFFICIENT_EVIDENCE = "insufficient_evidence" | |
| ERROR = "error" | |
| class GraphTensors: | |
| """A validated, batch-agnostic graph tensor contract. | |
| ``edge_index`` is shaped ``[2, E]`` and stores source/target node indices. | |
| IDs are indices into the graph tensors, never user-facing entity IDs. | |
| Two kinds of type id are carried, and the distinction is load-bearing: | |
| ``node_type_ids`` / ``edge_type_ids`` | |
| *Local* indices -- positions within this graph's sorted type and relation | |
| lists. They exist for bookkeeping: pooling per-edge states into a | |
| per-graph relation bank, and mapping a bank row back to a relation name. | |
| ``node_type_hashes`` / ``edge_type_hashes`` | |
| *Global, graph-independent* identities, used for the learned embeddings. | |
| Using the local index for the embedding lookup was a significant bug. Local | |
| indices come from the alphabetical rank of a name *within one graph*, so a | |
| graph that simply lacked one relation shifted every later index: ``owns`` | |
| could be embedding row 1 in one graph and row 0 in the next. The relation and | |
| type embedding tables could therefore not learn a stable representation of any | |
| relation, and renaming labels reshuffled everything, which is what collapsed | |
| the unseen-ontology holdout. | |
| """ | |
| node_ids: Tensor | |
| node_type_ids: Tensor | |
| node_features: Tensor | |
| edge_index: Tensor | |
| edge_type_ids: Tensor | |
| edge_features: Tensor | |
| graph_id: Tensor | |
| #: Stable global type identities for the learned embeddings. Default to the | |
| #: local ids so older callers keep working, with the drift they always had. | |
| node_type_hashes: Tensor | None = None | |
| edge_type_hashes: Tensor | None = None | |
| def __post_init__(self) -> None: | |
| _require_rank(self.node_ids, 1, "node_ids") | |
| _require_rank(self.node_type_ids, 1, "node_type_ids") | |
| _require_rank(self.node_features, 2, "node_features") | |
| _require_rank(self.edge_index, 2, "edge_index") | |
| _require_rank(self.edge_type_ids, 1, "edge_type_ids") | |
| _require_rank(self.edge_features, 2, "edge_features") | |
| _require_rank(self.graph_id, 1, "graph_id") | |
| if self.edge_index.shape[0] != 2: | |
| raise ValueError( | |
| f"edge_index must have shape [2, E], got {tuple(self.edge_index.shape)}" | |
| ) | |
| n = self.node_ids.shape[0] | |
| e = self.edge_index.shape[1] | |
| if self.node_type_ids.shape[0] != n or self.node_features.shape[0] != n: | |
| raise ValueError( | |
| "node_ids, node_type_ids, and node_features must have the same first dimension" | |
| ) | |
| if self.edge_type_ids.shape[0] != e or self.edge_features.shape[0] != e: | |
| raise ValueError( | |
| "edge_index, edge_type_ids, and edge_features must have the same edge dimension" | |
| ) | |
| if self.graph_id.shape[0] != n: | |
| raise ValueError("graph_id must contain one graph ID per node") | |
| if self.edge_index.numel() and ( | |
| self.edge_index.min() < 0 or self.edge_index.max() >= n | |
| ): | |
| raise ValueError("edge_index contains a node index outside node_ids") | |
| if ( | |
| self.node_ids.dtype not in (torch.int32, torch.int64) | |
| or self.node_type_ids.dtype not in (torch.int32, torch.int64) | |
| or self.edge_type_ids.dtype not in (torch.int32, torch.int64) | |
| or self.graph_id.dtype not in (torch.int32, torch.int64) | |
| ): | |
| raise TypeError( | |
| "node, type, edge-type, and graph IDs must be integer tensors" | |
| ) | |
| if self.edge_index.dtype not in (torch.int32, torch.int64): | |
| raise TypeError("edge_index must be an integer tensor") | |
| if self.node_type_hashes is not None: | |
| _require_rank(self.node_type_hashes, 1, "node_type_hashes") | |
| if self.node_type_hashes.shape[0] != n: | |
| raise ValueError("node_type_hashes must have one entry per node") | |
| if self.edge_type_hashes is not None: | |
| _require_rank(self.edge_type_hashes, 1, "edge_type_hashes") | |
| if self.edge_type_hashes.shape[0] != e: | |
| raise ValueError("edge_type_hashes must have one entry per edge") | |
| if ( | |
| not self.node_features.is_floating_point() | |
| or not self.edge_features.is_floating_point() | |
| ): | |
| raise TypeError( | |
| "node_features and edge_features must be floating-point tensors" | |
| ) | |
| def num_nodes(self) -> int: | |
| return int(self.node_ids.shape[0]) | |
| def num_edges(self) -> int: | |
| return int(self.edge_index.shape[1]) | |
| def node_type_identity(self) -> Tensor: | |
| """Global node-type ids when present, else the local indices.""" | |
| return ( | |
| self.node_type_ids | |
| if self.node_type_hashes is None | |
| else self.node_type_hashes | |
| ) | |
| def edge_type_identity(self) -> Tensor: | |
| """Global relation ids when present, else the local indices.""" | |
| return ( | |
| self.edge_type_ids | |
| if self.edge_type_hashes is None | |
| else self.edge_type_hashes | |
| ) | |
| def to(self, device: torch.device | str) -> GraphTensors: | |
| return GraphTensors( | |
| node_ids=self.node_ids.to(device), | |
| node_type_ids=self.node_type_ids.to(device), | |
| node_features=self.node_features.to(device), | |
| edge_index=self.edge_index.to(device), | |
| edge_type_ids=self.edge_type_ids.to(device), | |
| edge_features=self.edge_features.to(device), | |
| graph_id=self.graph_id.to(device), | |
| node_type_hashes=None | |
| if self.node_type_hashes is None | |
| else self.node_type_hashes.to(device), | |
| edge_type_hashes=None | |
| if self.edge_type_hashes is None | |
| else self.edge_type_hashes.to(device), | |
| ) | |
| class GraphQuery: | |
| text: str | None = None | |
| start_entities: tuple[str, ...] = () | |
| start_entity_ids: tuple[str, ...] = () | |
| allowed_relations: tuple[str, ...] = () | |
| required_relations: tuple[str, ...] = () | |
| required_relation_sequence: tuple[str, ...] = () | |
| forbidden_relations: tuple[str, ...] = () | |
| required_types: tuple[str, ...] = () | |
| forbidden_types: tuple[str, ...] = () | |
| required_intermediate_types: tuple[str, ...] = () | |
| max_hops: int = 3 | |
| min_hops: int = 1 | |
| exact_hops: int | None = None | |
| return_paths: bool = True | |
| verify: bool = True | |
| operation: str = "find_entities" | |
| answer_mode: str = "entity" | |
| target_entity_ids: tuple[str, ...] = () | |
| def __post_init__(self) -> None: | |
| if self.max_hops < 0 or self.min_hops < 0: | |
| raise ValueError("hop bounds must be non-negative") | |
| if self.min_hops > self.max_hops: | |
| raise ValueError("min_hops cannot exceed max_hops") | |
| if self.exact_hops is not None and not ( | |
| self.min_hops <= self.exact_hops <= self.max_hops | |
| ): | |
| raise ValueError("exact_hops must be within min_hops and max_hops") | |
| if self.answer_mode not in { | |
| "entity", | |
| "relation", | |
| "boolean", | |
| "count", | |
| "aggregate", | |
| }: | |
| raise ValueError(f"unsupported answer_mode: {self.answer_mode!r}") | |
| def canonical_text(self) -> str: | |
| return "|".join( | |
| ( | |
| self.operation, | |
| self.text or "", | |
| ",".join(self.start_entity_ids or self.start_entities), | |
| ",".join(self.allowed_relations), | |
| ",".join(self.required_relations), | |
| ",".join(self.required_relation_sequence), | |
| ",".join(self.forbidden_relations), | |
| ",".join(self.required_types), | |
| ",".join(self.forbidden_types), | |
| ",".join(self.required_intermediate_types), | |
| str(self.min_hops), | |
| str(self.max_hops), | |
| str(self.exact_hops), | |
| self.answer_mode, | |
| ",".join(self.target_entity_ids), | |
| ) | |
| ) | |
| class PathStep: | |
| source_id: str | |
| relation_id: str | |
| target_id: str | |
| source_type: str | None = None | |
| target_type: str | None = None | |
| edge_attributes: Mapping[str, str] = field(default_factory=dict) | |
| class GraphPath: | |
| steps: tuple[PathStep, ...] | |
| score: float = 0.0 | |
| verified: bool = False | |
| query_compatibility: float | None = None | |
| def hop_count(self) -> int: | |
| return len(self.steps) | |
| def start_id(self) -> str | None: | |
| return self.steps[0].source_id if self.steps else None | |
| def endpoint_id(self) -> str | None: | |
| return self.steps[-1].target_id if self.steps else None | |
| def relation_sequence(self) -> tuple[str, ...]: | |
| return tuple(step.relation_id for step in self.steps) | |
| class ConstraintState: | |
| allowed_relations: frozenset[str] = frozenset() | |
| required_relations: frozenset[str] = frozenset() | |
| required_relation_sequence: tuple[str, ...] = () | |
| forbidden_relations: frozenset[str] = frozenset() | |
| required_types: frozenset[str] = frozenset() | |
| forbidden_types: frozenset[str] = frozenset() | |
| required_intermediate_types: frozenset[str] = frozenset() | |
| min_hops: int = 0 | |
| max_hops: int = 8 | |
| exact_hops: int | None = None | |
| require_unique_nodes: bool = False | |
| def from_query(cls, query: GraphQuery) -> ConstraintState: | |
| return cls( | |
| allowed_relations=frozenset(query.allowed_relations), | |
| required_relations=frozenset(query.required_relations), | |
| required_relation_sequence=query.required_relation_sequence, | |
| forbidden_relations=frozenset(query.forbidden_relations), | |
| required_types=frozenset(query.required_types), | |
| forbidden_types=frozenset(query.forbidden_types), | |
| required_intermediate_types=frozenset(query.required_intermediate_types), | |
| min_hops=query.min_hops, | |
| max_hops=query.max_hops, | |
| exact_hops=query.exact_hops, | |
| ) | |
| class ReasoningState: | |
| query_embedding: Tensor | |
| entity_states: Tensor | |
| relation_states: Tensor | |
| candidate_nodes: Tensor | |
| candidate_edges: Tensor | |
| candidate_paths: tuple[GraphPath, ...] | |
| path_representations: Tensor | |
| hop: int | |
| constraint_state: ConstraintState | |
| evidence: tuple[GraphPath, ...] = () | |
| confidence: Tensor | None = None | |
| class EntityAnswer: | |
| entity_id: str | |
| score: float | |
| class ReasoningResult: | |
| status: ReasoningStatus | |
| answers: tuple[EntityAnswer, ...] | |
| supporting_paths: tuple[GraphPath, ...] | |
| conflicting_paths: tuple[GraphPath, ...] | |
| confidence: float | |
| calibrated: bool | |
| model_version: str | |
| graph_version: str | |
| reasoning_steps: int | |
| metadata: Mapping[str, Any] = field(default_factory=dict) | |
| relation_answers: tuple[str, ...] = () | |
| boolean_answer: bool | None = None | |
| aggregate_value: int | float | None = None | |
| def to_dict(self) -> dict[str, Any]: | |
| return { | |
| "status": self.status.value, | |
| "answers": [ | |
| {"entity_id": a.entity_id, "score": a.score} for a in self.answers | |
| ], | |
| "supporting_paths": [_path_to_dict(p) for p in self.supporting_paths], | |
| "conflicting_paths": [_path_to_dict(p) for p in self.conflicting_paths], | |
| "confidence": self.confidence, | |
| "calibrated": self.calibrated, | |
| "model_version": self.model_version, | |
| "graph_version": self.graph_version, | |
| "reasoning_steps": self.reasoning_steps, | |
| "metadata": dict(self.metadata), | |
| "relation_answers": list(self.relation_answers), | |
| "boolean_answer": self.boolean_answer, | |
| "aggregate_value": self.aggregate_value, | |
| } | |
| class GraphExample: | |
| graph: GraphTensors | |
| node_external_ids: tuple[str, ...] | |
| node_types: tuple[str, ...] | |
| relation_external_ids: tuple[str, ...] | |
| relation_names: tuple[str, ...] | |
| edge_attributes: tuple[Mapping[str, str], ...] = () | |
| node_attributes: tuple[Mapping[str, str], ...] = () | |
| relation_attributes: tuple[Mapping[str, str], ...] = () | |
| graph_version: str = "synthetic@0" | |
| def __post_init__(self) -> None: | |
| if len(self.node_external_ids) != self.graph.num_nodes: | |
| raise ValueError("node_external_ids must align with graph nodes") | |
| if len(self.node_types) != self.graph.num_nodes: | |
| raise ValueError("node_types must align with graph nodes") | |
| if len(self.edge_attributes) not in (0, self.graph.num_edges): | |
| raise ValueError("edge_attributes must be empty or align with graph edges") | |
| if len(self.node_attributes) not in (0, self.graph.num_nodes): | |
| raise ValueError("node_attributes must be empty or align with graph nodes") | |
| if len(self.relation_external_ids) != len(self.relation_names): | |
| raise ValueError("relation IDs and names must align") | |
| if len(self.relation_attributes) not in (0, len(self.relation_external_ids)): | |
| raise ValueError( | |
| "relation_attributes must be empty or align with relations" | |
| ) | |
| if len(set(self.node_external_ids)) != len(self.node_external_ids): | |
| raise ValueError("node_external_ids must be unique") | |
| def node_index(self) -> Mapping[str, int]: | |
| return {value: i for i, value in enumerate(self.node_external_ids)} | |
| def _require_rank(value: Tensor, rank: int, name: str) -> None: | |
| if value.ndim != rank: | |
| raise ValueError( | |
| f"{name} must have rank {rank}, got shape {tuple(value.shape)}" | |
| ) | |
| def _path_to_dict(path: GraphPath) -> dict[str, Any]: | |
| return { | |
| "steps": [ | |
| { | |
| "source_id": step.source_id, | |
| "relation_id": step.relation_id, | |
| "target_id": step.target_id, | |
| "source_type": step.source_type, | |
| "target_type": step.target_type, | |
| "edge_attributes": dict(step.edge_attributes), | |
| } | |
| for step in path.steps | |
| ], | |
| "score": path.score, | |
| "verified": path.verified, | |
| "query_compatibility": path.query_compatibility, | |
| } | |