riazmo commited on
Commit
18eef7e
Β·
verified Β·
1 Parent(s): 5b569ba

Upload stage2_graph.py

Browse files
Files changed (1) hide show
  1. agents/stage2_graph.py +150 -27
agents/stage2_graph.py CHANGED
@@ -53,6 +53,7 @@ class Stage2State(TypedDict):
53
  desktop_tokens: dict
54
  mobile_tokens: dict
55
  competitors: list[str]
 
56
 
57
  # Parallel analysis outputs
58
  llm1_analysis: Optional[dict]
@@ -302,13 +303,15 @@ async def analyze_with_llm1(state: Stage2State, log_callback: Optional[Callable]
302
  log_callback(f" Provider: {provider}")
303
  log_callback(f" πŸ’° Cost: ${llm1_config.get('cost_per_million_input', 0.29)}/M in, ${llm1_config.get('cost_per_million_output', 0.59)}/M out")
304
  log_callback(f" πŸ“ Task: Typography, Colors, AA, Spacing analysis")
 
305
  log_callback("")
306
 
307
- # Build prompt
308
  prompt = build_analyst_prompt(
309
  tokens_summary=summarize_tokens(state["desktop_tokens"], state["mobile_tokens"]),
310
  competitors=state["competitors"],
311
  persona=llm1_config.get("persona", "Senior Design Systems Architect"),
 
312
  )
313
 
314
  try:
@@ -408,13 +411,15 @@ async def analyze_with_llm2(state: Stage2State, log_callback: Optional[Callable]
408
  log_callback(f" Provider: {provider}")
409
  log_callback(f" πŸ’° Cost: ${llm2_config.get('cost_per_million_input', 0.59)}/M in, ${llm2_config.get('cost_per_million_output', 0.79)}/M out")
410
  log_callback(f" πŸ“ Task: Typography, Colors, AA, Spacing analysis")
 
411
  log_callback("")
412
 
413
- # Build prompt
414
  prompt = build_analyst_prompt(
415
  tokens_summary=summarize_tokens(state["desktop_tokens"], state["mobile_tokens"]),
416
  competitors=state["competitors"],
417
  persona=llm2_config.get("persona", "Senior Design Systems Architect"),
 
418
  )
419
 
420
  try:
@@ -567,13 +572,14 @@ async def compile_with_head(state: Stage2State, log_callback: Optional[Callable]
567
  log_callback(f" Provider: {provider}")
568
  log_callback(f" πŸ’° Cost: ${head_config.get('cost_per_million_input', 0.59)}/M in, ${head_config.get('cost_per_million_output', 0.79)}/M out")
569
  log_callback("")
570
- log_callback(" πŸ“₯ INPUT: Analyzing outputs from LLM 1 + LLM 2 + Rules...")
571
 
572
- # Build HEAD prompt
573
  prompt = build_head_prompt(
574
  llm1_analysis=state.get("llm1_analysis", {}),
575
  llm2_analysis=state.get("llm2_analysis", {}),
576
  rule_calculations=state.get("rule_calculations", {}),
 
577
  )
578
 
579
  try:
@@ -707,8 +713,60 @@ def summarize_tokens(desktop: dict, mobile: dict) -> str:
707
  return "\n".join(lines)
708
 
709
 
710
- def build_analyst_prompt(tokens_summary: str, competitors: list[str], persona: str) -> str:
711
- """Build prompt for analyst LLMs."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
712
  return f"""You are a {persona}.
713
 
714
  ## YOUR TASK
@@ -716,6 +774,7 @@ Analyze these design tokens extracted from a website and compare against industr
716
 
717
  ## EXTRACTED TOKENS
718
  {tokens_summary}
 
719
 
720
  ## COMPETITOR DESIGN SYSTEMS TO RESEARCH
721
  {', '.join(competitors)}
@@ -728,14 +787,18 @@ Analyze these design tokens extracted from a website and compare against industr
728
  - Compare to competitors: what ratios do they use?
729
  - Score (1-10) and specific recommendations
730
 
731
- ### 2. Colors
732
- - Is the color palette cohesive?
733
- - Are semantic colors properly defined (primary, secondary, etc.)?
734
- - Score (1-10) and specific recommendations
 
 
735
 
736
  ### 3. Accessibility (AA Compliance)
737
- - What contrast issues might exist?
738
- - Score (1-10)
 
 
739
 
740
  ### 4. Spacing
741
  - Is spacing consistent? Does it follow a grid (4px, 8px)?
@@ -747,9 +810,29 @@ Analyze these design tokens extracted from a website and compare against industr
747
  ## RESPOND IN JSON FORMAT ONLY:
748
  ```json
749
  {{
750
- "typography": {{"analysis": "...", "detected_ratio": 1.2, "score": 7, "recommendations": ["..."]}},
751
- "colors": {{"analysis": "...", "score": 6, "recommendations": ["..."]}},
752
- "accessibility": {{"issues": ["..."], "score": 5}},
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
753
  "spacing": {{"analysis": "...", "detected_base": 8, "score": 7, "recommendations": ["..."]}},
754
  "top_3_priorities": ["...", "...", "..."],
755
  "confidence": 85
@@ -757,15 +840,34 @@ Analyze these design tokens extracted from a website and compare against industr
757
  ```"""
758
 
759
 
760
- def build_head_prompt(llm1_analysis: dict, llm2_analysis: dict, rule_calculations: dict) -> str:
761
- """Build prompt for HEAD compiler."""
762
- return f"""You are a Principal Design Systems Architect compiling analyses from two expert analysts.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
763
 
 
 
764
  ## ANALYST 1 FINDINGS:
765
- {json.dumps(llm1_analysis, indent=2, default=str)[:2000]}
766
 
767
  ## ANALYST 2 FINDINGS:
768
- {json.dumps(llm2_analysis, indent=2, default=str)[:2000]}
769
 
770
  ## RULE-BASED CALCULATIONS:
771
  - Base font size: {rule_calculations.get('base_font_size', 16)}px
@@ -773,22 +875,30 @@ def build_head_prompt(llm1_analysis: dict, llm2_analysis: dict, rule_calculation
773
  - Spacing options: 4px grid, 8px grid
774
 
775
  ## YOUR TASK:
776
- 1. Compare both analyst perspectives
777
  2. Identify agreements and disagreements
778
- 3. Synthesize final recommendations
 
779
 
780
  ## RESPOND IN JSON FORMAT ONLY:
781
  ```json
782
  {{
783
  "agreements": [{{"topic": "...", "finding": "..."}}],
784
- "disagreements": [{{"topic": "...", "resolution": "..."}}],
785
  "final_recommendations": {{
786
  "type_scale": "1.25",
787
  "type_scale_rationale": "...",
788
  "spacing_base": "8px",
789
  "spacing_rationale": "...",
790
- "color_improvements": ["..."],
791
- "accessibility_fixes": ["..."]
 
 
 
 
 
 
 
792
  }},
793
  "overall_confidence": 85,
794
  "summary": "..."
@@ -911,8 +1021,9 @@ async def run_stage2_multi_agent(
911
  mobile_tokens: dict,
912
  competitors: list[str],
913
  log_callback: Optional[Callable] = None,
 
914
  ) -> dict:
915
- """Run the Stage 2 multi-agent analysis."""
916
 
917
  global cost_tracker
918
  cost_tracker = CostTracker() # Reset
@@ -923,6 +1034,17 @@ async def run_stage2_multi_agent(
923
  log_callback("🧠 STAGE 2: MULTI-AGENT ANALYSIS")
924
  log_callback("=" * 60)
925
  log_callback("")
 
 
 
 
 
 
 
 
 
 
 
926
  log_callback("πŸ“¦ LLM CONFIGURATION:")
927
 
928
  config = load_agent_config()
@@ -940,11 +1062,12 @@ async def run_stage2_multi_agent(
940
  log_callback("")
941
  log_callback("πŸ”„ RUNNING PARALLEL ANALYSIS...")
942
 
943
- # Initial state
944
  initial_state = {
945
  "desktop_tokens": desktop_tokens,
946
  "mobile_tokens": mobile_tokens,
947
  "competitors": competitors,
 
948
  "llm1_analysis": None,
949
  "llm2_analysis": None,
950
  "rule_calculations": None,
 
53
  desktop_tokens: dict
54
  mobile_tokens: dict
55
  competitors: list[str]
56
+ semantic_analysis: Optional[dict] # NEW: Semantic color categorization from Stage 1
57
 
58
  # Parallel analysis outputs
59
  llm1_analysis: Optional[dict]
 
303
  log_callback(f" Provider: {provider}")
304
  log_callback(f" πŸ’° Cost: ${llm1_config.get('cost_per_million_input', 0.29)}/M in, ${llm1_config.get('cost_per_million_output', 0.59)}/M out")
305
  log_callback(f" πŸ“ Task: Typography, Colors, AA, Spacing analysis")
306
+ log_callback(f" 🧠 Semantic context: {'Yes' if state.get('semantic_analysis') else 'No'}")
307
  log_callback("")
308
 
309
+ # Build prompt with semantic analysis
310
  prompt = build_analyst_prompt(
311
  tokens_summary=summarize_tokens(state["desktop_tokens"], state["mobile_tokens"]),
312
  competitors=state["competitors"],
313
  persona=llm1_config.get("persona", "Senior Design Systems Architect"),
314
+ semantic_analysis=state.get("semantic_analysis"),
315
  )
316
 
317
  try:
 
411
  log_callback(f" Provider: {provider}")
412
  log_callback(f" πŸ’° Cost: ${llm2_config.get('cost_per_million_input', 0.59)}/M in, ${llm2_config.get('cost_per_million_output', 0.79)}/M out")
413
  log_callback(f" πŸ“ Task: Typography, Colors, AA, Spacing analysis")
414
+ log_callback(f" 🧠 Semantic context: {'Yes' if state.get('semantic_analysis') else 'No'}")
415
  log_callback("")
416
 
417
+ # Build prompt with semantic analysis
418
  prompt = build_analyst_prompt(
419
  tokens_summary=summarize_tokens(state["desktop_tokens"], state["mobile_tokens"]),
420
  competitors=state["competitors"],
421
  persona=llm2_config.get("persona", "Senior Design Systems Architect"),
422
+ semantic_analysis=state.get("semantic_analysis"),
423
  )
424
 
425
  try:
 
572
  log_callback(f" Provider: {provider}")
573
  log_callback(f" πŸ’° Cost: ${head_config.get('cost_per_million_input', 0.59)}/M in, ${head_config.get('cost_per_million_output', 0.79)}/M out")
574
  log_callback("")
575
+ log_callback(" πŸ“₯ INPUT: Analyzing outputs from LLM 1 + LLM 2 + Rules + Semantic...")
576
 
577
+ # Build HEAD prompt with semantic context
578
  prompt = build_head_prompt(
579
  llm1_analysis=state.get("llm1_analysis", {}),
580
  llm2_analysis=state.get("llm2_analysis", {}),
581
  rule_calculations=state.get("rule_calculations", {}),
582
+ semantic_analysis=state.get("semantic_analysis"),
583
  )
584
 
585
  try:
 
713
  return "\n".join(lines)
714
 
715
 
716
+ def build_analyst_prompt(tokens_summary: str, competitors: list[str], persona: str, semantic_analysis: dict = None) -> str:
717
+ """Build prompt for analyst LLMs with semantic color context."""
718
+
719
+ # Build semantic colors section if available
720
+ semantic_section = ""
721
+ brand_primary_hex = "unknown"
722
+
723
+ if semantic_analysis:
724
+ semantic_section = "\n\n## SEMANTIC COLOR ANALYSIS (from Stage 1)\n"
725
+ semantic_section += "Colors have been categorized by their actual CSS usage:\n"
726
+
727
+ # Brand colors
728
+ brand = semantic_analysis.get("brand", {})
729
+ if brand:
730
+ semantic_section += "\n### 🎨 Brand Colors (used on buttons, CTAs, links)\n"
731
+ for role, data in brand.items():
732
+ if data and isinstance(data, dict):
733
+ hex_val = data.get('hex', 'N/A')
734
+ if role == "primary":
735
+ brand_primary_hex = hex_val
736
+ semantic_section += f"- **{role}**: {hex_val} ({data.get('confidence', 'unknown')} confidence)\n"
737
+
738
+ # Text colors
739
+ text = semantic_analysis.get("text", {})
740
+ if text:
741
+ semantic_section += "\n### πŸ“ Text Colors (used with 'color' CSS property)\n"
742
+ for role, data in text.items():
743
+ if data and isinstance(data, dict):
744
+ semantic_section += f"- **{role}**: {data.get('hex', 'N/A')}\n"
745
+
746
+ # Background colors
747
+ bg = semantic_analysis.get("background", {})
748
+ if bg:
749
+ semantic_section += "\n### πŸ–ΌοΈ Background Colors (used with 'background-color')\n"
750
+ for role, data in bg.items():
751
+ if data and isinstance(data, dict):
752
+ semantic_section += f"- **{role}**: {data.get('hex', 'N/A')}\n"
753
+
754
+ # Border colors
755
+ border = semantic_analysis.get("border", {})
756
+ if border:
757
+ semantic_section += "\n### πŸ“ Border Colors\n"
758
+ for role, data in border.items():
759
+ if data and isinstance(data, dict):
760
+ semantic_section += f"- **{role}**: {data.get('hex', 'N/A')}\n"
761
+
762
+ # Feedback colors
763
+ feedback = semantic_analysis.get("feedback", {})
764
+ if feedback:
765
+ semantic_section += "\n### 🚨 Feedback Colors\n"
766
+ for role, data in feedback.items():
767
+ if data and isinstance(data, dict):
768
+ semantic_section += f"- **{role}**: {data.get('hex', 'N/A')}\n"
769
+
770
  return f"""You are a {persona}.
771
 
772
  ## YOUR TASK
 
774
 
775
  ## EXTRACTED TOKENS
776
  {tokens_summary}
777
+ {semantic_section}
778
 
779
  ## COMPETITOR DESIGN SYSTEMS TO RESEARCH
780
  {', '.join(competitors)}
 
787
  - Compare to competitors: what ratios do they use?
788
  - Score (1-10) and specific recommendations
789
 
790
+ ### 2. Colors (USE SEMANTIC ANALYSIS ABOVE!)
791
+ - **IMPORTANT**: The brand primary color is {brand_primary_hex}
792
+ - Is this brand color appropriate? Check contrast on white/light backgrounds
793
+ - Does the text color hierarchy (primary β†’ secondary β†’ muted) work well?
794
+ - Are feedback colors (error, success, warning) properly defined?
795
+ - Score (1-10) and SPECIFIC recommendations per color role
796
 
797
  ### 3. Accessibility (AA Compliance)
798
+ - Check brand primary color ({brand_primary_hex}) contrast on white
799
+ - Check text primary color contrast
800
+ - Identify any colors that fail WCAG AA (4.5:1 for text, 3:1 for large text)
801
+ - Score (1-10) and specific fixes with suggested replacement hex values
802
 
803
  ### 4. Spacing
804
  - Is spacing consistent? Does it follow a grid (4px, 8px)?
 
810
  ## RESPOND IN JSON FORMAT ONLY:
811
  ```json
812
  {{
813
+ "typography": {{
814
+ "analysis": "...",
815
+ "detected_ratio": 1.2,
816
+ "score": 7,
817
+ "recommendations": ["..."]
818
+ }},
819
+ "colors": {{
820
+ "analysis": "...",
821
+ "brand_primary": "{brand_primary_hex}",
822
+ "brand_primary_issue": "...",
823
+ "brand_primary_suggestion": "#xxx or keep",
824
+ "score": 6,
825
+ "role_recommendations": [
826
+ {{"role": "brand.primary", "current": "#xxx", "action": "keep|darken|lighten", "suggested": "#xxx", "reason": "..."}},
827
+ {{"role": "text.primary", "current": "#xxx", "action": "keep|darken|lighten", "suggested": "#xxx", "reason": "..."}}
828
+ ]
829
+ }},
830
+ "accessibility": {{
831
+ "issues": [
832
+ {{"color": "#xxx", "role": "brand.primary", "problem": "fails AA on white", "current_contrast": 3.2, "fix": "#xxx", "fixed_contrast": 4.8}}
833
+ ],
834
+ "score": 5
835
+ }},
836
  "spacing": {{"analysis": "...", "detected_base": 8, "score": 7, "recommendations": ["..."]}},
837
  "top_3_priorities": ["...", "...", "..."],
838
  "confidence": 85
 
840
  ```"""
841
 
842
 
843
+ def build_head_prompt(llm1_analysis: dict, llm2_analysis: dict, rule_calculations: dict, semantic_analysis: dict = None) -> str:
844
+ """Build prompt for HEAD compiler with semantic context."""
845
+
846
+ # Build semantic summary for HEAD
847
+ semantic_summary = ""
848
+ if semantic_analysis:
849
+ brand = semantic_analysis.get("brand", {})
850
+ brand_primary = brand.get("primary", {}).get("hex", "unknown") if brand.get("primary") else "unknown"
851
+ brand_secondary = brand.get("secondary", {}).get("hex", "unknown") if brand.get("secondary") else "unknown"
852
+
853
+ text = semantic_analysis.get("text", {})
854
+ text_primary = text.get("primary", {}).get("hex", "unknown") if text.get("primary") else "unknown"
855
+
856
+ semantic_summary = f"""
857
+ ## SEMANTIC COLOR CONTEXT (from Stage 1 Analysis)
858
+ - Brand Primary: {brand_primary}
859
+ - Brand Secondary: {brand_secondary}
860
+ - Text Primary: {text_primary}
861
+ - Analysis Method: {semantic_analysis.get('summary', {}).get('method', 'rule-based')}
862
+ """
863
 
864
+ return f"""You are a Principal Design Systems Architect compiling analyses from two expert analysts.
865
+ {semantic_summary}
866
  ## ANALYST 1 FINDINGS:
867
+ {json.dumps(llm1_analysis, indent=2, default=str)[:2500]}
868
 
869
  ## ANALYST 2 FINDINGS:
870
+ {json.dumps(llm2_analysis, indent=2, default=str)[:2500]}
871
 
872
  ## RULE-BASED CALCULATIONS:
873
  - Base font size: {rule_calculations.get('base_font_size', 16)}px
 
875
  - Spacing options: 4px grid, 8px grid
876
 
877
  ## YOUR TASK:
878
+ 1. Compare both analyst perspectives on typography, colors, spacing
879
  2. Identify agreements and disagreements
880
+ 3. For COLOR recommendations: Use the semantic context above to make SPECIFIC recommendations per role
881
+ 4. Synthesize final recommendations that a designer can act on
882
 
883
  ## RESPOND IN JSON FORMAT ONLY:
884
  ```json
885
  {{
886
  "agreements": [{{"topic": "...", "finding": "..."}}],
887
+ "disagreements": [{{"topic": "...", "analyst1_view": "...", "analyst2_view": "...", "resolution": "..."}}],
888
  "final_recommendations": {{
889
  "type_scale": "1.25",
890
  "type_scale_rationale": "...",
891
  "spacing_base": "8px",
892
  "spacing_rationale": "...",
893
+ "color_recommendations": {{
894
+ "brand_primary": {{"current": "#xxx", "action": "keep|change", "suggested": "#xxx", "rationale": "..."}},
895
+ "brand_secondary": {{"current": "#xxx", "action": "keep|change", "suggested": "#xxx", "rationale": "..."}},
896
+ "text_primary": {{"current": "#xxx", "action": "keep|change", "suggested": "#xxx", "rationale": "..."}},
897
+ "generate_ramps_for": ["brand.primary", "brand.secondary", "neutral"]
898
+ }},
899
+ "accessibility_fixes": [
900
+ {{"color": "#xxx", "role": "...", "issue": "...", "fix": "#xxx"}}
901
+ ]
902
  }},
903
  "overall_confidence": 85,
904
  "summary": "..."
 
1021
  mobile_tokens: dict,
1022
  competitors: list[str],
1023
  log_callback: Optional[Callable] = None,
1024
+ semantic_analysis: Optional[dict] = None,
1025
  ) -> dict:
1026
+ """Run the Stage 2 multi-agent analysis with semantic context."""
1027
 
1028
  global cost_tracker
1029
  cost_tracker = CostTracker() # Reset
 
1034
  log_callback("🧠 STAGE 2: MULTI-AGENT ANALYSIS")
1035
  log_callback("=" * 60)
1036
  log_callback("")
1037
+
1038
+ # Log semantic context
1039
+ if semantic_analysis:
1040
+ brand = semantic_analysis.get("brand", {})
1041
+ brand_primary = brand.get("primary", {}).get("hex", "unknown") if brand.get("primary") else "unknown"
1042
+ log_callback("🧠 SEMANTIC CONTEXT FROM STAGE 1:")
1043
+ log_callback(f" Brand Primary: {brand_primary}")
1044
+ log_callback(f" Text Primary: {semantic_analysis.get('text', {}).get('primary', {}).get('hex', 'unknown')}")
1045
+ log_callback(f" Analysis Method: {semantic_analysis.get('summary', {}).get('method', 'rule-based')}")
1046
+ log_callback("")
1047
+
1048
  log_callback("πŸ“¦ LLM CONFIGURATION:")
1049
 
1050
  config = load_agent_config()
 
1062
  log_callback("")
1063
  log_callback("πŸ”„ RUNNING PARALLEL ANALYSIS...")
1064
 
1065
+ # Initial state with semantic analysis
1066
  initial_state = {
1067
  "desktop_tokens": desktop_tokens,
1068
  "mobile_tokens": mobile_tokens,
1069
  "competitors": competitors,
1070
+ "semantic_analysis": semantic_analysis, # NEW: Pass semantic context
1071
  "llm1_analysis": None,
1072
  "llm2_analysis": None,
1073
  "rule_calculations": None,