mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
c97733d785
Signed-off-by: phernandez <paul@basicmachines.co> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
237 lines
7.7 KiB
Python
237 lines
7.7 KiB
Python
"""Picoschema parser for Basic Memory.
|
|
|
|
Parses Picoschema YAML dicts (from note frontmatter) into typed dataclass
|
|
representations. Picoschema is a compact schema notation from Google's Dotprompt
|
|
that fits naturally in YAML frontmatter.
|
|
|
|
Syntax reference:
|
|
field: type, description # required field
|
|
field?: type, description # optional field
|
|
field(array): type # array of values
|
|
field?(enum): [val1, val2] # enumeration
|
|
field?(object): # nested object
|
|
sub_field: type
|
|
EntityName as type (capitalized) # entity reference
|
|
"""
|
|
|
|
from dataclasses import dataclass, field
|
|
|
|
|
|
# --- Data Model ---
|
|
|
|
|
|
@dataclass
|
|
class SchemaField:
|
|
"""A single field in a Picoschema definition.
|
|
|
|
Maps to either an observation category or a relation type in Basic Memory notes.
|
|
"""
|
|
|
|
name: str
|
|
type: str # string, integer, number, boolean, any, or EntityName
|
|
required: bool # True unless field name ends with ?
|
|
is_array: bool = False # True if (array) notation
|
|
is_enum: bool = False # True if (enum) notation
|
|
enum_values: list[str] = field(default_factory=list)
|
|
description: str | None = None # Text after comma
|
|
is_entity_ref: bool = False # True if type is capitalized (entity reference)
|
|
children: list["SchemaField"] = field(default_factory=list) # For (object) types
|
|
|
|
|
|
@dataclass
|
|
class SchemaDefinition:
|
|
"""A complete schema definition parsed from a schema note's frontmatter.
|
|
|
|
Combines the parsed fields with metadata about the schema itself.
|
|
"""
|
|
|
|
entity: str # The entity type this schema describes
|
|
version: int # Schema version
|
|
fields: list[SchemaField] # Parsed fields
|
|
validation_mode: str # "warn" | "strict" | "off"
|
|
|
|
|
|
# --- Built-in scalar types ---
|
|
# Types that are NOT entity references. Anything not in this set and starting
|
|
# with an uppercase letter is treated as an entity reference.
|
|
|
|
SCALAR_TYPES = frozenset({"string", "integer", "number", "boolean", "any"})
|
|
|
|
|
|
# --- Field Name Parsing ---
|
|
|
|
|
|
def _parse_field_key(key: str) -> tuple[str, bool, bool, bool, bool]:
|
|
"""Parse a Picoschema field key into its components.
|
|
|
|
Returns (name, required, is_array, is_enum, is_object).
|
|
The key format is: name[?][(array|enum|object)]
|
|
|
|
Examples:
|
|
"name" -> ("name", True, False, False)
|
|
"role?" -> ("role", False, False, False)
|
|
"tags?(array)" -> ("tags", False, True, False)
|
|
"status?(enum)" -> ("status", False, False, True)
|
|
"metadata?(object)" -> ("metadata", False, False, False) + children
|
|
"""
|
|
required = True
|
|
is_array = False
|
|
is_enum = False
|
|
is_object = False
|
|
|
|
# Check for modifier suffix: (array), (enum), (object)
|
|
if key.endswith("(array)"):
|
|
is_array = True
|
|
key = key[: -len("(array)")]
|
|
elif key.endswith("(enum)"):
|
|
is_enum = True
|
|
key = key[: -len("(enum)")]
|
|
elif key.endswith("(object)"):
|
|
is_object = True
|
|
key = key[: -len("(object)")]
|
|
|
|
# Check for optional marker
|
|
if key.endswith("?"):
|
|
required = False
|
|
key = key[:-1]
|
|
|
|
return key, required, is_array, is_enum, is_object
|
|
|
|
|
|
def _parse_type_and_description(value: str) -> tuple[str, str | None]:
|
|
"""Parse a type string that may include a comma-separated description.
|
|
|
|
Examples:
|
|
"string" -> ("string", None)
|
|
"string, full name" -> ("string", "full name")
|
|
"Organization, employer" -> ("Organization", "employer")
|
|
"""
|
|
if "," in value:
|
|
type_str, desc = value.split(",", 1)
|
|
return type_str.strip(), desc.strip()
|
|
return value.strip(), None
|
|
|
|
|
|
def _is_entity_ref_type(type_str: str) -> bool:
|
|
"""Determine if a type string represents an entity reference.
|
|
|
|
Entity references are capitalized type names that are not built-in scalar types.
|
|
"""
|
|
if type_str in SCALAR_TYPES:
|
|
return False
|
|
# Capitalized first letter = entity reference
|
|
return len(type_str) > 0 and type_str[0].isupper()
|
|
|
|
|
|
# --- Main Parser ---
|
|
|
|
|
|
def parse_picoschema(yaml_dict: dict) -> list[SchemaField]:
|
|
"""Parse a Picoschema YAML dict into a list of SchemaField objects.
|
|
|
|
This is the core parser that converts YAML frontmatter schema definitions
|
|
into structured SchemaField dataclasses.
|
|
|
|
Args:
|
|
yaml_dict: The schema dict from YAML frontmatter. Keys are field
|
|
declarations (e.g., "name", "role?", "tags?(array)"), values are
|
|
type declarations (e.g., "string", "string, description").
|
|
|
|
Returns:
|
|
List of SchemaField objects representing the schema.
|
|
"""
|
|
fields: list[SchemaField] = []
|
|
|
|
for key, value in yaml_dict.items():
|
|
name, required, is_array, is_enum, is_object = _parse_field_key(key)
|
|
|
|
# --- Enum fields ---
|
|
# Trigger: value is a list (e.g., [active, inactive])
|
|
# Why: enums declare allowed values directly as a YAML list
|
|
# Outcome: SchemaField with is_enum=True and enum_values populated
|
|
if is_enum:
|
|
enum_values = value if isinstance(value, list) else [str(value)]
|
|
fields.append(
|
|
SchemaField(
|
|
name=name,
|
|
type="enum",
|
|
required=required,
|
|
is_enum=True,
|
|
enum_values=[str(v) for v in enum_values],
|
|
)
|
|
)
|
|
continue
|
|
|
|
# --- Object fields ---
|
|
# Trigger: value is a dict (nested sub-fields)
|
|
# Why: objects contain child fields parsed recursively
|
|
# Outcome: SchemaField with children populated via recursive parse
|
|
if is_object or (isinstance(value, dict) and not is_enum):
|
|
children = parse_picoschema(value) if isinstance(value, dict) else []
|
|
fields.append(
|
|
SchemaField(
|
|
name=name,
|
|
type="object",
|
|
required=required,
|
|
children=children,
|
|
)
|
|
)
|
|
continue
|
|
|
|
# --- Scalar and entity ref fields ---
|
|
type_str, description = _parse_type_and_description(str(value))
|
|
is_entity_ref = _is_entity_ref_type(type_str)
|
|
|
|
fields.append(
|
|
SchemaField(
|
|
name=name,
|
|
type=type_str,
|
|
required=required,
|
|
is_array=is_array,
|
|
description=description,
|
|
is_entity_ref=is_entity_ref,
|
|
)
|
|
)
|
|
|
|
return fields
|
|
|
|
|
|
def parse_schema_note(frontmatter: dict) -> SchemaDefinition:
|
|
"""Parse a full schema note's frontmatter into a SchemaDefinition.
|
|
|
|
A schema note has type: schema and contains:
|
|
- entity: the entity type this schema describes
|
|
- version: schema version number
|
|
- schema: the Picoschema dict
|
|
- settings.validation: validation mode (warn/strict/off)
|
|
|
|
Args:
|
|
frontmatter: The complete YAML frontmatter dict from a schema note.
|
|
|
|
Returns:
|
|
A SchemaDefinition with parsed fields and metadata.
|
|
|
|
Raises:
|
|
ValueError: If required fields (entity, schema) are missing.
|
|
"""
|
|
entity = frontmatter.get("entity")
|
|
if not entity:
|
|
raise ValueError("Schema note missing required 'entity' field in frontmatter")
|
|
|
|
schema_dict = frontmatter.get("schema")
|
|
if not schema_dict or not isinstance(schema_dict, dict):
|
|
raise ValueError("Schema note missing required 'schema' dict in frontmatter")
|
|
|
|
version = frontmatter.get("version", 1)
|
|
settings = frontmatter.get("settings", {})
|
|
validation_mode = settings.get("validation", "warn") if isinstance(settings, dict) else "warn"
|
|
|
|
fields = parse_picoschema(schema_dict)
|
|
|
|
return SchemaDefinition(
|
|
entity=entity,
|
|
version=version,
|
|
fields=fields,
|
|
validation_mode=validation_mode,
|
|
)
|