import graphviz import json from tempfile import NamedTemporaryFile import os from graph_generator_utils import add_nodes_and_edges def generate_concept_map(json_input: str, output_format: str) -> str: """ Generates a concept map from JSON input. Args: json_input (str): A JSON string describing the concept map structure. It must follow the Expected JSON Format Example below. Expected JSON Format Example: { "central_node": "Artificial Intelligence (AI)", "nodes": [ { "id": "ml_fundamental", "label": "Machine Learning", "relationship": "is essential for", "subnodes": [ { "id": "dl_branch", "label": "Deep Learning", "relationship": "for example", "subnodes": [ { "id": "cnn_example", "label": "CNNs", "relationship": "for example" }, { "id": "rnn_example", "label": "RNNs", "relationship": "for example" } ] }, { "id": "rl_branch", "label": "Reinforcement Learning", "relationship": "for example", "subnodes": [ { "id": "qlearning_example", "label": "Q-Learning", "relationship": "example" }, { "id": "pg_example", "label": "Policy Gradients", "relationship": "example" } ] } ] }, { "id": "ai_types", "label": "Types", "relationship": "formed by", "subnodes": [ { "id": "agi_type", "label": "AGI", "relationship": "this is", "subnodes": [ { "id": "strong_ai", "label": "Strong AI", "relationship": "provoked by", "subnodes": [ { "id": "human_intel", "label": "Human-level Intel.", "relationship": "of" } ] } ] }, { "id": "ani_type", "label": "ANI", "relationship": "this is", "subnodes": [ { "id": "weak_ai", "label": "Weak AI", "relationship": "provoked by", "subnodes": [ { "id": "narrow_tasks", "label": "Narrow Tasks", "relationship": "of" } ] } ] } ] }, { "id": "ai_capabilities", "label": "Capabilities", "relationship": "change", "subnodes": [ { "id": "data_proc", "label": "Data Processing", "relationship": "can", "subnodes": [ { "id": "big_data", "label": "Big Data", "relationship": "as", "subnodes": [ { "id": "analysis_example", "label": "Data Analysis", "relationship": "example" }, { "id": "prediction_example", "label": "Prediction", "relationship": "example" } ] } ] }, { "id": "decision_making", "label": "Decision Making", "relationship": "can be", "subnodes": [ { "id": "automation", "label": "Automation", "relationship": "as", "subnodes": [ { "id": "robotics_example", "label": "Robotics", "relationship": "Example"}, { "id": "autonomous_example", "label": "Autonomous Vehicles", "relationship": "of one" } ] } ] }, { "id": "problem_solving", "label": "Problem Solving", "relationship": "can", "subnodes": [ { "id": "optimization", "label": "Optimization", "relationship": "as is", "subnodes": [ { "id": "algorithms_example", "label": "Algorithms", "relationship": "for example" } ] } ] } ] } ] } Returns: str: The filepath to the generated PNG image file. """ try: if not json_input.strip(): return "Error: Empty input" data = json.loads(json_input) if 'central_node' not in data or 'nodes' not in data: raise ValueError("Missing required fields: central_node or nodes") # 한글 폰트 설정 # 환경 변수에서 폰트 경로 가져오기 font_path = os.environ.get('KOREAN_FONT_PATH', '') # Graphviz는 시스템 폰트를 사용하므로 폰트 이름으로 지정 # NanumGothic이 시스템에 설치되어 있어야 함 korean_font = 'NanumGothic' dot = graphviz.Digraph( name='ConceptMap', format='png', graph_attr={ 'rankdir': 'TB', # Top-to-Bottom layout (vertical hierarchy) 'splines': 'ortho', # Straight lines 'bgcolor': 'white', # White background 'pad': '0.5', # Padding around the graph 'fontname': korean_font, # 그래프 전체 폰트 설정 'charset': 'UTF-8' # UTF-8 인코딩 }, node_attr={ 'fontname': korean_font # 모든 노드의 기본 폰트 }, edge_attr={ 'fontname': korean_font # 모든 엣지의 기본 폰트 } ) base_color = '#19191a' # Hardcoded base color # Central node styling (rounded box, dark color) dot.node( 'central', data['central_node'], shape='box', # Rectangular shape style='filled,rounded', # Filled and rounded corners fillcolor=base_color, # Darkest color fontcolor='white', # White text for dark background fontsize='16', # Larger font for central node fontname=korean_font # 한글 폰트 명시적 지정 ) # Add child nodes and edges recursively starting from depth 1 add_nodes_and_edges(dot, 'central', data.get('nodes', []), current_depth=1, base_color=base_color) with NamedTemporaryFile(delete=False, suffix=f'.{output_format}') as tmp: dot.render(tmp.name, format=output_format, cleanup=True) return f"{tmp.name}.{output_format}" except json.JSONDecodeError: return "Error: Invalid JSON format" except Exception as e: return f"Error: {str(e)}"