-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathschema.py
More file actions
159 lines (130 loc) · 5.13 KB
/
Copy pathschema.py
File metadata and controls
159 lines (130 loc) · 5.13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
"""Schema: entity types + relation types + LLM hints + recommended extractors.
A Schema is what the user (or an LLM) iterates on during a Session. It is
the **only** thing that decides what kind of graph comes out of a corpus.
Phase 0 = data class + preset loader skeleton. Real preset library lands
in Phase 1 (#5).
"""
from __future__ import annotations
import json
from dataclasses import asdict, dataclass, field
from pathlib import Path
from typing import Any
@dataclass
class EntityType:
name: str
description: str = ""
attrs: list[str] = field(default_factory=list)
@dataclass
class RelationType:
name: str
description: str = ""
domain: list[str] = field(default_factory=list) # source entity types
range: list[str] = field(default_factory=list) # target entity types
@dataclass
class Schema:
"""Live schema for a Session. Mutable through Session.refine()."""
name: str = "ad_hoc"
description: str = ""
entities: list[EntityType] = field(default_factory=list)
relations: list[RelationType] = field(default_factory=list)
extractor_hint: str | None = None # which extractor to prefer
prompt_hints: dict[str, str] = field(default_factory=dict)
version: int = 1
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "Schema":
ents = [EntityType(**e) for e in data.get("entities", [])]
rels = [RelationType(**r) for r in data.get("relations", [])]
return cls(
name=data.get("name", "ad_hoc"),
description=data.get("description", ""),
entities=ents,
relations=rels,
extractor_hint=data.get("extractor_hint"),
prompt_hints=data.get("prompt_hints", {}),
version=data.get("version", 1),
)
def add_entity(self, name: str, *, description: str = "", attrs: list[str] | None = None) -> None:
if any(e.name == name for e in self.entities):
return
self.entities.append(EntityType(name=name, description=description, attrs=attrs or []))
self.version += 1
def remove_entity(self, name: str) -> bool:
before = len(self.entities)
self.entities = [e for e in self.entities if e.name != name]
if len(self.entities) != before:
self.version += 1
return True
return False
def add_relation(
self,
name: str,
*,
description: str = "",
domain: list[str] | None = None,
range: list[str] | None = None,
) -> None:
if any(r.name == name for r in self.relations):
return
self.relations.append(
RelationType(name=name, description=description, domain=domain or [], range=range or [])
)
self.version += 1
def remove_relation(self, name: str) -> bool:
before = len(self.relations)
self.relations = [r for r in self.relations if r.name != name]
if len(self.relations) != before:
self.version += 1
return True
return False
def is_empty(self) -> bool:
return not self.entities and not self.relations
# ---------------------------------------------------------------------------
# Preset library
# ---------------------------------------------------------------------------
PRESETS_DIR = Path(__file__).parent / "schemas"
def list_presets() -> list[dict[str, str]]:
"""Return built-in presets with name + one-line description.
Sorted alphabetically; lazy-loaded yaml so missing pyyaml just yields
descriptions=''. Used by CLI ``graphanything presets`` and Skill
``graphanything_list_presets``.
"""
if not PRESETS_DIR.exists():
return []
out: list[dict[str, str]] = []
paths = sorted(PRESETS_DIR.glob("*.yaml")) + sorted(PRESETS_DIR.glob("*.json"))
for p in paths:
desc = ""
try:
if p.suffix == ".yaml":
import yaml # type: ignore
data = yaml.safe_load(p.read_text()) or {}
else:
data = json.loads(p.read_text())
desc = (data or {}).get("description", "") or ""
except Exception: # pragma: no cover — listing must never crash
desc = ""
out.append({"name": p.stem, "description": desc})
return out
def load_preset(name: str) -> Schema:
"""Load a preset by name. Tries .yaml first, then .json."""
yaml_path = PRESETS_DIR / f"{name}.yaml"
json_path = PRESETS_DIR / f"{name}.json"
if yaml_path.exists():
try:
import yaml # type: ignore
except ImportError as e:
raise RuntimeError(
f"Preset '{name}' is YAML; install pyyaml or use a .json preset."
) from e
data = yaml.safe_load(yaml_path.read_text())
return Schema.from_dict(data)
if json_path.exists():
data = json.loads(json_path.read_text())
return Schema.from_dict(data)
available = [p["name"] for p in list_presets()]
raise FileNotFoundError(
f"No preset named '{name}' under {PRESETS_DIR}. "
f"Available: {available}"
)