|
|
""" |
|
|
LangGraph State Definitions |
|
|
Design System Extractor v2 |
|
|
|
|
|
Defines the state schema and type hints for LangGraph workflow. |
|
|
""" |
|
|
|
|
|
from typing import TypedDict, Annotated, Sequence, Optional |
|
|
from datetime import datetime |
|
|
from langgraph.graph.message import add_messages |
|
|
|
|
|
from core.token_schema import ( |
|
|
DiscoveredPage, |
|
|
ExtractedTokens, |
|
|
NormalizedTokens, |
|
|
UpgradeRecommendations, |
|
|
FinalTokens, |
|
|
Viewport, |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def merge_lists(left: list, right: list) -> list: |
|
|
"""Merge two lists, avoiding duplicates.""" |
|
|
seen = set() |
|
|
result = [] |
|
|
for item in left + right: |
|
|
key = str(item) if not hasattr(item, 'url') else item.url |
|
|
if key not in seen: |
|
|
seen.add(key) |
|
|
result.append(item) |
|
|
return result |
|
|
|
|
|
|
|
|
def replace_value(left, right): |
|
|
"""Replace left with right (simple override).""" |
|
|
return right if right is not None else left |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AgentState(TypedDict): |
|
|
""" |
|
|
Main state for the LangGraph workflow. |
|
|
|
|
|
This state is passed between all agents and accumulates data |
|
|
as the workflow progresses through stages. |
|
|
""" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
base_url: str |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
discovered_pages: Annotated[list[DiscoveredPage], merge_lists] |
|
|
pages_to_crawl: list[str] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
desktop_extraction: Optional[ExtractedTokens] |
|
|
desktop_crawl_progress: float |
|
|
|
|
|
|
|
|
mobile_extraction: Optional[ExtractedTokens] |
|
|
mobile_crawl_progress: float |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
desktop_normalized: Optional[NormalizedTokens] |
|
|
mobile_normalized: Optional[NormalizedTokens] |
|
|
|
|
|
|
|
|
accepted_colors: list[str] |
|
|
rejected_colors: list[str] |
|
|
accepted_typography: list[str] |
|
|
rejected_typography: list[str] |
|
|
accepted_spacing: list[str] |
|
|
rejected_spacing: list[str] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
upgrade_recommendations: Optional[UpgradeRecommendations] |
|
|
|
|
|
|
|
|
selected_type_scale: Optional[str] |
|
|
selected_spacing_system: Optional[str] |
|
|
selected_naming_convention: Optional[str] |
|
|
selected_color_ramps: dict[str, bool] |
|
|
selected_a11y_fixes: list[str] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
desktop_final: Optional[FinalTokens] |
|
|
mobile_final: Optional[FinalTokens] |
|
|
|
|
|
|
|
|
version_label: str |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
current_stage: str |
|
|
|
|
|
|
|
|
awaiting_human_input: bool |
|
|
checkpoint_name: Optional[str] |
|
|
|
|
|
|
|
|
errors: Annotated[list[str], merge_lists] |
|
|
warnings: Annotated[list[str], merge_lists] |
|
|
|
|
|
|
|
|
messages: Annotated[Sequence[dict], add_messages] |
|
|
|
|
|
|
|
|
started_at: Optional[datetime] |
|
|
stage_started_at: Optional[datetime] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class DiscoveryState(TypedDict): |
|
|
"""State for page discovery sub-graph.""" |
|
|
base_url: str |
|
|
discovered_pages: list[DiscoveredPage] |
|
|
discovery_complete: bool |
|
|
error: Optional[str] |
|
|
|
|
|
|
|
|
class ExtractionState(TypedDict): |
|
|
"""State for extraction sub-graph (per viewport).""" |
|
|
viewport: Viewport |
|
|
pages_to_crawl: list[str] |
|
|
extraction_result: Optional[ExtractedTokens] |
|
|
progress: float |
|
|
current_page: Optional[str] |
|
|
error: Optional[str] |
|
|
|
|
|
|
|
|
class NormalizationState(TypedDict): |
|
|
"""State for normalization sub-graph.""" |
|
|
raw_tokens: ExtractedTokens |
|
|
normalized_tokens: Optional[NormalizedTokens] |
|
|
duplicates_found: list[tuple[str, str]] |
|
|
error: Optional[str] |
|
|
|
|
|
|
|
|
class AdvisorState(TypedDict): |
|
|
"""State for advisor sub-graph.""" |
|
|
normalized_desktop: NormalizedTokens |
|
|
normalized_mobile: Optional[NormalizedTokens] |
|
|
recommendations: Optional[UpgradeRecommendations] |
|
|
error: Optional[str] |
|
|
|
|
|
|
|
|
class GenerationState(TypedDict): |
|
|
"""State for generation sub-graph.""" |
|
|
normalized_tokens: NormalizedTokens |
|
|
selected_upgrades: dict[str, str] |
|
|
final_tokens: Optional[FinalTokens] |
|
|
error: Optional[str] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PageConfirmationState(TypedDict): |
|
|
"""State for page confirmation checkpoint.""" |
|
|
discovered_pages: list[DiscoveredPage] |
|
|
confirmed_pages: list[str] |
|
|
user_confirmed: bool |
|
|
|
|
|
|
|
|
class TokenReviewState(TypedDict): |
|
|
"""State for token review checkpoint (Stage 1 UI).""" |
|
|
desktop_tokens: NormalizedTokens |
|
|
mobile_tokens: Optional[NormalizedTokens] |
|
|
|
|
|
|
|
|
color_decisions: dict[str, bool] |
|
|
typography_decisions: dict[str, bool] |
|
|
spacing_decisions: dict[str, bool] |
|
|
|
|
|
user_confirmed: bool |
|
|
|
|
|
|
|
|
class UpgradeSelectionState(TypedDict): |
|
|
"""State for upgrade selection checkpoint (Stage 2 UI).""" |
|
|
recommendations: UpgradeRecommendations |
|
|
current_tokens: NormalizedTokens |
|
|
|
|
|
|
|
|
selected_options: dict[str, str] |
|
|
|
|
|
user_confirmed: bool |
|
|
|
|
|
|
|
|
class ExportApprovalState(TypedDict): |
|
|
"""State for export approval checkpoint (Stage 3 UI).""" |
|
|
desktop_final: FinalTokens |
|
|
mobile_final: Optional[FinalTokens] |
|
|
|
|
|
version_label: str |
|
|
user_confirmed: bool |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def create_initial_state(base_url: str) -> AgentState: |
|
|
"""Create initial state for a new workflow.""" |
|
|
return { |
|
|
|
|
|
"base_url": base_url, |
|
|
|
|
|
|
|
|
"discovered_pages": [], |
|
|
"pages_to_crawl": [], |
|
|
|
|
|
|
|
|
"desktop_extraction": None, |
|
|
"desktop_crawl_progress": 0.0, |
|
|
"mobile_extraction": None, |
|
|
"mobile_crawl_progress": 0.0, |
|
|
|
|
|
|
|
|
"desktop_normalized": None, |
|
|
"mobile_normalized": None, |
|
|
"accepted_colors": [], |
|
|
"rejected_colors": [], |
|
|
"accepted_typography": [], |
|
|
"rejected_typography": [], |
|
|
"accepted_spacing": [], |
|
|
"rejected_spacing": [], |
|
|
|
|
|
|
|
|
"upgrade_recommendations": None, |
|
|
"selected_type_scale": None, |
|
|
"selected_spacing_system": None, |
|
|
"selected_naming_convention": None, |
|
|
"selected_color_ramps": {}, |
|
|
"selected_a11y_fixes": [], |
|
|
|
|
|
|
|
|
"desktop_final": None, |
|
|
"mobile_final": None, |
|
|
"version_label": "v1-recovered", |
|
|
|
|
|
|
|
|
"current_stage": "discover", |
|
|
"awaiting_human_input": False, |
|
|
"checkpoint_name": None, |
|
|
"errors": [], |
|
|
"warnings": [], |
|
|
"messages": [], |
|
|
|
|
|
|
|
|
"started_at": datetime.now(), |
|
|
"stage_started_at": datetime.now(), |
|
|
} |
|
|
|
|
|
|
|
|
def get_stage_progress(state: AgentState) -> dict: |
|
|
"""Get progress information for the current workflow.""" |
|
|
stages = ["discover", "extract", "normalize", "advise", "generate", "export"] |
|
|
current_idx = stages.index(state["current_stage"]) if state["current_stage"] in stages else 0 |
|
|
|
|
|
return { |
|
|
"current_stage": state["current_stage"], |
|
|
"stage_index": current_idx, |
|
|
"total_stages": len(stages), |
|
|
"progress_percent": (current_idx / len(stages)) * 100, |
|
|
"awaiting_human": state["awaiting_human_input"], |
|
|
"checkpoint": state["checkpoint_name"], |
|
|
} |
|
|
|