"""
Public API for card_framework wrapper — symbols, search, DSL docs, and DSL parsing.
This module provides all the public-facing functions that consumers use.
It imports shared state from wrapper_setup via module reference to avoid
circular imports.
Usage:
from gchat.wrapper_api import get_gchat_symbols, parse_dsl
"""
import logging
from typing import Any, Dict, List, Optional
import gchat.wrapper_setup as _setup
from config.enhanced_logging import setup_logger
logger = setup_logger(__name__)
# =============================================================================
# SYMBOL GENERATION
# =============================================================================
def get_gchat_symbols(force_regenerate: bool = False) -> Dict[str, str]:
"""
Get the symbol table for gchat card components.
Returns cached symbols, generating them on first call.
Symbols are auto-generated by SymbolGenerator based on LETTER_SYMBOLS pools.
Args:
force_regenerate: If True, regenerate symbols even if cached
Returns:
Dict mapping component names to symbols (e.g., {"Button": "ᵬ"})
"""
with _setup._symbols_lock:
if _setup._symbols is None or force_regenerate:
_setup._symbols = _generate_gchat_symbols()
return _setup._symbols
def _generate_gchat_symbols() -> Dict[str, str]:
"""Load symbols from the ModuleWrapper (Single Source of Truth).
The ModuleWrapper loads symbols from Qdrant and also includes custom
Google Chat API components (Carousel, NestedWidget, CardFixedFooter, etc.)
that were registered via register_custom_components().
This function simply returns the wrapper's symbol_mapping, which is
the authoritative source for all component symbols.
"""
wrapper = _setup.get_card_framework_wrapper(ensure_text_indices=False)
# The wrapper's symbol_mapping includes both:
# 1. Symbols loaded from Qdrant (card_framework components)
# 2. Custom Chat API symbols (registered via register_custom_components)
symbols = wrapper.symbol_mapping or {}
if symbols:
logger.info(f"🔣 Loaded {len(symbols)} symbols from ModuleWrapper")
return symbols
# Fallback: Load from Qdrant directly if symbol_mapping is empty
if wrapper.client:
try:
from qdrant_client import models
# Scroll through ALL class components to get their stored symbols
offset = None
while True:
results, offset = wrapper.client.scroll(
collection_name=wrapper.collection_name,
scroll_filter=models.Filter(
must=[
models.FieldCondition(
key="type",
match=models.MatchValue(value="class"),
)
]
),
limit=500,
offset=offset,
with_payload=["name", "symbol"],
)
for p in results:
name = p.payload.get("name")
symbol = p.payload.get("symbol")
if name and symbol:
symbols[name] = symbol
if offset is None:
break
if symbols:
logger.info(f"🔣 Loaded {len(symbols)} symbols from Qdrant (fallback)")
return symbols
except Exception as e:
logger.warning(f"Failed to load symbols from Qdrant: {e}")
# Ultimate fallback: Generate symbols if Qdrant unavailable
logger.warning("⚠️ Falling back to symbol generation (Qdrant unavailable)")
from adapters.module_wrapper.symbol_generator import SymbolGenerator
component_names = []
if wrapper.components:
component_names = [
comp.name
for comp in wrapper.components.values()
if comp.component_type == "class"
]
generator = SymbolGenerator(module_prefix=None)
symbols = generator.generate_symbols(component_names)
logger.info(f"✅ Generated {len(symbols)} symbols (fallback)")
return symbols
def get_gchat_symbol_table_text() -> str:
"""
Get a formatted symbol table for LLM instructions.
Returns:
Markdown-formatted symbol table
"""
symbols = get_gchat_symbols()
# Group by first letter
from collections import defaultdict
by_letter = defaultdict(list)
for comp, sym in symbols.items():
by_letter[comp[0].upper()].append((sym, comp))
lines = ["## Google Chat Card Symbols\n"]
for letter in sorted(by_letter.keys()):
items = by_letter[letter]
mappings = [f"{sym}={comp}" for sym, comp in sorted(items, key=lambda x: x[1])]
lines.append(f"**{letter}:** {', '.join(mappings)}")
return "\n".join(lines)
def configure_structure_dsl_symbols():
"""
Configure the structure_dsl module with gchat symbols.
This updates the global symbol tables in structure_dsl.py to use
the gchat-specific symbols, enabling both natural language and
symbol-based card descriptions.
"""
import gchat.structure_dsl as structure_dsl_module
from gchat.structure_dsl import (
ALL_SYMBOLS,
COMPONENT_TO_SYMBOL,
SYMBOL_TO_COMPONENT,
)
symbols = get_gchat_symbols()
# Update structure_dsl globals
SYMBOL_TO_COMPONENT.clear()
COMPONENT_TO_SYMBOL.clear()
for comp, sym in symbols.items():
SYMBOL_TO_COMPONENT[sym] = comp
COMPONENT_TO_SYMBOL[comp] = sym
ALL_SYMBOLS.clear()
ALL_SYMBOLS.update(SYMBOL_TO_COMPONENT.keys())
# Build ASCII confusable aliases (e.g. 'g' → 'ℊ')
from gchat.structure_dsl import _build_ascii_confusables
_build_ascii_confusables()
# Mark as initialized
structure_dsl_module._initialized = True
logger.info(f"🔣 Configured structure_dsl with {len(symbols)} symbols")
# =============================================================================
# CONVENIENCE SEARCH FUNCTIONS
# =============================================================================
def search_components(
query: str,
limit: int = 10,
search_mode: str = "hybrid",
) -> List[Dict]:
"""
Search for card components using natural language or symbols.
Args:
query: Search query (natural language, keywords, or symbols)
limit: Maximum results
search_mode: "vector", "text", "relationship", or "hybrid"
Returns:
List of matching components
"""
wrapper = _setup.get_card_framework_wrapper()
if search_mode == "vector":
return wrapper.search(query, limit=limit)
elif search_mode == "text":
return wrapper.search_by_text(query, limit=limit)
elif search_mode == "relationship":
return wrapper.search_by_relationship_text(query, limit=limit)
else: # hybrid
return wrapper.hybrid_search(query, limit=limit)
def find_component_by_symbol(symbol: str) -> Optional[Dict]:
"""
Find a component by its symbol.
Args:
symbol: Component symbol (e.g., "ᵬ" for Button)
Returns:
Component info dict or None
"""
symbols = get_gchat_symbols()
# Reverse lookup
reverse = {v: k for k, v in symbols.items()}
component_name = reverse.get(symbol)
if not component_name:
return None
# Search for the component
wrapper = _setup.get_card_framework_wrapper()
results = wrapper.search_by_text(
component_name, field="name", limit=1, is_phrase=True
)
return results[0] if results else None
def search_patterns_for_card(
description: str,
limit: int = 5,
require_positive_feedback: bool = True,
) -> Dict[str, Any]:
"""
Search for card patterns matching a description.
This is a high-level convenience method for card tools that:
1. Extracts DSL symbols from the description (if present)
2. Uses DSL-aware search for precise pattern matching
3. Falls back to hybrid V7 search if no DSL
4. Returns structured results ready for card building
Args:
description: Card description (may include DSL notation like "§[δ, Ƀ[ᵬ×2]]")
limit: Maximum patterns to return
require_positive_feedback: Only return patterns with positive feedback
Returns:
Dict with:
- has_dsl: Whether DSL was detected
- dsl: Extracted DSL string (or None)
- patterns: List of matching patterns with component_paths, instance_params
- classes: List of matching class definitions
- query_description: Cleaned description without DSL
"""
wrapper = _setup.get_card_framework_wrapper()
# Extract DSL from description
extracted = wrapper.extract_dsl_from_text(description)
result = {
"has_dsl": extracted.get("has_dsl", False),
"dsl": extracted.get("dsl"),
"query_description": extracted.get("description", description),
"patterns": [],
"classes": [],
}
if extracted.get("has_dsl"):
# Use DSL-aware search for precise matching
logger.info(f"🔤 DSL search: {extracted['dsl']}")
# Search for patterns
pattern_results = wrapper.search_by_dsl(
text=description,
limit=limit,
score_threshold=0.3,
vector_name="inputs",
type_filter="instance_pattern",
)
# Search for classes
class_results = wrapper.search_by_dsl(
text=description,
limit=limit,
score_threshold=0.3,
vector_name="components",
type_filter="class",
)
result["patterns"] = pattern_results
result["classes"] = class_results
else:
# Use hybrid search
feedback_filter = "positive" if require_positive_feedback else None
class_results, content_patterns, form_patterns = wrapper.search_hybrid(
description=description,
component_paths=None,
limit=limit,
token_ratio=1.0,
content_feedback=feedback_filter,
form_feedback=feedback_filter,
include_classes=True,
)
result["classes"] = class_results
result["patterns"] = content_patterns
logger.info(
f"Pattern search: {len(result['classes'])} classes, "
f"{len(result['patterns'])} patterns (DSL={result['has_dsl']})"
)
return result
# =============================================================================
# AUTO-GENERATED DSL DOCUMENTATION
# =============================================================================
def get_dsl_documentation(
include_examples: bool = True, include_hierarchy: bool = True
) -> str:
"""
Auto-generate COMPACT DSL documentation for MCP tool descriptions.
Only includes the ~20 most crucial components to keep tool
descriptions concise. For complete documentation, use skill:// resources.
Args:
include_examples: Whether to include usage examples
include_hierarchy: Whether to include component hierarchy
Returns:
Markdown-formatted documentation string (compact)
"""
symbols = get_gchat_symbols()
# Crucial components for building cards - organized by role
CORE_COMPONENTS = {
"card_structure": ["Card", "CardHeader", "Section"],
"carousel": ["Carousel", "CarouselCard", "NestedWidget"],
"containers": ["ButtonList", "ChipList", "Grid", "Columns"],
"widgets": ["DecoratedText", "TextParagraph", "Image", "Divider"],
"items": ["Button", "Chip", "GridItem", "Column"],
"message": ["AccessoryWidget"], # For message-level buttons
}
lines = ["## Card DSL Quick Reference\n"]
# Core symbols organized by category
lines.append("### Core Symbols")
lines.append(
"**Card:** "
+ ", ".join(
f"{symbols.get(c, '?')}={c}"
for c in CORE_COMPONENTS["card_structure"]
if c in symbols
)
)
lines.append(
"**Carousel:** "
+ ", ".join(
f"{symbols.get(c, '?')}={c}"
for c in CORE_COMPONENTS["carousel"]
if c in symbols
)
)
lines.append(
"**Containers:** "
+ ", ".join(
f"{symbols.get(c, '?')}={c}"
for c in CORE_COMPONENTS["containers"]
if c in symbols
)
)
lines.append(
"**Widgets:** "
+ ", ".join(
f"{symbols.get(c, '?')}={c}"
for c in CORE_COMPONENTS["widgets"]
if c in symbols
)
)
lines.append(
"**Items:** "
+ ", ".join(
f"{symbols.get(c, '?')}={c}"
for c in CORE_COMPONENTS["items"]
if c in symbols
)
)
# Key containment rules (most common patterns)
if include_hierarchy:
lines.append("\n### Containment Rules")
# Get symbols
section_sym = symbols.get("Section", "§")
buttonlist_sym = symbols.get("ButtonList", "Ƀ")
button_sym = symbols.get("Button", "ᵬ")
chiplist_sym = symbols.get("ChipList", "ȼ")
chip_sym = symbols.get("Chip", "ℂ")
grid_sym = symbols.get("Grid", "ℊ")
gitem_sym = symbols.get("GridItem", "ǵ")
columns_sym = symbols.get("Columns", "¢")
column_sym = symbols.get("Column", "ç")
dtext_sym = symbols.get("DecoratedText", "δ")
tpara_sym = symbols.get("TextParagraph", "ʈ")
image_sym = symbols.get("Image", "ǐ")
carousel_sym = symbols.get("Carousel", "◦")
ccard_sym = symbols.get("CarouselCard", "▲")
nested_sym = symbols.get("NestedWidget", "ŋ")
lines.append(
f"- {section_sym} Section → {dtext_sym} {tpara_sym} {image_sym} {buttonlist_sym} {chiplist_sym} {grid_sym} {columns_sym}"
)
lines.append(
f"- {carousel_sym} Carousel → {ccard_sym} CarouselCard → {nested_sym} NestedWidget → {tpara_sym} {buttonlist_sym} {image_sym}"
)
lines.append(
f"- {buttonlist_sym} ButtonList → {button_sym}, {chiplist_sym} ChipList → {chip_sym}"
)
lines.append(
f"- {grid_sym} Grid → {gitem_sym}, {columns_sym} Columns → {column_sym}"
)
if include_examples:
section_sym = symbols.get("Section", "§")
button_sym = symbols.get("Button", "ᵬ")
buttonlist_sym = symbols.get("ButtonList", "Ƀ")
dtext_sym = symbols.get("DecoratedText", "δ")
grid_sym = symbols.get("Grid", "ℊ")
gitem_sym = symbols.get("GridItem", "ǵ")
carousel_sym = symbols.get("Carousel", "◦")
ccard_sym = symbols.get("CarouselCard", "▲")
nested_sym = symbols.get("NestedWidget", "ŋ")
tpara_sym = symbols.get("TextParagraph", "ʈ")
lines.append("\n### Examples")
lines.append(f"- `{section_sym}[{dtext_sym}]` → Section with text")
lines.append(
f"- `{section_sym}[{dtext_sym}, {buttonlist_sym}[{button_sym}×2]]` → Text + 2 buttons"
)
lines.append(
f"- `{section_sym}[{grid_sym}[{gitem_sym}×4]]` → Grid with 4 items"
)
lines.append(f"- `{carousel_sym}[{ccard_sym}×3]` → Carousel with 3 cards")
lines.append("- Syntax: `×N` = multiplier, `[]` = children, `,` = siblings")
# Reference to complete docs
lines.append("\n### More Info")
lines.append(
"Read `skill://gchat-cards/` resources for complete docs (100+ components)."
)
return "\n".join(lines)
def get_dsl_field_description() -> str:
"""
Get a compact field description for the structure_dsl parameter.
Returns a single-line description suitable for Field(description=...).
"""
symbols = get_gchat_symbols()
# Core symbols - most commonly used components
key_mappings = []
key_components = [
"Section",
"DecoratedText",
"ButtonList",
"Button",
"Grid",
"GridItem",
"Carousel",
"CarouselCard",
"NestedWidget",
]
for comp in key_components:
if comp in symbols:
key_mappings.append(f"{symbols[comp]}={comp}")
return (
f"DSL structure using symbols. "
f"Examples: '§[δ, Ƀ[ᵬ×2]]' = Section + text + 2 buttons, "
f"'◦[▲×3]' = Carousel with 3 cards. "
f"Symbols: {', '.join(key_mappings)}. "
f"Read skill://gchat-cards/ for full reference."
)
def get_tool_examples(max_examples: int = 5) -> List[Dict[str, Any]]:
"""
Generate dynamic tool examples using symbols from the DAG.
Creates examples that demonstrate common card patterns with proper
DSL notation and matching card_params.
Args:
max_examples: Maximum number of examples to generate
Returns:
List of example dicts with description, card_description, card_params
"""
symbols = get_gchat_symbols()
# Get symbols for common components (with fallbacks)
section = symbols.get("Section", "§")
dtext = symbols.get("DecoratedText", "δ")
btnlist = symbols.get("ButtonList", "Ƀ")
btn = symbols.get("Button", "ᵬ")
grid = symbols.get("Grid", "ℊ")
gitem = symbols.get("GridItem", "ǵ")
chiplist = symbols.get("ChipList", "ȼ")
chip = symbols.get("Chip", "ℂ")
carousel = symbols.get("Carousel", "◦")
ccard = symbols.get("CarouselCard", "▲")
image = symbols.get("Image", "Ɨ")
divider = symbols.get("Divider", "Đ")
# Define examples using dynamic symbols
all_examples = [
{
"description": "Simple text card",
"card_description": f"{section}[{dtext}]",
"card_params": {"title": "Alert", "text": "System update complete"},
},
{
"description": "Text + 2 action buttons",
"card_description": f"{section}[{dtext}, {btnlist}[{btn}×2]]",
"card_params": {
"title": "Actions Required",
"text": "Choose an action",
"buttons": [
{"text": "Approve", "url": "https://example.com/yes"},
{"text": "Reject", "url": "https://example.com/no"},
],
},
},
{
"description": "Grid with 4 items",
"card_description": f"{section}[{grid}[{gitem}×4]]",
"card_params": {
"title": "Gallery",
"images": [
"https://picsum.photos/200/200?1",
"https://picsum.photos/200/200?2",
"https://picsum.photos/200/200?3",
"https://picsum.photos/200/200?4",
],
},
},
{
"description": "Jinja styled status text",
"card_description": f"{section}[{dtext}]",
"card_params": {
"title": "System Status",
"text": "Server: {{ 'Online' | success_text }} | DB: {{ 'Warning' | warning_text }}",
},
},
{
"description": "Chip list for quick selection",
"card_description": f"{section}[{dtext}, {chiplist}[{chip}×3]]",
"card_params": {
"title": "Select Tags",
"text": "Choose categories",
"chips": [
{"text": "Bug", "url": "#bug"},
{"text": "Feature", "url": "#feature"},
{"text": "Docs", "url": "#docs"},
],
},
},
{
"description": "Carousel with 3 cards",
"card_description": f"{carousel}[{ccard}×3]",
"card_params": {
"title": "Recent Items",
"cards": [
{"title": "Card 1", "text": "First item"},
{"title": "Card 2", "text": "Second item"},
{"title": "Card 3", "text": "Third item"},
],
},
},
{
"description": "Image with text and divider",
"card_description": f"{section}[{image}, {divider}, {dtext}]",
"card_params": {
"title": "Featured",
"image_url": "https://picsum.photos/400/200",
"text": "Featured content description",
},
},
]
return all_examples[:max_examples]
def get_hierarchy_tree_text(
root_components: Optional[List[str]] = None,
max_depth: int = 3,
include_symbols: bool = True,
) -> str:
"""
Generate a deterministic text-based tree representation of component hierarchy.
Delegates to ModuleWrapper.get_hierarchy_tree_text() for the actual implementation.
Args:
root_components: Optional list of root component names to start from.
If None, uses common card containers (Card, Section, etc.)
max_depth: Maximum depth to traverse (default 3)
include_symbols: Whether to include symbols in the output
Returns:
ASCII tree representation of the hierarchy
"""
wrapper = _setup.get_card_framework_wrapper()
# Default root components for gchat cards
if root_components is None:
root_components = ["Card", "Section", "DecoratedText", "Grid", "ButtonList"]
return wrapper.get_hierarchy_tree_text(
root_components=root_components,
max_depth=max_depth,
include_symbols=include_symbols,
)
def get_full_hierarchy_documentation(include_tree: bool = True) -> str:
"""
Generate complete hierarchy documentation with symbols and tree visualization.
Delegates to ModuleWrapper.get_full_module_documentation().
Args:
include_tree: Whether to include the ASCII tree visualization
Returns:
Complete markdown documentation string
"""
wrapper = _setup.get_card_framework_wrapper()
return wrapper.get_full_module_documentation(
include_tree=include_tree,
include_symbols=True,
include_examples=True,
)
def get_component_relationships_for_dsl() -> Dict[str, List[str]]:
"""
Get component relationships formatted for DSL validation.
Returns a dict mapping parent components to their valid children,
using component names (not symbols).
Dynamically loads relationships from Qdrant and expands base class
references (e.g., Widget) to actual widget subclasses via Python introspection.
"""
wrapper = _setup.get_card_framework_wrapper()
relationships = {}
# Step 1: Get Widget subclasses via Python introspection (most accurate)
widget_subclasses = _get_widget_subclasses()
logger.info(
f"📦 Found {len(widget_subclasses)} Widget subclasses via introspection"
)
if not wrapper.client:
logger.warning("No Qdrant client - using introspection-based relationships")
return _build_relationships_from_introspection(widget_subclasses)
try:
from qdrant_client import models
# Step 2: Get all classes with their relationships from Qdrant
results, _ = wrapper.client.scroll(
collection_name=wrapper.collection_name,
scroll_filter=models.Filter(
must=[
models.FieldCondition(
key="type",
match=models.MatchValue(value="class"),
)
]
),
limit=300,
with_payload=["name", "relationships"],
)
# Step 3: Build relationships, expanding Widget -> all widget subclasses
for point in results:
payload = point.payload
name = payload.get("name")
rels = payload.get("relationships", {})
children = rels.get("child_classes", [])
if name and children:
# Expand "Widget" to all actual widget subclasses
expanded_children = []
for child in children:
if child == "Widget":
# Replace Widget with all subclasses from introspection
expanded_children.extend(widget_subclasses)
else:
expanded_children.append(child)
relationships[name] = expanded_children
logger.info(
f"📋 Loaded {len(relationships)} component relationships for DSL (Qdrant + introspection)"
)
# Log Section's children for debugging
section_children = relationships.get("Section", [])
logger.info(
f"📋 Section can contain: {len(section_children)} widget types: {section_children}"
)
return relationships
except Exception as e:
logger.warning(f"Failed to load relationships from Qdrant: {e}")
return _build_relationships_from_introspection(widget_subclasses)
def _get_widget_subclasses() -> List[str]:
"""Get all Widget subclasses via Python introspection."""
try:
from card_framework.v2.widget import Widget
def get_all_subclasses(cls):
all_subs = []
for sub in cls.__subclasses__():
all_subs.append(sub)
all_subs.extend(get_all_subclasses(sub))
return all_subs
subclasses = get_all_subclasses(Widget)
return sorted([c.__name__ for c in subclasses])
except ImportError:
logger.warning("Could not import Widget class for introspection")
return [
"Action",
"Button",
"ButtonList",
"Chip",
"ChipList",
"Columns",
"DateTimePicker",
"DecoratedText",
"Divider",
"Grid",
"Image",
"SelectionInput",
"SwitchControl",
"TextInput",
"TextParagraph",
"UpdatedWidget",
]
def _build_relationships_from_introspection(
widget_subclasses: List[str],
) -> Dict[str, List[str]]:
"""Build relationships using Python introspection when Qdrant is unavailable.
This includes manually-added Google Chat API components that may not exist
in the card_framework Python package but are supported by the Chat API.
Custom components are defined in CUSTOM_CHAT_API_RELATIONSHIPS.
"""
# Base relationships from card_framework introspection
base_relationships = {
"Card": ["CardHeader", "Section", "CardFixedFooter", "Carousel"],
"Section": ["CollapseControl"] + widget_subclasses,
"Columns": ["Column"],
"Column": widget_subclasses, # Column can contain any widget
"ButtonList": ["Button"],
"ChipList": ["Chip"],
"Grid": ["GridItem"],
"DecoratedText": ["Icon", "Button", "OnClick", "SwitchControl"],
"Button": ["Icon", "OnClick", "Color"],
"Chip": ["Icon", "OnClick"],
"OnClick": ["OpenLink", "Action", "OverflowMenu"],
"OverflowMenu": ["OverflowMenuItem"],
"GridItem": ["ImageComponent"],
"TextInput": ["Suggestions", "Validation"],
}
# Merge with custom Chat API relationships (avoids duplication)
base_relationships.update(_setup.CUSTOM_CHAT_API_RELATIONSHIPS)
return base_relationships
# =============================================================================
# DSL PARSING HELPERS (using refactored module_wrapper.dsl_parser)
# =============================================================================
def get_dsl_parser():
"""
Get a configured DSL parser for gchat card components.
Uses the new refactored DSLParser from adapters.module_wrapper.dsl_parser
which provides:
- Robust tokenization and parsing
- Qdrant query generation
- DSL extraction from descriptions
- Validation against component hierarchy
Returns:
DSLParser instance configured with gchat symbols and relationships
"""
from adapters.module_wrapper.dsl_parser import DSLParser
symbols = get_gchat_symbols()
relationships = get_component_relationships_for_dsl()
return DSLParser(
symbol_mapping=symbols,
reverse_mapping={v: k for k, v in symbols.items()},
relationships=relationships,
)
def parse_dsl(dsl_string: str):
"""
Parse a DSL string using the refactored DSL parser.
Args:
dsl_string: DSL notation like "§[đ×3, Ƀ[ᵬ×2]]"
Returns:
DSLParseResult with:
- is_valid: Whether structure is valid
- component_counts: Dict of component name → count
- component_paths: Flat list of component names
- root_nodes: Parsed tree structure
- issues: List of validation issues
Example:
result = parse_dsl("§[đ×3, Ƀ[ᵬ×2]]")
if result.is_valid:
print(result.component_counts)
# {'Section': 1, 'DecoratedText': 3, 'ButtonList': 1, 'Button': 2}
"""
parser = get_dsl_parser()
return parser.parse(dsl_string)
def extract_dsl_from_description(description: str) -> Optional[str]:
"""
Extract DSL notation from a description string.
Uses the refactored DSL parser for robust extraction.
Args:
description: Text that may contain DSL notation
e.g., "§[đ×3, Ƀ[ᵬ×2]] Server Status Dashboard"
Returns:
DSL string if found, None otherwise
e.g., "§[δ×3, Ƀ[ᵬ×2]]"
Example:
dsl = extract_dsl_from_description("§[đ×3] Server Status")
# Returns: "§[đ×3]"
"""
if not description:
return None
parser = get_dsl_parser()
return parser.extract_dsl_from_text(description)
def dsl_to_qdrant_queries(
dsl_string: str, collection_name: Optional[str] = None
) -> List[Dict]:
"""
Generate Qdrant queries from a DSL string.
Uses the refactored DSL parser to generate optimized queries
for the relationships, components, and inputs vectors.
Args:
dsl_string: DSL notation
collection_name: Qdrant collection name (uses settings.card_collection if None)
Returns:
List of query dicts with:
- vector_name: Which vector to search ('relationships', 'components', 'inputs')
- query_text: The query text
- filters: Any filters to apply
Example:
queries = dsl_to_qdrant_queries("§[đ×3, Ƀ[ᵬ×2]]")
for q in queries:
print(f"{q['vector_name']}: {q['query_text']}")
"""
from config.settings import settings
parser = get_dsl_parser()
result = parser.parse(dsl_string)
if not result.is_valid:
logger.warning(f"Invalid DSL structure: {result.issues}")
return []
collection = collection_name or settings.card_collection
queries = parser.to_qdrant_queries(result, collection)
return [q.to_dict() for q in queries]
def validate_dsl(dsl_string: str) -> tuple:
"""
Validate a DSL structure against the component hierarchy.
Args:
dsl_string: DSL notation to validate
Returns:
Tuple of (is_valid, issues_list)
Example:
is_valid, issues = validate_dsl("§[đ, ᵬ]")
if not is_valid:
print(f"Issues: {issues}")
"""
parser = get_dsl_parser()
result = parser.parse(dsl_string)
return result.is_valid, result.issues
def expand_dsl(dsl_string: str) -> str:
"""
Expand DSL symbols to full component names.
Args:
dsl_string: Compact DSL like "§[đ, ᵬ×2]"
Returns:
Expanded notation like "Section[DecoratedText, Button×2]"
"""
parser = get_dsl_parser()
return parser.expand_dsl(dsl_string)
def compact_dsl(component_notation: str) -> str:
"""
Compact component names to DSL symbols.
Args:
component_notation: Full names like "Section[DecoratedText, Button×2]"
Returns:
Compact DSL like "§[đ, ᵬ×2]"
"""
parser = get_dsl_parser()
return parser.compact_to_dsl(component_notation)
# =============================================================================
# CONTENT DSL HELPERS
# =============================================================================
def parse_content_dsl(content_text: str):
"""
Parse Content DSL text into structured blocks.
Content DSL allows expressing component content with styling modifiers:
δ 'Status: Online' success bold
ᵬ Click Here https://example.com
Continue on next line
Features:
- Symbol prefix indicates component type (δ, ᵬ, §, etc.)
- Quoted or unquoted text content
- Style modifiers (yellow, bold, success, error, italic, etc.)
- URL detection for button/link actions
- Continuation lines (indented) for multi-line content
Args:
content_text: Multi-line Content DSL text
Returns:
ContentDSLResult with:
- is_valid: Whether parsing succeeded
- blocks: List of ContentBlock objects
- issues: Any parsing issues
Example:
result = parse_content_dsl('''
§ Dashboard bold
δ 'Server Status' success
ᵬ Refresh https://api.example.com/refresh
''')
for block in result.blocks:
print(f"{block.primary.component_name}: {block.to_jinja()}")
"""
parser = get_dsl_parser()
return parser.parse_content_dsl(content_text)
def content_to_jinja(content_text: str) -> List[str]:
"""
Convert Content DSL directly to Jinja expressions.
A convenience function that parses Content DSL and returns
the Jinja template expressions for each block.
Args:
content_text: Content DSL text
Returns:
List of Jinja expressions like ["{{ 'text' | success_text | bold }}"]
Example:
expressions = content_to_jinja("δ 'Hello' success bold")
# Returns: ["{{ 'Hello' | success_text | bold }}"]
"""
parser = get_dsl_parser()
result = parser.parse_content_dsl(content_text)
return result.to_jinja_list()
def content_to_params(content_text: str) -> List[Dict]:
"""
Convert Content DSL to component parameter dictionaries.
Useful for directly building card components without going
through the Jinja template system.
Args:
content_text: Content DSL text
Returns:
List of dicts with component parameters:
- component: Component name (e.g., "DecoratedText")
- text: The text content
- jinja_text: Jinja-formatted text with filters
- url: URL if detected (for buttons)
- styles: List of style modifier names
Example:
params = content_to_params('''
δ 'Server Online' success
ᵬ Refresh https://api.example.com
''')
# Returns:
# [
# {"component": "DecoratedText", "text": "Server Online", "styles": ["success"], ...},
# {"component": "Button", "text": "Refresh", "url": "https://api.example.com", ...}
# ]
"""
parser = get_dsl_parser()
result = parser.parse_content_dsl(content_text)
return parser.content_to_component_params(result)
def get_available_style_modifiers() -> Dict[str, str]:
"""
Get available style modifiers for Content DSL.
Returns:
Dict mapping modifier name to Jinja filter name
Example:
modifiers = get_available_style_modifiers()
# {'bold': 'bold', 'success': 'success_text', 'yellow': 'color', ...}
"""
from adapters.module_wrapper.dsl_parser import STYLE_MODIFIERS
return {name: filter_info[0] for name, filter_info in STYLE_MODIFIERS.items()}