"""
Card Structure DSL (Domain Specific Language)
This module provides a compact symbol-based language for describing card structures.
Symbols are embedded alongside component names during ingestion, creating strong
associations that make structure matching more precise and token-efficient.
Usage:
"§[đ, Ƀ[ᵬ×2], ℊ[ǵ×4]]"
Means: Section containing:
- DecoratedText
- ButtonList with 2 Buttons
- Grid with 4 GridItems
The symbol table is provided to LLMs via MCP tool instructions.
During execution, symbols are expanded and validated against the relationship hierarchy.
Multi-Module Support:
Symbols can be prefixed with a module identifier for disambiguation:
"g:§[g:đ, g:ᵬ×2]" # gchat-specific Section with DecoratedText and Buttons
Symbol Generation:
Symbols can be auto-generated via ModuleWrapper.generate_component_symbols()
or use the hardcoded defaults below for backward compatibility.
"""
import logging
import re
from collections import Counter
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Tuple
logger = logging.getLogger(__name__)
# =============================================================================
# SYMBOL TABLE (Initialized from SymbolGenerator - SSoT)
# =============================================================================
# NO HARDCODED DEFAULTS - symbols must be generated by SymbolGenerator
# based on LETTER_SYMBOLS pools in adapters/symbol_generator.py
#
# These tables are populated by calling configure_from_generator() or
# get_card_framework_wrapper() which triggers auto-configuration.
# Active symbol table - populated at runtime from SymbolGenerator
SYMBOL_TO_COMPONENT: Dict[str, str] = {}
# Reverse mapping - populated at runtime
COMPONENT_TO_SYMBOL: Dict[str, str] = {}
# All symbols as a set for quick lookup - populated at runtime
ALL_SYMBOLS: set = set()
# Flag to track initialization
_initialized = False
# =============================================================================
# DYNAMIC SYMBOL TABLE MANAGEMENT
# =============================================================================
def configure_symbols_from_generator(generator: "SymbolGenerator") -> None:
"""
Configure the symbol table from a SymbolGenerator instance.
This allows runtime configuration of symbols, including multi-module
support via prefixed symbols.
Args:
generator: Configured SymbolGenerator instance
"""
global SYMBOL_TO_COMPONENT, COMPONENT_TO_SYMBOL, ALL_SYMBOLS, _initialized
new_symbols = generator.get_reverse_mapping()
SYMBOL_TO_COMPONENT.clear()
SYMBOL_TO_COMPONENT.update(new_symbols)
COMPONENT_TO_SYMBOL.clear()
COMPONENT_TO_SYMBOL.update({v: k for k, v in SYMBOL_TO_COMPONENT.items()})
ALL_SYMBOLS.clear()
ALL_SYMBOLS.update(SYMBOL_TO_COMPONENT.keys())
_initialized = True
logger.info(f"🔣 Configured {len(SYMBOL_TO_COMPONENT)} symbols from generator")
def configure_symbols_from_wrapper(
wrapper: "ModuleWrapper",
module_prefix: Optional[str] = None,
) -> Dict[str, str]:
"""
Configure symbols from a ModuleWrapper instance.
Convenience function that creates a generator from wrapper components
and configures the symbol table.
Args:
wrapper: Initialized ModuleWrapper instance
module_prefix: Optional module prefix for multi-module support
Returns:
Dict mapping component names to symbols
"""
from adapters.symbol_generator import (
SymbolGenerator,
extract_component_names_from_wrapper,
)
component_names = extract_component_names_from_wrapper(wrapper)
generator = SymbolGenerator(module_prefix=module_prefix)
symbols = generator.generate_symbols(component_names)
configure_symbols_from_generator(generator)
return symbols
def clear_symbols() -> None:
"""Clear all symbols (requires re-initialization via configure_symbols_*)."""
global SYMBOL_TO_COMPONENT, COMPONENT_TO_SYMBOL, ALL_SYMBOLS, _initialized
SYMBOL_TO_COMPONENT.clear()
COMPONENT_TO_SYMBOL.clear()
ALL_SYMBOLS.clear()
_initialized = False
logger.info("🔄 Cleared symbol tables (re-initialization required)")
def ensure_initialized() -> bool:
"""
Ensure symbol tables are initialized from the card framework wrapper.
Auto-initializes from get_card_framework_wrapper() if not yet configured.
This allows lazy initialization - symbols are loaded on first use.
Returns:
True if initialized, False if initialization failed
"""
global _initialized
if _initialized and SYMBOL_TO_COMPONENT:
return True
try:
# Import here to avoid circular imports
from gchat.card_framework_wrapper import (
configure_structure_dsl_symbols,
get_card_framework_wrapper,
)
# This will configure the symbol tables
configure_structure_dsl_symbols()
_initialized = True
return True
except Exception as e:
logger.warning(f"⚠️ Failed to auto-initialize symbols: {e}")
return False
# =============================================================================
# EMBEDDING TEXT BUILDERS
# =============================================================================
def build_symbol_embedding_text(component_name: str) -> str:
"""
Build text for embedding that creates strong symbol-component association.
The symbol appears multiple times in different contexts to strengthen
the bidirectional embedding association.
Args:
component_name: Name of the component (e.g., "Grid")
Returns:
Text suitable for embedding, e.g., "ℊ Grid ℊ | Grid widget ℊ"
"""
ensure_initialized()
symbol = COMPONENT_TO_SYMBOL.get(component_name)
if not symbol:
return f"{component_name} widget component"
# Multiple mentions create stronger association
return f"{symbol} {component_name} {symbol} | {component_name} widget {symbol}"
def build_component_identity_with_symbol(
component_name: str,
component_type: str,
full_path: str,
docstring: Optional[str] = None,
) -> str:
"""
Build full component identity text including symbol for embedding.
This is used during ingestion to create the 'components' vector.
Args:
component_name: Name of the component
component_type: Type (class, function, etc.)
full_path: Full module path
docstring: Optional docstring
Returns:
Identity text with symbol associations
"""
ensure_initialized()
symbol = COMPONENT_TO_SYMBOL.get(component_name, "")
parts = [
f"Name: {component_name}",
f"Symbol: {symbol}" if symbol else "",
f"Type: {component_type}",
f"Path: {full_path}",
]
if symbol:
# Add symbol associations
parts.append(f"Shortcut: {symbol}={component_name}")
if docstring:
parts.append(f"Documentation: {docstring[:500]}")
return "\n".join(p for p in parts if p)
# =============================================================================
# STRUCTURE PARSING
# =============================================================================
@dataclass
class ParsedNode:
"""A node in the parsed structure tree."""
symbol: str
component: str
multiplier: int = 1
children: List["ParsedNode"] = field(default_factory=list)
def to_compact(self) -> str:
"""Convert back to compact notation."""
result = self.symbol
if self.multiplier > 1:
result = f"{self.symbol}×{self.multiplier}"
if self.children:
children_str = ", ".join(c.to_compact() for c in self.children)
result = f"{result}[{children_str}]"
return result
def to_expanded(self) -> str:
"""Convert to expanded component names."""
result = self.component
if self.multiplier > 1:
result = f"{self.component}×{self.multiplier}"
if self.children:
children_str = ", ".join(c.to_expanded() for c in self.children)
result = f"{result}[{children_str}]"
return result
def parse_structure(structure_str: str) -> List[ParsedNode]:
"""
Parse a structure string into a tree of ParsedNodes.
Syntax:
§[đ, ᵬ×2] → Section with DecoratedText and 2 Buttons
ℊ[ǵ×4] → Grid with 4 GridItems
¶[ǀ[đ], ǀ[ɨ]] → 2 Columns with different content
Args:
structure_str: Compact structure notation
Returns:
List of root ParsedNodes
"""
# Tokenize
tokens = _tokenize_structure(structure_str)
# Parse into tree
nodes, _ = _parse_tokens(tokens, 0)
return nodes
def _tokenize_structure(s: str) -> List[str]:
"""Tokenize structure string into symbols, brackets, and multipliers."""
# Ensure symbols are initialized before tokenizing
ensure_initialized()
tokens = []
i = 0
while i < len(s):
char = s[i]
# Skip whitespace and commas
if char in " ,\n\t":
i += 1
continue
# Brackets
if char in "[]":
tokens.append(char)
i += 1
continue
# Multiplier (×N or *N or xN)
if char in "×*x" and i + 1 < len(s) and s[i + 1].isdigit():
# Read the number
j = i + 1
while j < len(s) and s[j].isdigit():
j += 1
tokens.append(f"×{s[i + 1 : j]}")
i = j
continue
# Symbol (single character from our table)
if char in ALL_SYMBOLS:
tokens.append(char)
i += 1
continue
# Component name (for mixed mode: "Grid" or "ℊ")
if char.isalpha():
j = i
while j < len(s) and (s[j].isalnum() or s[j] == "_"):
j += 1
word = s[i:j]
# Convert to symbol if it's a known component
if word in COMPONENT_TO_SYMBOL:
tokens.append(COMPONENT_TO_SYMBOL[word])
else:
tokens.append(word) # Keep as-is for unknown
i = j
continue
# Skip unknown characters
i += 1
return tokens
def _parse_tokens(tokens: List[str], pos: int) -> Tuple[List[ParsedNode], int]:
"""Parse tokens into ParsedNode tree recursively."""
nodes = []
while pos < len(tokens):
token = tokens[pos]
# End of current level
if token == "]":
return nodes, pos + 1
# Start of children
if token == "[":
if nodes:
# Children belong to previous node
children, pos = _parse_tokens(tokens, pos + 1)
nodes[-1].children = children
else:
pos += 1
continue
# Multiplier
if token.startswith("×"):
if nodes:
nodes[-1].multiplier = int(token[1:])
pos += 1
continue
# Symbol or component name
if token in SYMBOL_TO_COMPONENT:
nodes.append(
ParsedNode(
symbol=token,
component=SYMBOL_TO_COMPONENT[token],
)
)
elif token in COMPONENT_TO_SYMBOL:
# It's a full component name
symbol = COMPONENT_TO_SYMBOL[token]
nodes.append(
ParsedNode(
symbol=symbol,
component=token,
)
)
else:
# Unknown - treat as literal
nodes.append(
ParsedNode(
symbol=token,
component=token,
)
)
pos += 1
return nodes, pos
# =============================================================================
# STRUCTURE VALIDATION
# =============================================================================
def validate_structure(
nodes: List[ParsedNode],
relationships: Dict[str, List[str]],
) -> Tuple[bool, List[str]]:
"""
Validate a parsed structure against the relationship hierarchy.
Args:
nodes: Parsed structure nodes
relationships: Dict mapping parent components to valid children
Returns:
Tuple of (is_valid, list of error messages)
"""
errors = []
def validate_node(node: ParsedNode, parent: Optional[str] = None) -> None:
# Check if this component can be a child of the parent
if parent:
valid_children = relationships.get(parent, [])
if node.component not in valid_children and valid_children:
errors.append(
f"Invalid: {node.component} cannot be child of {parent}. "
f"Valid children: {valid_children}"
)
# Validate children recursively
for child in node.children:
validate_node(child, node.component)
for node in nodes:
validate_node(node)
return len(errors) == 0, errors
# =============================================================================
# SKELETON BUILDING
# =============================================================================
@dataclass
class SkeletonSlot:
"""A slot in the card skeleton waiting for input."""
path: str # e.g., "Section[0].DecoratedText[0].text"
component: str # e.g., "DecoratedText"
field_name: str # e.g., "text"
field_type: str # e.g., "str", "Optional[str]", "List[Button]"
required: bool = False
value: Any = None # Filled during input matching
def build_skeleton(
nodes: List[ParsedNode],
component_fields: Dict[str, List[Dict[str, Any]]],
) -> List[SkeletonSlot]:
"""
Build a skeleton with empty slots from parsed structure.
Args:
nodes: Parsed structure nodes
component_fields: Dict mapping component names to their fields
[{name: "text", type: "str", required: False}, ...]
Returns:
List of SkeletonSlots to be filled with inputs
"""
slots = []
def process_node(node: ParsedNode, path_prefix: str = "") -> None:
component = node.component
for i in range(node.multiplier):
# Build path for this instance
if node.multiplier > 1:
instance_path = f"{path_prefix}{component}[{i}]"
else:
instance_path = f"{path_prefix}{component}"
# Get fields for this component
fields = component_fields.get(component, [])
for field_info in fields:
field_name = field_info.get("name", "")
field_type = field_info.get("type", "Any")
required = field_info.get("required", False)
# Skip fields that are other components (handled by children)
if field_info.get("is_component", False):
continue
slots.append(
SkeletonSlot(
path=f"{instance_path}.{field_name}",
component=component,
field_name=field_name,
field_type=field_type,
required=required,
)
)
# Process children
for child in node.children:
process_node(child, f"{instance_path}.")
for node in nodes:
process_node(node)
return slots
# =============================================================================
# MCP TOOL INSTRUCTIONS
# =============================================================================
def get_structure_instructions() -> str:
"""
Get instructions for LLMs on how to use the structure DSL.
This is included in MCP tool descriptions.
Symbols are dynamically generated from COMPONENT_TO_SYMBOL (SSoT).
"""
ensure_initialized()
# Group components by category, then look up their symbols from SSoT
categories = {
"Layout": [
"Card",
"Section",
"Header",
"CardFixedFooter",
"Columns",
"Column",
"Divider",
],
"Text": ["DecoratedText", "TextParagraph", "TextInput"],
"Buttons": ["Button", "ButtonList", "ChipList", "Chip"],
"Media": ["Grid", "GridItem", "Image", "Icon"],
"Inputs": [
"SelectionInput",
"SelectionItem",
"DateTimePicker",
"SwitchControl",
],
"Actions": ["OnClick", "OpenLink", "Action"],
"Menus": ["OverflowMenu", "OverflowMenuItem"],
}
# Build symbol mappings from SSoT
category_lines = []
for cat_name, components in categories.items():
mappings = []
for comp in components:
symbol = COMPONENT_TO_SYMBOL.get(comp)
if symbol:
mappings.append(f"{symbol}={comp}")
if mappings:
category_lines.append(f"**{cat_name}:** {', '.join(mappings)}")
# Get example symbols from SSoT - no fallbacks, symbols come ONLY from ModuleWrapper
section_sym = COMPONENT_TO_SYMBOL.get("Section", "Section")
dt_sym = COMPONENT_TO_SYMBOL.get("DecoratedText", "DecoratedText")
btn_sym = COMPONENT_TO_SYMBOL.get("Button", "Button")
btnlist_sym = COMPONENT_TO_SYMBOL.get("ButtonList", "ButtonList")
grid_sym = COMPONENT_TO_SYMBOL.get("Grid", "Grid")
griditem_sym = COMPONENT_TO_SYMBOL.get("GridItem", "GridItem")
cols_sym = COMPONENT_TO_SYMBOL.get("Columns", "Columns")
col_sym = COMPONENT_TO_SYMBOL.get("Column", "Column")
img_sym = COMPONENT_TO_SYMBOL.get("Image", "Image")
header_sym = COMPONENT_TO_SYMBOL.get("Header", "Header")
textinput_sym = COMPONENT_TO_SYMBOL.get("TextInput", "TextInput")
datepicker_sym = COMPONENT_TO_SYMBOL.get("DateTimePicker", "DateTimePicker")
categories_text = "\n".join(category_lines)
return f"""
## Card Structure Symbols
Use these shortcuts to describe card structure compactly:
{categories_text}
## Syntax
- `{section_sym}[{dt_sym}, {btn_sym}]` → Section with DecoratedText and Button
- `{btn_sym}×2` → 2 Buttons
- `{grid_sym}[{griditem_sym}×4]` → Grid with 4 GridItems
- `{cols_sym}[{col_sym}[{dt_sym}], {col_sym}[{img_sym}]]` → 2 Columns (left: text, right: image)
## Examples
Simple card: `{section_sym}[{dt_sym}, {btnlist_sym}[{btn_sym}×2]]`
Product grid: `{section_sym}[{header_sym}, {grid_sym}[{griditem_sym}×6], {btnlist_sym}[{btn_sym}]]`
Form: `{section_sym}[{textinput_sym}×3, {datepicker_sym}, {btnlist_sym}[{btn_sym}]]`
"""
# =============================================================================
# EXPANSION (Symbol → Full Structure)
# =============================================================================
def expand_to_full_notation(structure_str: str) -> str:
"""
Expand a compact symbol structure to full component names.
Args:
structure_str: Compact notation, e.g., "§[đ, ᵬ×2]"
Returns:
Expanded notation, e.g., "Section[DecoratedText, Button×2]"
"""
nodes = parse_structure(structure_str)
return ", ".join(n.to_expanded() for n in nodes)
def compact_to_symbol_notation(structure_str: str) -> str:
"""
Convert full component names to compact symbol notation.
Args:
structure_str: Full notation, e.g., "Section[DecoratedText, Button×2]"
Returns:
Compact notation, e.g., "§[đ, ᵬ×2]"
"""
nodes = parse_structure(structure_str)
return ", ".join(n.to_compact() for n in nodes)