| """ |
| 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"], |
| } |
|
|