Upload stage2_graph.py
Browse files- 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 |
-
-
|
| 733 |
-
-
|
| 734 |
-
-
|
|
|
|
|
|
|
| 735 |
|
| 736 |
### 3. Accessibility (AA Compliance)
|
| 737 |
-
-
|
| 738 |
-
-
|
|
|
|
|
|
|
| 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": {{
|
| 751 |
-
|
| 752 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 763 |
|
|
|
|
|
|
|
| 764 |
## ANALYST 1 FINDINGS:
|
| 765 |
-
{json.dumps(llm1_analysis, indent=2, default=str)[:
|
| 766 |
|
| 767 |
## ANALYST 2 FINDINGS:
|
| 768 |
-
{json.dumps(llm2_analysis, indent=2, default=str)[:
|
| 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.
|
|
|
|
| 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 |
-
"
|
| 791 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|