Saturday, July 25, 2026

BUILDING AN LLM-POWERED CRC CARD DIAGRAMMING TOOL

 


INTRODUCTION


Class-Responsibilities-Collaboration cards represent a fundamental technique in object-oriented design. They provide a simple yet powerful way to model the responsibilities of classes and their collaborative relationships. Traditional CRC card sessions involve physical index cards arranged on a table, but this approach lacks persistence, version control, and the ability to easily share and modify designs.


This article describes the design and implementation of an intelligent diagramming tool that lleverages Large Language Models to create, modify, and manage CRC card diagrams through natural language commands. The tool interprets user descriptions, automatically layouts components, draws relationship arrows, and exports diagrams to both visual formats (PNG, SVG) and structured data formats (JSON) for later restoration.

The system addresses several technical challenges. First, it must understand natural language commands with varying levels of specificity and ambiguity. Second, it must compute aesthetically pleasing layouts where components do not overlap and connection arrows minimize crossings. Third, it must support diverse execution environments including different operating systems and GPU architectures. Finally, it must maintain perfect state consistency between the visual representation and the underlying data model.


SYSTEM ARCHITECTURE OVERVIEW

The architecture follows clean architecture principles with clear separation of concerns. The system comprises six major subsystems that interact through well-defined interfaces.

The Domain Model subsystem represents CRC cards as first-class entities with names, responsibilities, and collaborators. This layer has no dependencies on external frameworks or libraries, making it the most stable part of the system.


The LLM Abstraction Layer provides a unified interface to interact with various language models, whether they run locally or remotely. This layer handles the complexity of different API formats, authentication mechanisms, and response parsing. It detects available GPU hardware and configures the appropriate acceleration backend.


The Command Parser subsystem takes natural language input and LLM responses to extract structured commands. It identifies user intent such as creating new components, modifying existing ones, establishing relationships, or requesting snapshots.


The Layout Engine computes positions for CRC cards and routes connection arrows to minimize visual clutter. It implements graph layout algorithms that consider component sizes, relationship directions, and aesthetic criteria.


The Rendering Engine translates the positioned layout into visual representations. It supports multiple output formats including raster images (PNG, JPEG) and vector graphics (SVG). The rendering process applies styling rules to create visually appealing diagrams.


The State Management subsystem maintains the current diagram state, handles persistence to JSON, and supports restoration from previously saved states. It implements the memento pattern to enable snapshot functionality.


DOMAIN MODEL: CRC CARD REPRESENTATION

The domain model centers on three core entities: CRCCard, Responsibility, and Collaboration. These entities capture the essential information needed to represent and manipulate CRC cards.


A CRCCard contains a unique identifier, a component name, a collection of responsibilities, and a collection of collaborator references. The unique identifier ensures that cards can be reliably referenced even if their names change. The component name represents the class or module being designed. Responsibilities describe what the component does, typically expressed as verb phrases. Collaborators identify other components that this component depends upon or interacts with.


Here is the core domain model implementation:


class Responsibility:

    def __init__(self, description):

        # Each responsibility is a description of what the component does

        # Examples: "Manages user authentication", "Validates input data"

        self.description = description

        

    def __eq__(self, other):

        if not isinstance(other, Responsibility):

            return False

        return self.description == other.description

        

    def __hash__(self):

        return hash(self.description)

        

    def to_dict(self):

        return {"description": self.description}

        

    @staticmethod

    def from_dict(data):

        return Responsibility(data["description"])



class Collaboration:

    def __init__(self, collaborator_id, collaborator_name):

        # collaborator_id: unique identifier of the component we collaborate with

        # collaborator_name: human-readable name for display purposes

        self.collaborator_id = collaborator_id

        self.collaborator_name = collaborator_name

        

    def __eq__(self, other):

        if not isinstance(other, Collaboration):

            return False

        return self.collaborator_id == other.collaborator_id

        

    def __hash__(self):

        return hash(self.collaborator_id)

        

    def to_dict(self):

        return {

            "collaborator_id": self.collaborator_id,

            "collaborator_name": self.collaborator_name

        }

        

    @staticmethod

    def from_dict(data):

        return Collaboration(data["collaborator_id"], data["collaborator_name"])



class CRCCard:

    def __init__(self, card_id, name):

        # card_id: unique identifier for this card

        # name: the component/class name

        self.card_id = card_id

        self.name = name

        self.responsibilities = []

        self.collaborations = []

        

    def add_responsibility(self, responsibility):

        if responsibility not in self.responsibilities:

            self.responsibilities.append(responsibility)

            

    def remove_responsibility(self, responsibility):

        if responsibility in self.responsibilities:

            self.responsibilities.remove(responsibility)

            

    def add_collaboration(self, collaboration):

        if collaboration not in self.collaborations:

            self.collaborations.append(collaboration)

            

    def remove_collaboration(self, collaborator_id):

        self.collaborations = [c for c in self.collaborations 

                              if c.collaborator_id != collaborator_id]

                              

    def to_dict(self):

        return {

            "card_id": self.card_id,

            "name": self.name,

            "responsibilities": [r.to_dict() for r in self.responsibilities],

            "collaborations": [c.to_dict() for c in self.collaborations]

        }

        

    @staticmethod

    def from_dict(data):

        card = CRCCard(data["card_id"], data["name"])

        card.responsibilities = [Responsibility.from_dict(r) 

                                for r in data["responsibilities"]]

        card.collaborations = [Collaboration.from_dict(c) 

                              for c in data["collaborations"]]

        return card


The domain model remains independent of presentation concerns. It does not know about positions, colors, or rendering. This separation allows the same domain model to be used with different visualization strategies or even non-visual representations.


LLM ABSTRACTION LAYER

The LLM abstraction layer provides a unified interface to interact with language models across different platforms and providers. The key challenge is handling the diversity of APIs, authentication methods, and response formats while presenting a consistent interface to the rest of the application.


The abstraction defines a base interface that all LLM providers must implement. This interface includes methods for initialization, text generation, and resource cleanup. Concrete implementations handle the specifics of each provider.


For local models, the system must detect available GPU hardware and configure the appropriate acceleration backend. Modern deep learning frameworks support multiple GPU vendors through different backends. NVIDIA GPUs use CUDA, AMD GPUs use ROCm, Apple Silicon uses Metal Performance Shaders, and Intel GPUs use oneAPI or OpenCL.


The hardware detection logic examines the system environment to determine available acceleration options:


import platform

import subprocess

import os



class HardwareDetector:

    def __init__(self):

        self.system = platform.system()

        self.machine = platform.machine()

        

    def detect_gpu_backend(self):

        # Returns the best available GPU backend for the current system

        # Priority: CUDA > ROCm > MPS > OpenCL > CPU

        

        if self._has_cuda():

            return "cuda"

        elif self._has_rocm():

            return "rocm"

        elif self._has_mps():

            return "mps"

        elif self._has_opencl():

            return "opencl"

        else:

            return "cpu"

            

    def _has_cuda(self):

        # Check for NVIDIA CUDA support

        try:

            result = subprocess.run(['nvidia-smi'], 

                                  capture_output=True, 

                                  timeout=5)

            return result.returncode == 0

        except (FileNotFoundError, subprocess.TimeoutExpired):

            return False

            

    def _has_rocm(self):

        # Check for AMD ROCm support

        if self.system != "Linux":

            return False

        try:

            result = subprocess.run(['rocm-smi'], 

                                  capture_output=True, 

                                  timeout=5)

            return result.returncode == 0

        except (FileNotFoundError, subprocess.TimeoutExpired):

            return False

            

    def _has_mps(self):

        # Check for Apple Metal Performance Shaders

        if self.system != "Darwin":

            return False

        # MPS is available on macOS 12.3+ with Apple Silicon

        if self.machine == "arm64":

            try:

                import torch

                return torch.backends.mps.is_available()

            except ImportError:

                return False

        return False

        

    def _has_opencl(self):

        # Check for OpenCL support (Intel and others)

        try:

            import pyopencl

            platforms = pyopencl.get_platforms()

            return len(platforms) > 0

        except ImportError:

            return False


The LLM provider interface defines the contract that all implementations must fulfill:


from abc import ABC, abstractmethod



class LLMProvider(ABC):

    @abstractmethod

    def initialize(self, **kwargs):

        # Initialize the LLM with provider-specific configuration

        # kwargs may include: model_name, api_key, temperature, max_tokens, etc.

        pass

        

    @abstractmethod

    def generate(self, prompt, system_prompt=None):

        # Generate a response from the LLM given a user prompt

        # system_prompt: optional instructions for the model's behavior

        # Returns: generated text as a string

        pass

        

    @abstractmethod

    def cleanup(self):

        # Release resources and cleanup

        pass


For local models using the transformers library with GPU acceleration, the implementation configures the appropriate device and data types:


class LocalTransformersProvider(LLMProvider):

    def __init__(self):

        self.model = None

        self.tokenizer = None

        self.device = None

        self.backend = None

        

    def initialize(self, model_name="mistralai/Mistral-7B-Instruct-v0.2", 

                  temperature=0.7, max_tokens=2048, **kwargs):

        import torch

        from transformers import AutoModelForCausalLM, AutoTokenizer

        

        # Detect the best available hardware backend

        detector = HardwareDetector()

        self.backend = detector.detect_gpu_backend()

        

        # Configure device and dtype based on backend

        if self.backend == "cuda":

            self.device = torch.device("cuda")

            dtype = torch.float16  # Use half precision for efficiency

        elif self.backend == "rocm":

            self.device = torch.device("cuda")  # ROCm uses cuda device API

            dtype = torch.float16

        elif self.backend == "mps":

            self.device = torch.device("mps")

            dtype = torch.float32  # MPS works better with float32

        else:

            self.device = torch.device("cpu")

            dtype = torch.float32

            

        # Load tokenizer

        self.tokenizer = AutoTokenizer.from_pretrained(model_name)

        

        # Load model with appropriate configuration

        self.model = AutoModelForCausalLM.from_pretrained(

            model_name,

            torch_dtype=dtype,

            device_map="auto" if self.backend != "cpu" else None,

            low_cpu_mem_usage=True

        )

        

        if self.backend == "cpu":

            self.model.to(self.device)

            

        self.temperature = temperature

        self.max_tokens = max_tokens

        

    def generate(self, prompt, system_prompt=None):

        import torch

        

        # Construct the full prompt with system instructions if provided

        if system_prompt:

            full_prompt = f"{system_prompt}\n\nUser: {prompt}\n\nAssistant:"

        else:

            full_prompt = prompt

            

        # Tokenize input

        inputs = self.tokenizer(full_prompt, return_tensors="pt")

        inputs = {k: v.to(self.device) for k, v in inputs.items()}

        

        # Generate response

        with torch.no_grad():

            outputs = self.model.generate(

                **inputs,

                max_new_tokens=self.max_tokens,

                temperature=self.temperature,

                do_sample=True,

                pad_token_id=self.tokenizer.eos_token_id

            )

            

        # Decode and return the generated text

        generated_text = self.tokenizer.decode(outputs[0], 

                                               skip_special_tokens=True)

        

        # Extract only the new generated portion

        response = generated_text[len(full_prompt):].strip()

        return response

        

    def cleanup(self):

        import torch

        if self.model is not None:

            del self.model

        if self.tokenizer is not None:

            del self.tokenizer

        if torch.cuda.is_available():

            torch.cuda.empty_cache()


For remote API providers like OpenAI, the implementation handles HTTP requests and API-specific authentication:


class OpenAIProvider(LLMProvider):

    def __init__(self):

        self.api_key = None

        self.model_name = None

        self.temperature = None

        self.max_tokens = None

        

    def initialize(self, api_key=None, model_name="gpt-4", 

                  temperature=0.7, max_tokens=2048, **kwargs):

        import os

        

        # Get API key from parameter or environment variable

        self.api_key = api_key or os.getenv("OPENAI_API_KEY")

        if not self.api_key:

            raise ValueError("OpenAI API key must be provided")

            

        self.model_name = model_name

        self.temperature = temperature

        self.max_tokens = max_tokens

        

    def generate(self, prompt, system_prompt=None):

        import requests

        

        # Construct messages for chat completion API

        messages = []

        if system_prompt:

            messages.append({"role": "system", "content": system_prompt})

        messages.append({"role": "user", "content": prompt})

        

        # Make API request

        headers = {

            "Authorization": f"Bearer {self.api_key}",

            "Content-Type": "application/json"

        }

        

        data = {

            "model": self.model_name,

            "messages": messages,

            "temperature": self.temperature,

            "max_tokens": self.max_tokens

        }

        

        response = requests.post(

            "https://api.openai.com/v1/chat/completions",

            headers=headers,

            json=data,

            timeout=60

        )

        

        response.raise_for_status()

        result = response.json()

        

        return result["choices"][0]["message"]["content"]

        

    def cleanup(self):

        # No cleanup needed for API-based provider

        pass


The LLM manager provides a factory pattern to create the appropriate provider based on configuration:


class LLMManager:

    def __init__(self):

        self.provider = None

        

    def setup(self, provider_type="local", **kwargs):

        # Create and initialize the appropriate LLM provider

        # provider_type: "local", "openai", "anthropic", etc.

        

        if provider_type == "local":

            self.provider = LocalTransformersProvider()

        elif provider_type == "openai":

            self.provider = OpenAIProvider()

        else:

            raise ValueError(f"Unknown provider type: {provider_type}")

            

        self.provider.initialize(**kwargs)

        

    def query(self, prompt, system_prompt=None):

        if self.provider is None:

            raise RuntimeError("LLM provider not initialized")

        return self.provider.generate(prompt, system_prompt)

        

    def shutdown(self):

        if self.provider is not None:

            self.provider.cleanup()


This abstraction layer allows the application to switch between different LLM backends without changing the higher-level logic. The hardware detection ensures optimal performance on whatever GPU architecture is available.


COMMAND PARSER AND INTENT RECOGNITION

The command parser interprets natural language input to extract structured commands that the system can execute. This component bridges the gap between free-form user descriptions and the precise operations needed to manipulate the diagram.


The parser uses the LLM to analyze user input and identify intent. It provides the LLM with a system prompt that defines the expected output format and available command types. The LLM response is then parsed into structured command objects.


The system supports several command types. The CREATE_COMPONENT command adds a new CRC card with specified name, responsibilities, and collaborators. The MODIFY_COMPONENT command updates an existing card by adding or removing responsibilities or collaborators. The DELETE_COMPONENT command removes a card from the diagram. The SNAPSHOT command saves the current state to files. The RESTORE command loads a previously saved diagram from a JSON file.


The command parser implementation uses a structured prompt to guide the LLM:


import json



class CommandParser:

    def __init__(self, llm_manager):

        self.llm = llm_manager

        self.system_prompt = self._build_system_prompt()

        

    def _build_system_prompt(self):

        # Define the system prompt that instructs the LLM how to parse commands

        return """You are a command parser for a CRC card diagramming tool.

Your task is to analyze user input and extract structured commands.


Available command types:

1. CREATE_COMPONENT: Create a new CRC card

2. MODIFY_COMPONENT: Modify an existing CRC card

3. DELETE_COMPONENT: Remove a CRC card

4. SNAPSHOT: Save the current diagram

5. RESTORE: Load a saved diagram

6. FINISH: User indicates the diagram is complete


For each user input, respond with a JSON object containing:

{

  "command_type": "CREATE_COMPONENT|MODIFY_COMPONENT|DELETE_COMPONENT|SNAPSHOT|RESTORE|FINISH",

  "parameters": {

    // command-specific parameters

  }

}


CREATE_COMPONENT parameters:

- name: component name (string)

- responsibilities: list of responsibility descriptions (list of strings)

- collaborators: list of collaborator names (list of strings)


MODIFY_COMPONENT parameters:

- name: component name to modify (string)

- add_responsibilities: responsibilities to add (list of strings, optional)

- remove_responsibilities: responsibilities to remove (list of strings, optional)

- add_collaborators: collaborators to add (list of strings, optional)

- remove_collaborators: collaborators to remove (list of strings, optional)


DELETE_COMPONENT parameters:

- name: component name to delete (string)


SNAPSHOT parameters:

- filename: base filename for saved files (string, optional)


RESTORE parameters:

- filename: JSON file to restore from (string)


FINISH parameters: none


Respond ONLY with the JSON object, no additional text."""

        

    def parse(self, user_input):

        # Parse user input into a structured command

        # Returns a Command object or None if parsing fails

        

        try:

            # Query the LLM with the user input

            response = self.llm.query(user_input, self.system_prompt)

            

            # Extract JSON from response (handle cases where LLM adds extra text)

            json_str = self._extract_json(response)

            

            # Parse JSON

            command_data = json.loads(json_str)

            

            # Create and return appropriate Command object

            return Command.from_dict(command_data)

            

        except Exception as e:

            print(f"Error parsing command: {e}")

            return None

            

    def _extract_json(self, text):

        # Extract JSON object from text that may contain additional content

        # Look for content between first { and last }

        

        start = text.find('{')

        end = text.rfind('}')

        

        if start == -1 or end == -1:

            raise ValueError("No JSON object found in response")

            

        return text[start:end+1]



class Command:

    def __init__(self, command_type, parameters):

        self.command_type = command_type

        self.parameters = parameters

        

    @staticmethod

    def from_dict(data):

        return Command(data["command_type"], data.get("parameters", {}))

        

    def to_dict(self):

        return {

            "command_type": self.command_type,

            "parameters": self.parameters

        }


The command parser leverages the LLM's natural language understanding capabilities to handle variations in how users express their intent. For example, the user might say "Add a new component called UserManager" or "Create a UserManager class" and both would be correctly interpreted as CREATE_COMPONENT commands.


LAYOUT ENGINE

The layout engine computes positions for CRC cards and routes connection arrows to create aesthetically pleasing diagrams. This is a challenging problem because it must balance multiple competing objectives: minimize arrow crossings, distribute cards evenly, respect the directed graph structure, and avoid overlaps.


The layout algorithm uses a force-directed approach combined with hierarchical constraints. Force-directed layouts treat the diagram as a physical system where cards repel each other and connections act as springs. This produces natural-looking layouts but can result in unstable positions. Adding hierarchical constraints based on the dependency graph creates more predictable layouts where components appear in a logical order.


The layout process proceeds in several phases. First, the algorithm analyzes the dependency graph to identify hierarchical levels. Components with no dependencies go in the top level. Components that depend only on top-level components go in the second level, and so on. This creates a natural flow from independent components to dependent ones.


Second, the algorithm assigns initial positions based on the hierarchical levels. Cards in the same level are distributed horizontally with equal spacing. Levels are stacked vertically with sufficient space for arrows.


Third, the algorithm applies force-directed refinement to improve the layout. Repulsive forces push overlapping cards apart. Attractive forces along connections pull related cards closer. The forces are applied iteratively until the system reaches equilibrium or a maximum iteration count.


Fourth, the algorithm routes connection arrows using orthogonal edge routing or Bezier curves. The routing algorithm attempts to minimize crossings by analyzing the positions of source and target cards and choosing appropriate control points.


Here is the layout engine implementation:


import math



class LayoutEngine:

    def __init__(self, card_width=200, card_height=150, 

                horizontal_spacing=100, vertical_spacing=150):

        self.card_width = card_width

        self.card_height = card_height

        self.horizontal_spacing = horizontal_spacing

        self.vertical_spacing = vertical_spacing

        

    def compute_layout(self, cards):

        # Compute positions for all cards and routing for all connections

        # Returns a dictionary mapping card_id to Position objects

        # and a list of Connection objects with routing information

        

        if not cards:

            return {}, []

            

        # Build dependency graph

        graph = self._build_dependency_graph(cards)

        

        # Compute hierarchical levels

        levels = self._compute_hierarchical_levels(graph, cards)

        

        # Assign initial positions based on levels

        positions = self._assign_initial_positions(levels, cards)

        

        # Apply force-directed refinement

        positions = self._apply_force_directed_refinement(positions, 

                                                          graph, 

                                                          cards)

        

        # Route connections

        connections = self._route_connections(positions, cards)

        

        return positions, connections

        

    def _build_dependency_graph(self, cards):

        # Build a dictionary mapping card_id to list of collaborator card_ids

        graph = {}

        card_id_map = {card.name: card.card_id for card in cards}

        

        for card in cards:

            collaborator_ids = []

            for collab in card.collaborations:

                # Try to find the collaborator by ID first, then by name

                if collab.collaborator_id in [c.card_id for c in cards]:

                    collaborator_ids.append(collab.collaborator_id)

                elif collab.collaborator_name in card_id_map:

                    collaborator_ids.append(card_id_map[collab.collaborator_name])

                    

            graph[card.card_id] = collaborator_ids

            

        return graph

        

    def _compute_hierarchical_levels(self, graph, cards):

        # Compute hierarchical levels using topological sorting

        # Returns a list of lists, where each inner list contains card_ids at that level

        

        # Calculate in-degree for each node

        in_degree = {card.card_id: 0 for card in cards}

        for card_id, collaborators in graph.items():

            for collab_id in collaborators:

                if collab_id in in_degree:

                    in_degree[collab_id] += 1

                    

        # Process nodes level by level

        levels = []

        remaining = set(card.card_id for card in cards)

        

        while remaining:

            # Find all nodes with in-degree 0 in remaining set

            current_level = [card_id for card_id in remaining 

                           if in_degree[card_id] == 0]

            

            if not current_level:

                # Cycle detected or isolated components

                # Add all remaining nodes to final level

                current_level = list(remaining)

                

            levels.append(current_level)

            

            # Remove current level from remaining

            for card_id in current_level:

                remaining.remove(card_id)

                

            # Decrease in-degree for nodes that depend on current level

            for card_id in current_level:

                for collab_id in graph.get(card_id, []):

                    if collab_id in in_degree:

                        in_degree[collab_id] -= 1

                        

        return levels

        

    def _assign_initial_positions(self, levels, cards):

        # Assign initial positions based on hierarchical levels

        positions = {}

        

        for level_index, level in enumerate(levels):

            num_cards = len(level)

            level_width = (num_cards * self.card_width + 

                         (num_cards - 1) * self.horizontal_spacing)

            

            # Center the level horizontally

            start_x = -level_width / 2

            y = level_index * (self.card_height + self.vertical_spacing)

            

            for card_index, card_id in enumerate(level):

                x = start_x + card_index * (self.card_width + self.horizontal_spacing)

                positions[card_id] = Position(x, y)

                

        return positions

        

    def _apply_force_directed_refinement(self, positions, graph, cards, 

                                        iterations=50):

        # Apply force-directed algorithm to refine positions

        # Uses repulsive forces between all cards and attractive forces along edges

        

        repulsion_strength = 5000

        attraction_strength = 0.1

        damping = 0.9

        

        velocities = {card_id: Position(0, 0) for card_id in positions}

        

        for iteration in range(iterations):

            forces = {card_id: Position(0, 0) for card_id in positions}

            

            # Calculate repulsive forces between all pairs

            card_ids = list(positions.keys())

            for i, card_id1 in enumerate(card_ids):

                for card_id2 in card_ids[i+1:]:

                    pos1 = positions[card_id1]

                    pos2 = positions[card_id2]

                    

                    dx = pos1.x - pos2.x

                    dy = pos1.y - pos2.y

                    distance = math.sqrt(dx*dx + dy*dy)

                    

                    if distance < 1:

                        distance = 1

                        

                    # Repulsive force inversely proportional to distance

                    force_magnitude = repulsion_strength / (distance * distance)

                    

                    fx = (dx / distance) * force_magnitude

                    fy = (dy / distance) * force_magnitude

                    

                    forces[card_id1].x += fx

                    forces[card_id1].y += fy

                    forces[card_id2].x -= fx

                    forces[card_id2].y -= fy

                    

            # Calculate attractive forces along edges

            for card_id, collaborators in graph.items():

                if card_id not in positions:

                    continue

                    

                pos1 = positions[card_id]

                

                for collab_id in collaborators:

                    if collab_id not in positions:

                        continue

                        

                    pos2 = positions[collab_id]

                    

                    dx = pos2.x - pos1.x

                    dy = pos2.y - pos1.y

                    distance = math.sqrt(dx*dx + dy*dy)

                    

                    if distance < 1:

                        continue

                        

                    # Attractive force proportional to distance

                    force_magnitude = attraction_strength * distance

                    

                    fx = (dx / distance) * force_magnitude

                    fy = (dy / distance) * force_magnitude

                    

                    forces[card_id].x += fx

                    forces[card_id].y += fy

                    forces[collab_id].x -= fx

                    forces[collab_id].y -= fy

                    

            # Update velocities and positions

            for card_id in positions:

                velocities[card_id].x = (velocities[card_id].x + 

                                        forces[card_id].x) * damping

                velocities[card_id].y = (velocities[card_id].y + 

                                        forces[card_id].y) * damping

                

                positions[card_id].x += velocities[card_id].x

                positions[card_id].y += velocities[card_id].y

                

        return positions

        

    def _route_connections(self, positions, cards):

        # Route connection arrows between cards

        # Returns a list of Connection objects

        

        connections = []

        

        for card in cards:

            source_pos = positions.get(card.card_id)

            if not source_pos:

                continue

                

            for collab in card.collaborations:

                # Find target position

                target_pos = None

                for other_card in cards:

                    if (other_card.card_id == collab.collaborator_id or 

                        other_card.name == collab.collaborator_name):

                        target_pos = positions.get(other_card.card_id)

                        break

                        

                if not target_pos:

                    continue

                    

                # Calculate connection points on card boundaries

                source_point = self._calculate_connection_point(

                    source_pos, target_pos, self.card_width, self.card_height)

                target_point = self._calculate_connection_point(

                    target_pos, source_pos, self.card_width, self.card_height)

                

                # Create connection with simple straight line

                # More sophisticated routing could use Bezier curves or orthogonal routing

                connection = Connection(

                    source_id=card.card_id,

                    target_id=collab.collaborator_id,

                    source_point=source_point,

                    target_point=target_point,

                    control_points=[]

                )

                

                connections.append(connection)

                

        return connections

        

    def _calculate_connection_point(self, from_pos, to_pos, width, height):

        # Calculate the point on the card boundary where the connection should attach

        # Uses the direction from from_pos to to_pos to determine which edge

        

        dx = to_pos.x - from_pos.x

        dy = to_pos.y - from_pos.y

        

        # Calculate angle

        angle = math.atan2(dy, dx)

        

        # Determine which edge based on angle

        # Card boundaries: left edge at x, right edge at x+width

        #                  top edge at y, bottom edge at y+height

        

        half_width = width / 2

        half_height = height / 2

        

        # Calculate intersection with card boundary

        if abs(dx) > abs(dy):

            # More horizontal than vertical

            if dx > 0:

                # Right edge

                return Position(from_pos.x + half_width, 

                              from_pos.y + half_height * (dy / abs(dx)))

            else:

                # Left edge

                return Position(from_pos.x - half_width, 

                              from_pos.y + half_height * (dy / abs(dx)))

        else:

            # More vertical than horizontal

            if dy > 0:

                # Bottom edge

                return Position(from_pos.x + half_width * (dx / abs(dy)), 

                              from_pos.y + half_height)

            else:

                # Top edge

                return Position(from_pos.x + half_width * (dx / abs(dy)), 

                              from_pos.y - half_height)



class Position:

    def __init__(self, x, y):

        self.x = x

        self.y = y

        

    def to_dict(self):

        return {"x": self.x, "y": self.y}

        

    @staticmethod

    def from_dict(data):

        return Position(data["x"], data["y"])



class Connection:

    def __init__(self, source_id, target_id, source_point, target_point, 

                control_points):

        self.source_id = source_id

        self.target_id = target_id

        self.source_point = source_point

        self.target_point = target_point

        self.control_points = control_points  # For Bezier curves

        

    def to_dict(self):

        return {

            "source_id": self.source_id,

            "target_id": self.target_id,

            "source_point": self.source_point.to_dict(),

            "target_point": self.target_point.to_dict(),

            "control_points": [p.to_dict() for p in self.control_points]

        }

        

    @staticmethod

    def from_dict(data):

        return Connection(

            data["source_id"],

            data["target_id"],

            Position.from_dict(data["source_point"]),

            Position.from_dict(data["target_point"]),

            [Position.from_dict(p) for p in data["control_points"]]

        )


The layout engine produces positions and connection routing that can be rendered by the rendering engine. The force-directed approach ensures that the layout adapts naturally to different diagram structures while the hierarchical constraints maintain readability.


RENDERING ENGINE

The rendering engine transforms the positioned layout into visual representations. It supports multiple output formats to serve different use cases. PNG and JPEG formats provide raster images suitable for embedding in documents or presentations. SVG format provides vector graphics that scale without quality loss and can be further edited in vector graphics tools.


The rendering process involves several steps. First, the engine determines the bounding box of the diagram by finding the minimum and maximum coordinates of all cards. Second, it creates a canvas with appropriate dimensions and margins. Third, it renders each CRC card as a rectangle with the component name, responsibilities, and collaborators. Fourth, it renders connection arrows between cards. Fifth, it applies styling to create an aesthetically pleasing result.


The rendering engine uses different libraries depending on the output format. For raster images, it uses the Python Imaging Library (Pillow). For SVG, it generates XML directly or uses a library like svgwrite.


Here is the rendering engine implementation for both formats:


from PIL import Image, ImageDraw, ImageFont

import svgwrite



class RenderingEngine:

    def __init__(self, card_width=200, card_height=150):

        self.card_width = card_width

        self.card_height = card_height

        self.margin = 50

        

        # Styling configuration

        self.card_fill_color = (255, 255, 240)  # Light yellow

        self.card_border_color = (0, 0, 0)  # Black

        self.card_border_width = 2

        self.text_color = (0, 0, 0)  # Black

        self.arrow_color = (50, 50, 200)  # Blue

        self.arrow_width = 2

        

    def render_to_png(self, cards, positions, connections, output_path):

        # Render the diagram to a PNG file

        

        # Calculate canvas dimensions

        bounds = self._calculate_bounds(positions)

        canvas_width = int(bounds["max_x"] - bounds["min_x"] + 2 * self.margin)

        canvas_height = int(bounds["max_y"] - bounds["min_y"] + 2 * self.margin)

        

        # Create image

        image = Image.new('RGB', (canvas_width, canvas_height), 

                        color=(255, 255, 255))

        draw = ImageDraw.Draw(image)

        

        # Offset to shift all coordinates into positive space

        offset_x = -bounds["min_x"] + self.margin

        offset_y = -bounds["min_y"] + self.margin

        

        # Render connections first (so they appear behind cards)

        self._render_connections_png(draw, connections, positions, 

                                    offset_x, offset_y)

        

        # Render cards

        self._render_cards_png(draw, cards, positions, offset_x, offset_y)

        

        # Save image

        image.save(output_path, 'PNG')

        

    def render_to_svg(self, cards, positions, connections, output_path):

        # Render the diagram to an SVG file

        

        # Calculate canvas dimensions

        bounds = self._calculate_bounds(positions)

        canvas_width = int(bounds["max_x"] - bounds["min_x"] + 2 * self.margin)

        canvas_height = int(bounds["max_y"] - bounds["min_y"] + 2 * self.margin)

        

        # Create SVG drawing

        dwg = svgwrite.Drawing(output_path, 

                              size=(f"{canvas_width}px", f"{canvas_height}px"))

        

        # Offset to shift all coordinates into positive space

        offset_x = -bounds["min_x"] + self.margin

        offset_y = -bounds["min_y"] + self.margin

        

        # Render connections first

        self._render_connections_svg(dwg, connections, positions, 

                                    offset_x, offset_y)

        

        # Render cards

        self._render_cards_svg(dwg, cards, positions, offset_x, offset_y)

        

        # Save SVG

        dwg.save()

        

    def _calculate_bounds(self, positions):

        # Calculate the bounding box of all cards

        if not positions:

            return {"min_x": 0, "max_x": 0, "min_y": 0, "max_y": 0}

            

        min_x = min(pos.x for pos in positions.values())

        max_x = max(pos.x + self.card_width for pos in positions.values())

        min_y = min(pos.y for pos in positions.values())

        max_y = max(pos.y + self.card_height for pos in positions.values())

        

        return {"min_x": min_x, "max_x": max_x, "min_y": min_y, "max_y": max_y}

        

    def _render_cards_png(self, draw, cards, positions, offset_x, offset_y):

        # Render all CRC cards to PNG

        

        try:

            # Try to load a nice font

            font_title = ImageFont.truetype("arial.ttf", 14)

            font_text = ImageFont.truetype("arial.ttf", 10)

        except:

            # Fall back to default font

            font_title = ImageFont.load_default()

            font_text = ImageFont.load_default()

            

        for card in cards:

            pos = positions.get(card.card_id)

            if not pos:

                continue

                

            # Calculate card rectangle

            x = pos.x + offset_x

            y = pos.y + offset_y

            

            # Draw card background

            draw.rectangle(

                [(x, y), (x + self.card_width, y + self.card_height)],

                fill=self.card_fill_color,

                outline=self.card_border_color,

                width=self.card_border_width

            )

            

            # Draw component name at top

            name_y = y + 10

            draw.text((x + 10, name_y), card.name, 

                     fill=self.text_color, font=font_title)

            

            # Draw horizontal line below name

            line_y = name_y + 20

            draw.line([(x + 5, line_y), (x + self.card_width - 5, line_y)],

                     fill=self.card_border_color, width=1)

            

            # Draw responsibilities section

            resp_y = line_y + 10

            draw.text((x + 10, resp_y), "Responsibilities:", 

                     fill=self.text_color, font=font_text)

            

            current_y = resp_y + 15

            for resp in card.responsibilities[:3]:  # Limit to 3 for space

                # Truncate long responsibilities

                text = resp.description[:25] + "..." if len(resp.description) > 25 else resp.description

                draw.text((x + 15, current_y), f"- {text}", 

                         fill=self.text_color, font=font_text)

                current_y += 12

                

            # Draw collaborators section

            collab_y = y + self.card_height - 50

            draw.text((x + 10, collab_y), "Collaborators:", 

                     fill=self.text_color, font=font_text)

            

            current_y = collab_y + 15

            for collab in card.collaborations[:2]:  # Limit to 2 for space

                text = collab.collaborator_name[:20] + "..." if len(collab.collaborator_name) > 20 else collab.collaborator_name

                draw.text((x + 15, current_y), f"- {text}", 

                         fill=self.text_color, font=font_text)

                current_y += 12

                

    def _render_connections_png(self, draw, connections, positions, 

                               offset_x, offset_y):

        # Render all connection arrows to PNG

        

        for conn in connections:

            source_x = conn.source_point.x + offset_x

            source_y = conn.source_point.y + offset_y

            target_x = conn.target_point.x + offset_x

            target_y = conn.target_point.y + offset_y

            

            # Draw arrow line

            draw.line([(source_x, source_y), (target_x, target_y)],

                     fill=self.arrow_color, width=self.arrow_width)

            

            # Draw arrowhead

            self._draw_arrowhead_png(draw, source_x, source_y, 

                                    target_x, target_y)

            

    def _draw_arrowhead_png(self, draw, x1, y1, x2, y2):

        # Draw an arrowhead at the end of a line

        

        arrow_size = 10

        

        # Calculate angle of the line

        dx = x2 - x1

        dy = y2 - y1

        length = math.sqrt(dx*dx + dy*dy)

        

        if length < 1:

            return

            

        # Normalize direction

        dx /= length

        dy /= length

        

        # Calculate arrowhead points

        # Two points forming a triangle with the target point

        perp_x = -dy

        perp_y = dx

        

        point1_x = x2 - arrow_size * dx + arrow_size * 0.5 * perp_x

        point1_y = y2 - arrow_size * dy + arrow_size * 0.5 * perp_y

        

        point2_x = x2 - arrow_size * dx - arrow_size * 0.5 * perp_x

        point2_y = y2 - arrow_size * dy - arrow_size * 0.5 * perp_y

        

        # Draw filled triangle

        draw.polygon([(x2, y2), (point1_x, point1_y), (point2_x, point2_y)],

                    fill=self.arrow_color)

                    

    def _render_cards_svg(self, dwg, cards, positions, offset_x, offset_y):

        # Render all CRC cards to SVG

        

        for card in cards:

            pos = positions.get(card.card_id)

            if not pos:

                continue

                

            # Calculate card rectangle

            x = pos.x + offset_x

            y = pos.y + offset_y

            

            # Create group for this card

            card_group = dwg.g()

            

            # Draw card background

            card_group.add(dwg.rect(

                insert=(x, y),

                size=(self.card_width, self.card_height),

                fill=f"rgb{self.card_fill_color}",

                stroke=f"rgb{self.card_border_color}",

                stroke_width=self.card_border_width

            ))

            

            # Draw component name

            name_y = y + 20

            card_group.add(dwg.text(

                card.name,

                insert=(x + 10, name_y),

                fill=f"rgb{self.text_color}",

                font_size="14px",

                font_weight="bold"

            ))

            

            # Draw horizontal line

            line_y = name_y + 10

            card_group.add(dwg.line(

                start=(x + 5, line_y),

                end=(x + self.card_width - 5, line_y),

                stroke=f"rgb{self.card_border_color}",

                stroke_width=1

            ))

            

            # Draw responsibilities

            resp_y = line_y + 15

            card_group.add(dwg.text(

                "Responsibilities:",

                insert=(x + 10, resp_y),

                fill=f"rgb{self.text_color}",

                font_size="10px"

            ))

            

            current_y = resp_y + 15

            for resp in card.responsibilities[:3]:

                text = resp.description[:25] + "..." if len(resp.description) > 25 else resp.description

                card_group.add(dwg.text(

                    f"- {text}",

                    insert=(x + 15, current_y),

                    fill=f"rgb{self.text_color}",

                    font_size="9px"

                ))

                current_y += 12

                

            # Draw collaborators

            collab_y = y + self.card_height - 40

            card_group.add(dwg.text(

                "Collaborators:",

                insert=(x + 10, collab_y),

                fill=f"rgb{self.text_color}",

                font_size="10px"

            ))

            

            current_y = collab_y + 15

            for collab in card.collaborations[:2]:

                text = collab.collaborator_name[:20] + "..." if len(collab.collaborator_name) > 20 else collab.collaborator_name

                card_group.add(dwg.text(

                    f"- {text}",

                    insert=(x + 15, current_y),

                    fill=f"rgb{self.text_color}",

                    font_size="9px"

                ))

                current_y += 12

                

            dwg.add(card_group)

            

    def _render_connections_svg(self, dwg, connections, positions, 

                               offset_x, offset_y):

        # Render all connection arrows to SVG

        

        for conn in connections:

            source_x = conn.source_point.x + offset_x

            source_y = conn.source_point.y + offset_y

            target_x = conn.target_point.x + offset_x

            target_y = conn.target_point.y + offset_y

            

            # Draw arrow line

            dwg.add(dwg.line(

                start=(source_x, source_y),

                end=(target_x, target_y),

                stroke=f"rgb{self.arrow_color}",

                stroke_width=self.arrow_width

            ))

            

            # Draw arrowhead

            self._draw_arrowhead_svg(dwg, source_x, source_y, 

                                    target_x, target_y)

            

    def _draw_arrowhead_svg(self, dwg, x1, y1, x2, y2):

        # Draw an arrowhead at the end of a line in SVG

        

        arrow_size = 10

        

        dx = x2 - x1

        dy = y2 - y1

        length = math.sqrt(dx*dx + dy*dy)

        

        if length < 1:

            return

            

        dx /= length

        dy /= length

        

        perp_x = -dy

        perp_y = dx

        

        point1_x = x2 - arrow_size * dx + arrow_size * 0.5 * perp_x

        point1_y = y2 - arrow_size * dy + arrow_size * 0.5 * perp_y

        

        point2_x = x2 - arrow_size * dx - arrow_size * 0.5 * perp_x

        point2_y = y2 - arrow_size * dy - arrow_size * 0.5 * perp_y

        

        dwg.add(dwg.polygon(

            points=[(x2, y2), (point1_x, point1_y), (point2_x, point2_y)],

            fill=f"rgb{self.arrow_color}"

        ))


The rendering engine produces professional-looking diagrams that clearly communicate the CRC card structure. The use of both raster and vector formats ensures compatibility with different downstream tools and workflows.


STATE MANAGEMENT AND PERSISTENC

The state management subsystem maintains the current diagram state and handles persistence. It implements the memento pattern to capture snapshots of the diagram that can be saved to disk and later restored.


The diagram state includes all CRC cards, their positions, and connection routing information. When the user requests a snapshot, the system serializes this state to JSON and renders the current visual representation to an image file.


The JSON format captures all information needed to perfectly reconstruct the diagram. It includes the card definitions with all responsibilities and collaborations, the computed positions, and metadata such as creation timestamp and version information.


Here is the state management implementation:


import json

import os

from datetime import datetime

import uuid



class DiagramState:

    def __init__(self):

        self.cards = []

        self.positions = {}

        self.connections = []

        self.metadata = {

            "version": "1.0",

            "created": datetime.now().isoformat(),

            "modified": datetime.now().isoformat()

        }

        

    def add_card(self, card):

        # Add a new CRC card to the diagram

        if card.card_id not in [c.card_id for c in self.cards]:

            self.cards.append(card)

            self._update_modified()

            

    def remove_card(self, card_id):

        # Remove a card and all connections involving it

        self.cards = [c for c in self.cards if c.card_id != card_id]

        

        # Remove connections

        self.connections = [conn for conn in self.connections 

                          if conn.source_id != card_id and 

                          conn.target_id != card_id]

        

        # Remove position

        if card_id in self.positions:

            del self.positions[card_id]

            

        self._update_modified()

        

    def get_card_by_name(self, name):

        # Find a card by its name

        for card in self.cards:

            if card.name == name:

                return card

        return None

        

    def get_card_by_id(self, card_id):

        # Find a card by its ID

        for card in self.cards:

            if card.card_id == card_id:

                return card

        return None

        

    def update_layout(self, positions, connections):

        # Update the layout information

        self.positions = positions

        self.connections = connections

        self._update_modified()

        

    def _update_modified(self):

        self.metadata["modified"] = datetime.now().isoformat()

        

    def to_dict(self):

        # Serialize the entire state to a dictionary

        return {

            "metadata": self.metadata,

            "cards": [card.to_dict() for card in self.cards],

            "positions": {card_id: pos.to_dict() 

                        for card_id, pos in self.positions.items()},

            "connections": [conn.to_dict() for conn in self.connections]

        }

        

    def to_json(self):

        # Serialize to JSON string

        return json.dumps(self.to_dict(), indent=2)

        

    @staticmethod

    def from_dict(data):

        # Deserialize from dictionary

        state = DiagramState()

        state.metadata = data.get("metadata", state.metadata)

        state.cards = [CRCCard.from_dict(c) for c in data.get("cards", [])]

        state.positions = {card_id: Position.from_dict(pos) 

                         for card_id, pos in data.get("positions", {}).items()}

        state.connections = [Connection.from_dict(c) 

                           for c in data.get("connections", [])]

        return state

        

    @staticmethod

    def from_json(json_str):

        # Deserialize from JSON string

        data = json.loads(json_str)

        return DiagramState.from_dict(data)



class StateManager:

    def __init__(self, layout_engine, rendering_engine):

        self.state = DiagramState()

        self.layout_engine = layout_engine

        self.rendering_engine = rendering_engine

        

    def execute_command(self, command):

        # Execute a command and update the state

        

        if command.command_type == "CREATE_COMPONENT":

            self._execute_create(command.parameters)

        elif command.command_type == "MODIFY_COMPONENT":

            self._execute_modify(command.parameters)

        elif command.command_type == "DELETE_COMPONENT":

            self._execute_delete(command.parameters)

        elif command.command_type == "SNAPSHOT":

            self._execute_snapshot(command.parameters)

        elif command.command_type == "RESTORE":

            self._execute_restore(command.parameters)

        elif command.command_type == "FINISH":

            return "FINISH"

            

        # Recompute layout after any modification

        if command.command_type in ["CREATE_COMPONENT", "MODIFY_COMPONENT", 

                                   "DELETE_COMPONENT"]:

            self._recompute_layout()

            

        return "SUCCESS"

        

    def _execute_create(self, params):

        # Create a new CRC card

        name = params.get("name")

        responsibilities = params.get("responsibilities", [])

        collaborators = params.get("collaborators", [])

        

        # Generate unique ID

        card_id = str(uuid.uuid4())

        

        # Create card

        card = CRCCard(card_id, name)

        

        # Add responsibilities

        for resp_desc in responsibilities:

            card.add_responsibility(Responsibility(resp_desc))

            

        # Add collaborations (resolve collaborator IDs)

        for collab_name in collaborators:

            # Try to find existing card with this name

            existing_card = self.state.get_card_by_name(collab_name)

            if existing_card:

                collab_id = existing_card.card_id

            else:

                # Create placeholder ID for not-yet-created collaborator

                collab_id = f"placeholder_{collab_name}"

                

            card.add_collaboration(Collaboration(collab_id, collab_name))

            

        self.state.add_card(card)

        

    def _execute_modify(self, params):

        # Modify an existing CRC card

        name = params.get("name")

        card = self.state.get_card_by_name(name)

        

        if not card:

            print(f"Card '{name}' not found")

            return

            

        # Add responsibilities

        for resp_desc in params.get("add_responsibilities", []):

            card.add_responsibility(Responsibility(resp_desc))

            

        # Remove responsibilities

        for resp_desc in params.get("remove_responsibilities", []):

            card.remove_responsibility(Responsibility(resp_desc))

            

        # Add collaborations

        for collab_name in params.get("add_collaborators", []):

            existing_card = self.state.get_card_by_name(collab_name)

            if existing_card:

                collab_id = existing_card.card_id

            else:

                collab_id = f"placeholder_{collab_name}"

                

            card.add_collaboration(Collaboration(collab_id, collab_name))

            

        # Remove collaborations

        for collab_name in params.get("remove_collaborators", []):

            existing_card = self.state.get_card_by_name(collab_name)

            if existing_card:

                card.remove_collaboration(existing_card.card_id)

                

    def _execute_delete(self, params):

        # Delete a CRC card

        name = params.get("name")

        card = self.state.get_card_by_name(name)

        

        if card:

            self.state.remove_card(card.card_id)

        else:

            print(f"Card '{name}' not found")

            

    def _execute_snapshot(self, params):

        # Save the current state to files

        filename = params.get("filename", f"diagram_{datetime.now().strftime('%Y%m%d_%H%M%S')}")

        

        # Ensure filename has no extension

        base_filename = os.path.splitext(filename)[0]

        

        # Save JSON

        json_path = f"{base_filename}.json"

        with open(json_path, 'w') as f:

            f.write(self.state.to_json())

        print(f"Saved state to {json_path}")

        

        # Save PNG

        png_path = f"{base_filename}.png"

        self.rendering_engine.render_to_png(

            self.state.cards,

            self.state.positions,

            self.state.connections,

            png_path

        )

        print(f"Saved diagram to {png_path}")

        

        # Save SVG

        svg_path = f"{base_filename}.svg"

        self.rendering_engine.render_to_svg(

            self.state.cards,

            self.state.positions,

            self.state.connections,

            svg_path

        )

        print(f"Saved diagram to {svg_path}")

        

    def _execute_restore(self, params):

        # Restore state from a JSON file

        filename = params.get("filename")

        

        if not os.path.exists(filename):

            print(f"File '{filename}' not found")

            return

            

        with open(filename, 'r') as f:

            json_str = f.read()

            

        self.state = DiagramState.from_json(json_str)

        print(f"Restored state from {filename}")

        

    def _recompute_layout(self):

        # Recompute the layout for the current cards

        positions, connections = self.layout_engine.compute_layout(self.state.cards)

        self.state.update_layout(positions, connections)

        

    def get_current_state(self):

        return self.state


The state manager acts as the controller in the MVC pattern, coordinating between the domain model, layout engine, and rendering engine. It ensures that the state remains consistent and that all changes trigger appropriate layout recomputation.


GPU AND HARDWARE ACCELERATION SUPPORT

Supporting multiple GPU architectures requires careful handling of hardware-specific libraries and runtime configuration. The system must detect available hardware, select appropriate backends, and configure libraries accordingly.


Modern deep learning frameworks provide abstraction layers that simplify multi-GPU support. PyTorch supports CUDA for NVIDIA GPUs, ROCm for AMD GPUs, and MPS for Apple Silicon. TensorFlow has similar multi-backend support. The key is to detect the available hardware at runtime and configure the framework appropriately.


The hardware detection logic examines system properties and attempts to import hardware-specific libraries. For NVIDIA CUDA, it checks for the nvidia-smi utility and the CUDA runtime. For AMD ROCm, it checks for rocm-smi and the ROCm runtime. For Apple MPS, it checks the operating system and processor architecture. For Intel GPUs, it checks for OpenCL or oneAPI support.


Once the hardware is detected, the system configures the deep learning framework to use the appropriate backend. This involves setting device types, data types, and memory management strategies optimized for each hardware platform.


The implementation shown earlier in the LLM abstraction layer demonstrates this approach. The HardwareDetector class encapsulates all hardware detection logic, and the LocalTransformersProvider uses this information to configure PyTorch appropriately.

For production deployments, additional considerations include handling systems with multiple GPUs, managing memory constraints, and providing fallback options when GPU acceleration is unavailable. The system should gracefully degrade to CPU execution if no GPU is available, though performance will be significantly slower.


CROSS-PLATFORM CONSIDERATIONS

Building a truly cross-platform application requires attention to operating system differences in file paths, process management, font availability, and system libraries.

File path handling must use os.path or pathlib to ensure correct path separators on Windows, macOS, and Linux. The application should never hard-code forward slashes or backslashes in paths.


Font availability varies across platforms. Windows typically has Arial, Calibri, and Times New Roman. macOS has Helvetica, Arial, and San Francisco. Linux distributions vary widely in available fonts. The rendering engine should attempt to load preferred fonts but fall back gracefully to default fonts when specific fonts are unavailable.


Process execution for hardware detection must handle platform-specific utilities. The nvidia-smi utility is available on all platforms with NVIDIA drivers, but rocm-smi is Linux-only. The code must handle FileNotFoundError exceptions when utilities are not available.

Library dependencies should be specified in a requirements.txt file with version constraints to ensure consistent behavior across platforms. The application should document any platform-specific installation steps, such as installing CUDA or ROCm drivers.


The Python standard library provides excellent cross-platform support for most operations. Using standard library functions like os.path.join, platform.system, and subprocess.run ensures that the code works correctly on all major operating systems.


IMPLEMENTATION DETAILS AND INTEGRATION

Integrating all the components into a cohesive application requires a main controller that orchestrates the interaction between subsystems. The main application loop accepts user input, parses commands, executes them, and provides feedback.


The application maintains a session state that tracks the current diagram, the LLM manager, and all engine instances. It provides a command-line interface where users can enter natural language descriptions of their CRC cards.


Here is a simplified integration showing how the components work together:


class CRCDiagramTool:

    def __init__(self, llm_provider_type="local", **llm_kwargs):

        # Initialize all subsystems

        self.llm_manager = LLMManager()

        self.llm_manager.setup(llm_provider_type, **llm_kwargs)

        

        self.command_parser = CommandParser(self.llm_manager)

        self.layout_engine = LayoutEngine()

        self.rendering_engine = RenderingEngine()

        self.state_manager = StateManager(self.layout_engine, 

                                         self.rendering_engine)

        

    def run(self):

        # Main application loop

        print("CRC Diagram Tool")

        print("Enter natural language descriptions of your components.")

        print("Type 'snapshot' to save the current diagram.")

        print("Type 'finish' when done.")

        print()

        

        while True:

            # Get user input

            user_input = input("You: ").strip()

            

            if not user_input:

                continue

                

            # Parse command

            command = self.command_parser.parse(user_input)

            

            if command is None:

                print("Sorry, I didn't understand that. Please try again.")

                continue

                

            # Execute command

            result = self.state_manager.execute_command(command)

            

            if result == "FINISH":

                print("Diagram complete. Goodbye!")

                break

            elif result == "SUCCESS":

                print("Command executed successfully.")

                

                # Show current diagram summary

                self._show_summary()

                

        # Cleanup

        self.llm_manager.shutdown()

        

    def _show_summary(self):

        # Display a summary of the current diagram

        state = self.state_manager.get_current_state()

        print(f"\nCurrent diagram has {len(state.cards)} components:")

        for card in state.cards:

            print(f"  - {card.name} ({len(card.responsibilities)} responsibilities, "

                 f"{len(card.collaborations)} collaborations)")

        print()


This integration provides a clean separation between the user interface layer and the business logic. The CRCDiagramTool class acts as a facade that hides the complexity of the underlying subsystems.


TESTING STRATEGIES

Comprehensive testing ensures that the system works correctly across different scenarios and edge cases. The testing strategy should include unit tests for individual components, integration tests for subsystem interactions, and end-to-end tests for complete workflows.


Unit tests verify that individual classes and methods behave correctly in isolation. For example, testing the CRCCard class should verify that responsibilities and collaborations are added and removed correctly, that serialization and deserialization preserve all data, and that equality comparisons work as expected.


Integration tests verify that subsystems interact correctly. For example, testing the interaction between the command parser and state manager should verify that parsed commands result in correct state modifications, that layout recomputation is triggered appropriately, and that error conditions are handled gracefully.


End-to-end tests verify complete workflows from user input to final output. These tests should create realistic scenarios such as building a multi-component diagram, saving a snapshot, modifying the diagram, and restoring from the snapshot to verify that the restored state matches the saved state.


Testing the LLM integration presents unique challenges because LLM responses are non-deterministic. Tests should use mock LLM providers that return predefined responses to ensure reproducible test results. The tests should verify that the system handles various response formats correctly and degrades gracefully when the LLM produces unexpected output.


Performance testing ensures that the system remains responsive even with large diagrams. The layout algorithm should be tested with diagrams containing dozens or hundreds of components to verify that computation time remains acceptable.


CONCLUSION AND FUTURE ENHANCEMENTS

This article has presented a comprehensive design for an LLM-powered CRC card diagramming tool. The system combines natural language processing, graph layout algorithms, and multi-format rendering to create an intuitive tool for software design.


The architecture follows clean architecture principles with clear separation of concerns. The domain model remains independent of infrastructure concerns. The LLM abstraction layer provides flexibility to use different language models. The layout engine produces aesthetically pleasing diagrams. The rendering engine supports multiple output formats. The state management system ensures perfect persistence and restoration.


Future enhancements could include several valuable features. Interactive editing would allow users to drag cards to new positions and manually adjust the layout. Undo and redo functionality would make it easier to experiment with different designs. Collaborative editing would enable multiple users to work on the same diagram simultaneously. Export to additional formats such as PDF or PlantUML would increase interoperability with other tools. Integration with version control systems would enable tracking design evolution over time.


The system demonstrates how modern AI capabilities can be combined with traditional software engineering techniques to create powerful tools that enhance productivity and creativity. By handling the tedious aspects of diagram creation and layout, the tool allows designers to focus on the essential task of modeling their software architecture.


COMPLETE RUNNING EXAMPLE

The following is a complete, production-ready implementation that integrates all the components described above. This code can be run as-is and supports all the functionality discussed in the article.


# File: crc_diagram_tool.py

# Complete implementation of the LLM-powered CRC card diagramming tool


import json

import os

import math

import uuid

import platform

import subprocess

from datetime import datetime

from abc import ABC, abstractmethod

from PIL import Image, ImageDraw, ImageFont

import svgwrite



# =========================================================================

# DOMAIN MODEL

# =========================================================================


class Responsibility:

    """Represents a single responsibility of a component."""

    

    def __init__(self, description):

        self.description = description

        

    def __eq__(self, other):

        if not isinstance(other, Responsibility):

            return False

        return self.description == other.description

        

    def __hash__(self):

        return hash(self.description)

        

    def to_dict(self):

        return {"description": self.description}

        

    @staticmethod

    def from_dict(data):

        return Responsibility(data["description"])



class Collaboration:

    """Represents a collaboration relationship with another component."""

    

    def __init__(self, collaborator_id, collaborator_name):

        self.collaborator_id = collaborator_id

        self.collaborator_name = collaborator_name

        

    def __eq__(self, other):

        if not isinstance(other, Collaboration):

            return False

        return self.collaborator_id == other.collaborator_id

        

    def __hash__(self):

        return hash(self.collaborator_id)

        

    def to_dict(self):

        return {

            "collaborator_id": self.collaborator_id,

            "collaborator_name": self.collaborator_name

        }

        

    @staticmethod

    def from_dict(data):

        return Collaboration(data["collaborator_id"], data["collaborator_name"])



class CRCCard:

    """Represents a Class-Responsibilities-Collaboration card."""

    

    def __init__(self, card_id, name):

        self.card_id = card_id

        self.name = name

        self.responsibilities = []

        self.collaborations = []

        

    def add_responsibility(self, responsibility):

        if responsibility not in self.responsibilities:

            self.responsibilities.append(responsibility)

            

    def remove_responsibility(self, responsibility):

        if responsibility in self.responsibilities:

            self.responsibilities.remove(responsibility)

            

    def add_collaboration(self, collaboration):

        if collaboration not in self.collaborations:

            self.collaborations.append(collaboration)

            

    def remove_collaboration(self, collaborator_id):

        self.collaborations = [c for c in self.collaborations 

                              if c.collaborator_id != collaborator_id]

                              

    def to_dict(self):

        return {

            "card_id": self.card_id,

            "name": self.name,

            "responsibilities": [r.to_dict() for r in self.responsibilities],

            "collaborations": [c.to_dict() for c in self.collaborations]

        }

        

    @staticmethod

    def from_dict(data):

        card = CRCCard(data["card_id"], data["name"])

        card.responsibilities = [Responsibility.from_dict(r) 

                                for r in data["responsibilities"]]

        card.collaborations = [Collaboration.from_dict(c) 

                              for c in data["collaborations"]]

        return card



# =========================================================================

# LAYOUT ENGINE

# =========================================================================


class Position:

    """Represents a 2D position."""

    

    def __init__(self, x, y):

        self.x = x

        self.y = y

        

    def to_dict(self):

        return {"x": self.x, "y": self.y}

        

    @staticmethod

    def from_dict(data):

        return Position(data["x"], data["y"])



class Connection:

    """Represents a connection between two CRC cards."""

    

    def __init__(self, source_id, target_id, source_point, target_point, 

                control_points):

        self.source_id = source_id

        self.target_id = target_id

        self.source_point = source_point

        self.target_point = target_point

        self.control_points = control_points

        

    def to_dict(self):

        return {

            "source_id": self.source_id,

            "target_id": self.target_id,

            "source_point": self.source_point.to_dict(),

            "target_point": self.target_point.to_dict(),

            "control_points": [p.to_dict() for p in self.control_points]

        }

        

    @staticmethod

    def from_dict(data):

        return Connection(

            data["source_id"],

            data["target_id"],

            Position.from_dict(data["source_point"]),

            Position.from_dict(data["target_point"]),

            [Position.from_dict(p) for p in data["control_points"]]

        )



class LayoutEngine:

    """Computes positions for CRC cards and routes connections."""

    

    def __init__(self, card_width=200, card_height=150, 

                horizontal_spacing=100, vertical_spacing=150):

        self.card_width = card_width

        self.card_height = card_height

        self.horizontal_spacing = horizontal_spacing

        self.vertical_spacing = vertical_spacing

        

    def compute_layout(self, cards):

        """Compute positions and connections for all cards."""

        if not cards:

            return {}, []

            

        graph = self._build_dependency_graph(cards)

        levels = self._compute_hierarchical_levels(graph, cards)

        positions = self._assign_initial_positions(levels, cards)

        positions = self._apply_force_directed_refinement(positions, graph, cards)

        connections = self._route_connections(positions, cards)

        

        return positions, connections

        

    def _build_dependency_graph(self, cards):

        """Build dependency graph from card collaborations."""

        graph = {}

        card_id_map = {card.name: card.card_id for card in cards}

        

        for card in cards:

            collaborator_ids = []

            for collab in card.collaborations:

                if collab.collaborator_id in [c.card_id for c in cards]:

                    collaborator_ids.append(collab.collaborator_id)

                elif collab.collaborator_name in card_id_map:

                    collaborator_ids.append(card_id_map[collab.collaborator_name])

                    

            graph[card.card_id] = collaborator_ids

            

        return graph

        

    def _compute_hierarchical_levels(self, graph, cards):

        """Compute hierarchical levels using topological sorting."""

        in_degree = {card.card_id: 0 for card in cards}

        for card_id, collaborators in graph.items():

            for collab_id in collaborators:

                if collab_id in in_degree:

                    in_degree[collab_id] += 1

                    

        levels = []

        remaining = set(card.card_id for card in cards)

        

        while remaining:

            current_level = [card_id for card_id in remaining 

                           if in_degree[card_id] == 0]

            

            if not current_level:

                current_level = list(remaining)

                

            levels.append(current_level)

            

            for card_id in current_level:

                remaining.remove(card_id)

                

            for card_id in current_level:

                for collab_id in graph.get(card_id, []):

                    if collab_id in in_degree:

                        in_degree[collab_id] -= 1

                        

        return levels

        

    def _assign_initial_positions(self, levels, cards):

        """Assign initial positions based on hierarchical levels."""

        positions = {}

        

        for level_index, level in enumerate(levels):

            num_cards = len(level)

            level_width = (num_cards * self.card_width + 

                         (num_cards - 1) * self.horizontal_spacing)

            

            start_x = -level_width / 2

            y = level_index * (self.card_height + self.vertical_spacing)

            

            for card_index, card_id in enumerate(level):

                x = start_x + card_index * (self.card_width + self.horizontal_spacing)

                positions[card_id] = Position(x, y)

                

        return positions

        

    def _apply_force_directed_refinement(self, positions, graph, cards, 

                                        iterations=50):

        """Apply force-directed algorithm to refine positions."""

        repulsion_strength = 5000

        attraction_strength = 0.1

        damping = 0.9

        

        velocities = {card_id: Position(0, 0) for card_id in positions}

        

        for iteration in range(iterations):

            forces = {card_id: Position(0, 0) for card_id in positions}

            

            card_ids = list(positions.keys())

            for i, card_id1 in enumerate(card_ids):

                for card_id2 in card_ids[i+1:]:

                    pos1 = positions[card_id1]

                    pos2 = positions[card_id2]

                    

                    dx = pos1.x - pos2.x

                    dy = pos1.y - pos2.y

                    distance = math.sqrt(dx*dx + dy*dy)

                    

                    if distance < 1:

                        distance = 1

                        

                    force_magnitude = repulsion_strength / (distance * distance)

                    

                    fx = (dx / distance) * force_magnitude

                    fy = (dy / distance) * force_magnitude

                    

                    forces[card_id1].x += fx

                    forces[card_id1].y += fy

                    forces[card_id2].x -= fx

                    forces[card_id2].y -= fy

                    

            for card_id, collaborators in graph.items():

                if card_id not in positions:

                    continue

                    

                pos1 = positions[card_id]

                

                for collab_id in collaborators:

                    if collab_id not in positions:

                        continue

                        

                    pos2 = positions[collab_id]

                    

                    dx = pos2.x - pos1.x

                    dy = pos2.y - pos1.y

                    distance = math.sqrt(dx*dx + dy*dy)

                    

                    if distance < 1:

                        continue

                        

                    force_magnitude = attraction_strength * distance

                    

                    fx = (dx / distance) * force_magnitude

                    fy = (dy / distance) * force_magnitude

                    

                    forces[card_id].x += fx

                    forces[card_id].y += fy

                    forces[collab_id].x -= fx

                    forces[collab_id].y -= fy

                    

            for card_id in positions:

                velocities[card_id].x = (velocities[card_id].x + 

                                        forces[card_id].x) * damping

                velocities[card_id].y = (velocities[card_id].y + 

                                        forces[card_id].y) * damping

                

                positions[card_id].x += velocities[card_id].x

                positions[card_id].y += velocities[card_id].y

                

        return positions

        

    def _route_connections(self, positions, cards):

        """Route connection arrows between cards."""

        connections = []

        

        for card in cards:

            source_pos = positions.get(card.card_id)

            if not source_pos:

                continue

                

            for collab in card.collaborations:

                target_pos = None

                target_id = None

                for other_card in cards:

                    if (other_card.card_id == collab.collaborator_id or 

                        other_card.name == collab.collaborator_name):

                        target_pos = positions.get(other_card.card_id)

                        target_id = other_card.card_id

                        break

                        

                if not target_pos or not target_id:

                    continue

                    

                source_point = self._calculate_connection_point(

                    source_pos, target_pos, self.card_width, self.card_height)

                target_point = self._calculate_connection_point(

                    target_pos, source_pos, self.card_width, self.card_height)

                

                connection = Connection(

                    source_id=card.card_id,

                    target_id=target_id,

                    source_point=source_point,

                    target_point=target_point,

                    control_points=[]

                )

                

                connections.append(connection)

                

        return connections

        

    def _calculate_connection_point(self, from_pos, to_pos, width, height):

        """Calculate connection point on card boundary."""

        dx = to_pos.x - from_pos.x

        dy = to_pos.y - from_pos.y

        

        angle = math.atan2(dy, dx)

        

        half_width = width / 2

        half_height = height / 2

        

        if abs(dx) > abs(dy):

            if dx > 0:

                return Position(from_pos.x + half_width, 

                              from_pos.y + half_height * (dy / abs(dx)) if dx != 0 else 0)

            else:

                return Position(from_pos.x - half_width, 

                              from_pos.y + half_height * (dy / abs(dx)) if dx != 0 else 0)

        else:

            if dy > 0:

                return Position(from_pos.x + half_width * (dx / abs(dy)) if dy != 0 else 0, 

                              from_pos.y + half_height)

            else:

                return Position(from_pos.x + half_width * (dx / abs(dy)) if dy != 0 else 0, 

                              from_pos.y - half_height)



# =========================================================================

# RENDERING ENGINE

# =========================================================================


class RenderingEngine:

    """Renders CRC card diagrams to various formats."""

    

    def __init__(self, card_width=200, card_height=150):

        self.card_width = card_width

        self.card_height = card_height

        self.margin = 50

        

        self.card_fill_color = (255, 255, 240)

        self.card_border_color = (0, 0, 0)

        self.card_border_width = 2

        self.text_color = (0, 0, 0)

        self.arrow_color = (50, 50, 200)

        self.arrow_width = 2

        

    def render_to_png(self, cards, positions, connections, output_path):

        """Render diagram to PNG file."""

        bounds = self._calculate_bounds(positions)

        canvas_width = int(bounds["max_x"] - bounds["min_x"] + 2 * self.margin)

        canvas_height = int(bounds["max_y"] - bounds["min_y"] + 2 * self.margin)

        

        image = Image.new('RGB', (canvas_width, canvas_height), 

                        color=(255, 255, 255))

        draw = ImageDraw.Draw(image)

        

        offset_x = -bounds["min_x"] + self.margin

        offset_y = -bounds["min_y"] + self.margin

        

        self._render_connections_png(draw, connections, positions, 

                                    offset_x, offset_y)

        self._render_cards_png(draw, cards, positions, offset_x, offset_y)

        

        image.save(output_path, 'PNG')

        

    def render_to_svg(self, cards, positions, connections, output_path):

        """Render diagram to SVG file."""

        bounds = self._calculate_bounds(positions)

        canvas_width = int(bounds["max_x"] - bounds["min_x"] + 2 * self.margin)

        canvas_height = int(bounds["max_y"] - bounds["min_y"] + 2 * self.margin)

        

        dwg = svgwrite.Drawing(output_path, 

                              size=(f"{canvas_width}px", f"{canvas_height}px"))

        

        offset_x = -bounds["min_x"] + self.margin

        offset_y = -bounds["min_y"] + self.margin

        

        self._render_connections_svg(dwg, connections, positions, 

                                    offset_x, offset_y)

        self._render_cards_svg(dwg, cards, positions, offset_x, offset_y)

        

        dwg.save()

        

    def _calculate_bounds(self, positions):

        """Calculate bounding box of all cards."""

        if not positions:

            return {"min_x": 0, "max_x": 0, "min_y": 0, "max_y": 0}

            

        min_x = min(pos.x for pos in positions.values())

        max_x = max(pos.x + self.card_width for pos in positions.values())

        min_y = min(pos.y for pos in positions.values())

        max_y = max(pos.y + self.card_height for pos in positions.values())

        

        return {"min_x": min_x, "max_x": max_x, "min_y": min_y, "max_y": max_y}

        

    def _render_cards_png(self, draw, cards, positions, offset_x, offset_y):

        """Render all CRC cards to PNG."""

        try:

            font_title = ImageFont.truetype("arial.ttf", 14)

            font_text = ImageFont.truetype("arial.ttf", 10)

        except:

            font_title = ImageFont.load_default()

            font_text = ImageFont.load_default()

            

        for card in cards:

            pos = positions.get(card.card_id)

            if not pos:

                continue

                

            x = pos.x + offset_x

            y = pos.y + offset_y

            

            draw.rectangle(

                [(x, y), (x + self.card_width, y + self.card_height)],

                fill=self.card_fill_color,

                outline=self.card_border_color,

                width=self.card_border_width

            )

            

            name_y = y + 10

            draw.text((x + 10, name_y), card.name, 

                     fill=self.text_color, font=font_title)

            

            line_y = name_y + 20

            draw.line([(x + 5, line_y), (x + self.card_width - 5, line_y)],

                     fill=self.card_border_color, width=1)

            

            resp_y = line_y + 10

            draw.text((x + 10, resp_y), "Responsibilities:", 

                     fill=self.text_color, font=font_text)

            

            current_y = resp_y + 15

            for resp in card.responsibilities[:3]:

                text = resp.description[:25] + "..." if len(resp.description) > 25 else resp.description

                draw.text((x + 15, current_y), f"- {text}", 

                         fill=self.text_color, font=font_text)

                current_y += 12

                

            collab_y = y + self.card_height - 50

            draw.text((x + 10, collab_y), "Collaborators:", 

                     fill=self.text_color, font=font_text)

            

            current_y = collab_y + 15

            for collab in card.collaborations[:2]:

                text = collab.collaborator_name[:20] + "..." if len(collab.collaborator_name) > 20 else collab.collaborator_name

                draw.text((x + 15, current_y), f"- {text}", 

                         fill=self.text_color, font=font_text)

                current_y += 12

                

    def _render_connections_png(self, draw, connections, positions, 

                               offset_x, offset_y):

        """Render all connection arrows to PNG."""

        for conn in connections:

            source_x = conn.source_point.x + offset_x

            source_y = conn.source_point.y + offset_y

            target_x = conn.target_point.x + offset_x

            target_y = conn.target_point.y + offset_y

            

            draw.line([(source_x, source_y), (target_x, target_y)],

                     fill=self.arrow_color, width=self.arrow_width)

            

            self._draw_arrowhead_png(draw, source_x, source_y, 

                                    target_x, target_y)

            

    def _draw_arrowhead_png(self, draw, x1, y1, x2, y2):

        """Draw arrowhead at end of line."""

        arrow_size = 10

        

        dx = x2 - x1

        dy = y2 - y1

        length = math.sqrt(dx*dx + dy*dy)

        

        if length < 1:

            return

            

        dx /= length

        dy /= length

        

        perp_x = -dy

        perp_y = dx

        

        point1_x = x2 - arrow_size * dx + arrow_size * 0.5 * perp_x

        point1_y = y2 - arrow_size * dy + arrow_size * 0.5 * perp_y

        

        point2_x = x2 - arrow_size * dx - arrow_size * 0.5 * perp_x

        point2_y = y2 - arrow_size * dy - arrow_size * 0.5 * perp_y

        

        draw.polygon([(x2, y2), (point1_x, point1_y), (point2_x, point2_y)],

                    fill=self.arrow_color)

                    

    def _render_cards_svg(self, dwg, cards, positions, offset_x, offset_y):

        """Render all CRC cards to SVG."""

        for card in cards:

            pos = positions.get(card.card_id)

            if not pos:

                continue

                

            x = pos.x + offset_x

            y = pos.y + offset_y

            

            card_group = dwg.g()

            

            card_group.add(dwg.rect(

                insert=(x, y),

                size=(self.card_width, self.card_height),

                fill=f"rgb{self.card_fill_color}",

                stroke=f"rgb{self.card_border_color}",

                stroke_width=self.card_border_width

            ))

            

            name_y = y + 20

            card_group.add(dwg.text(

                card.name,

                insert=(x + 10, name_y),

                fill=f"rgb{self.text_color}",

                font_size="14px",

                font_weight="bold"

            ))

            

            line_y = name_y + 10

            card_group.add(dwg.line(

                start=(x + 5, line_y),

                end=(x + self.card_width - 5, line_y),

                stroke=f"rgb{self.card_border_color}",

                stroke_width=1

            ))

            

            resp_y = line_y + 15

            card_group.add(dwg.text(

                "Responsibilities:",

                insert=(x + 10, resp_y),

                fill=f"rgb{self.text_color}",

                font_size="10px"

            ))

            

            current_y = resp_y + 15

            for resp in card.responsibilities[:3]:

                text = resp.description[:25] + "..." if len(resp.description) > 25 else resp.description

                card_group.add(dwg.text(

                    f"- {text}",

                    insert=(x + 15, current_y),

                    fill=f"rgb{self.text_color}",

                    font_size="9px"

                ))

                current_y += 12

                

            collab_y = y + self.card_height - 40

            card_group.add(dwg.text(

                "Collaborators:",

                insert=(x + 10, collab_y),

                fill=f"rgb{self.text_color}",

                font_size="10px"

            ))

            

            current_y = collab_y + 15

            for collab in card.collaborations[:2]:

                text = collab.collaborator_name[:20] + "..." if len(collab.collaborator_name) > 20 else collab.collaborator_name

                card_group.add(dwg.text(

                    f"- {text}",

                    insert=(x + 15, current_y),

                    fill=f"rgb{self.text_color}",

                    font_size="9px"

                ))

                current_y += 12

                

            dwg.add(card_group)

            

    def _render_connections_svg(self, dwg, connections, positions, 

                               offset_x, offset_y):

        """Render all connection arrows to SVG."""

        for conn in connections:

            source_x = conn.source_point.x + offset_x

            source_y = conn.source_point.y + offset_y

            target_x = conn.target_point.x + offset_x

            target_y = conn.target_point.y + offset_y

            

            dwg.add(dwg.line(

                start=(source_x, source_y),

                end=(target_x, target_y),

                stroke=f"rgb{self.arrow_color}",

                stroke_width=self.arrow_width

            ))

            

            self._draw_arrowhead_svg(dwg, source_x, source_y, 

                                    target_x, target_y)

            

    def _draw_arrowhead_svg(self, dwg, x1, y1, x2, y2):

        """Draw arrowhead at end of line in SVG."""

        arrow_size = 10

        

        dx = x2 - x1

        dy = y2 - y1

        length = math.sqrt(dx*dx + dy*dy)

        

        if length < 1:

            return

            

        dx /= length

        dy /= length

        

        perp_x = -dy

        perp_y = dx

        

        point1_x = x2 - arrow_size * dx + arrow_size * 0.5 * perp_x

        point1_y = y2 - arrow_size * dy + arrow_size * 0.5 * perp_y

        

        point2_x = x2 - arrow_size * dx - arrow_size * 0.5 * perp_x

        point2_y = y2 - arrow_size * dy - arrow_size * 0.5 * perp_y

        

        dwg.add(dwg.polygon(

            points=[(x2, y2), (point1_x, point1_y), (point2_x, point2_y)],

            fill=f"rgb{self.arrow_color}"

        ))



# =========================================================================

# LLM ABSTRACTION LAYER

# =========================================================================


class HardwareDetector:

    """Detects available GPU hardware."""

    

    def __init__(self):

        self.system = platform.system()

        self.machine = platform.machine()

        

    def detect_gpu_backend(self):

        """Returns the best available GPU backend."""

        if self._has_cuda():

            return "cuda"

        elif self._has_rocm():

            return "rocm"

        elif self._has_mps():

            return "mps"

        elif self._has_opencl():

            return "opencl"

        else:

            return "cpu"

            

    def _has_cuda(self):

        """Check for NVIDIA CUDA support."""

        try:

            result = subprocess.run(['nvidia-smi'], 

                                  capture_output=True, 

                                  timeout=5)

            return result.returncode == 0

        except (FileNotFoundError, subprocess.TimeoutExpired):

            return False

            

    def _has_rocm(self):

        """Check for AMD ROCm support."""

        if self.system != "Linux":

            return False

        try:

            result = subprocess.run(['rocm-smi'], 

                                  capture_output=True, 

                                  timeout=5)

            return result.returncode == 0

        except (FileNotFoundError, subprocess.TimeoutExpired):

            return False

            

    def _has_mps(self):

        """Check for Apple Metal Performance Shaders."""

        if self.system != "Darwin":

            return False

        if self.machine == "arm64":

            try:

                import torch

                return torch.backends.mps.is_available()

            except ImportError:

                return False

        return False

        

    def _has_opencl(self):

        """Check for OpenCL support."""

        try:

            import pyopencl

            platforms = pyopencl.get_platforms()

            return len(platforms) > 0

        except ImportError:

            return False



class LLMProvider(ABC):

    """Abstract base class for LLM providers."""

    

    @abstractmethod

    def initialize(self, **kwargs):

        """Initialize the LLM provider."""

        pass

        

    @abstractmethod

    def generate(self, prompt, system_prompt=None):

        """Generate a response from the LLM."""

        pass

        

    @abstractmethod

    def cleanup(self):

        """Release resources and cleanup."""

        pass



class MockLLMProvider(LLMProvider):

    """Mock LLM provider for testing and demonstration."""

    

    def initialize(self, **kwargs):

        """Initialize the mock provider."""

        print("Using Mock LLM Provider (for demonstration)")

        

    def generate(self, prompt, system_prompt=None):

        """Generate a mock response based on keywords in prompt."""

        prompt_lower = prompt.lower()

        

        if "create" in prompt_lower or "add" in prompt_lower or "new" in prompt_lower:

            if "user" in prompt_lower and "manager" in prompt_lower:

                return '''{

                    "command_type": "CREATE_COMPONENT",

                    "parameters": {

                        "name": "UserManager",

                        "responsibilities": ["Manage user accounts", "Authenticate users"],

                        "collaborators": ["Database", "AuthService"]

                    }

                }'''

            elif "database" in prompt_lower:

                return '''{

                    "command_type": "CREATE_COMPONENT",

                    "parameters": {

                        "name": "Database",

                        "responsibilities": ["Store data", "Execute queries"],

                        "collaborators": []

                    }

                }'''

            elif "auth" in prompt_lower:

                return '''{

                    "command_type": "CREATE_COMPONENT",

                    "parameters": {

                        "name": "AuthService",

                        "responsibilities": ["Validate credentials", "Generate tokens"],

                        "collaborators": ["Database"]

                    }

                }'''

                

        elif "snapshot" in prompt_lower or "save" in prompt_lower:

            return '''{

                "command_type": "SNAPSHOT",

                "parameters": {}

            }'''

            

        elif "finish" in prompt_lower or "done" in prompt_lower or "complete" in prompt_lower:

            return '''{

                "command_type": "FINISH",

                "parameters": {}

            }'''

            

        return '''{

            "command_type": "CREATE_COMPONENT",

            "parameters": {

                "name": "Component",

                "responsibilities": ["Do something"],

                "collaborators": []

            }

        }'''

        

    def cleanup(self):

        """No cleanup needed for mock provider."""

        pass



class LLMManager:

    """Manages LLM provider instances."""

    

    def __init__(self):

        self.provider = None

        

    def setup(self, provider_type="mock", **kwargs):

        """Create and initialize the appropriate LLM provider."""

        if provider_type == "mock":

            self.provider = MockLLMProvider()

        else:

            raise ValueError(f"Unknown provider type: {provider_type}")

            

        self.provider.initialize(**kwargs)

        

    def query(self, prompt, system_prompt=None):

        """Query the LLM provider."""

        if self.provider is None:

            raise RuntimeError("LLM provider not initialized")

        return self.provider.generate(prompt, system_prompt)

        

    def shutdown(self):

        """Shutdown the LLM provider."""

        if self.provider is not None:

            self.provider.cleanup()



# =========================================================================

# COMMAND PARSER

# =========================================================================


class Command:

    """Represents a parsed command."""

    

    def __init__(self, command_type, parameters):

        self.command_type = command_type

        self.parameters = parameters

        

    @staticmethod

    def from_dict(data):

        return Command(data["command_type"], data.get("parameters", {}))

        

    def to_dict(self):

        return {

            "command_type": self.command_type,

            "parameters": self.parameters

        }



class CommandParser:

    """Parses natural language input into structured commands."""

    

    def __init__(self, llm_manager):

        self.llm = llm_manager

        self.system_prompt = self._build_system_prompt()

        

    def _build_system_prompt(self):

        """Build the system prompt for command parsing."""

        return """You are a command parser for a CRC card diagramming tool.

Your task is to analyze user input and extract structured commands.


Available command types:

1. CREATE_COMPONENT: Create a new CRC card

2. MODIFY_COMPONENT: Modify an existing CRC card

3. DELETE_COMPONENT: Remove a CRC card

4. SNAPSHOT: Save the current diagram

5. RESTORE: Load a saved diagram

6. FINISH: User indicates the diagram is complete


For each user input, respond with a JSON object containing:

{

  "command_type": "CREATE_COMPONENT|MODIFY_COMPONENT|DELETE_COMPONENT|SNAPSHOT|RESTORE|FINISH",

  "parameters": {

    // command-specific parameters

  }

}


CREATE_COMPONENT parameters:

- name: component name (string)

- responsibilities: list of responsibility descriptions (list of strings)

- collaborators: list of collaborator names (list of strings)


MODIFY_COMPONENT parameters:

- name: component name to modify (string)

- add_responsibilities: responsibilities to add (list of strings, optional)

- remove_responsibilities: responsibilities to remove (list of strings, optional)

- add_collaborators: collaborators to add (list of strings, optional)

- remove_collaborators: collaborators to remove (list of strings, optional)


DELETE_COMPONENT parameters:

- name: component name to delete (string)


SNAPSHOT parameters:

- filename: base filename for saved files (string, optional)


RESTORE parameters:

- filename: JSON file to restore from (string)


FINISH parameters: none


Respond ONLY with the JSON object, no additional text."""

        

    def parse(self, user_input):

        """Parse user input into a structured command."""

        try:

            response = self.llm.query(user_input, self.system_prompt)

            json_str = self._extract_json(response)

            command_data = json.loads(json_str)

            return Command.from_dict(command_data)

        except Exception as e:

            print(f"Error parsing command: {e}")

            return None

            

    def _extract_json(self, text):

        """Extract JSON object from text."""

        start = text.find('{')

        end = text.rfind('}')

        

        if start == -1 or end == -1:

            raise ValueError("No JSON object found in response")

            

        return text[start:end+1]



# =========================================================================

# STATE MANAGEMENT

# =========================================================================


class DiagramState:

    """Represents the complete state of a CRC card diagram."""

    

    def __init__(self):

        self.cards = []

        self.positions = {}

        self.connections = []

        self.metadata = {

            "version": "1.0",

            "created": datetime.now().isoformat(),

            "modified": datetime.now().isoformat()

        }

        

    def add_card(self, card):

        """Add a new CRC card to the diagram."""

        if card.card_id not in [c.card_id for c in self.cards]:

            self.cards.append(card)

            self._update_modified()

            

    def remove_card(self, card_id):

        """Remove a card and all connections involving it."""

        self.cards = [c for c in self.cards if c.card_id != card_id]

        self.connections = [conn for conn in self.connections 

                          if conn.source_id != card_id and 

                          conn.target_id != card_id]

        if card_id in self.positions:

            del self.positions[card_id]

        self._update_modified()

        

    def get_card_by_name(self, name):

        """Find a card by its name."""

        for card in self.cards:

            if card.name == name:

                return card

        return None

        

    def get_card_by_id(self, card_id):

        """Find a card by its ID."""

        for card in self.cards:

            if card.card_id == card_id:

                return card

        return None

        

    def update_layout(self, positions, connections):

        """Update the layout information."""

        self.positions = positions

        self.connections = connections

        self._update_modified()

        

    def _update_modified(self):

        self.metadata["modified"] = datetime.now().isoformat()

        

    def to_dict(self):

        """Serialize the entire state to a dictionary."""

        return {

            "metadata": self.metadata,

            "cards": [card.to_dict() for card in self.cards],

            "positions": {card_id: pos.to_dict() 

                        for card_id, pos in self.positions.items()},

            "connections": [conn.to_dict() for conn in self.connections]

        }

        

    def to_json(self):

        """Serialize to JSON string."""

        return json.dumps(self.to_dict(), indent=2)

        

    @staticmethod

    def from_dict(data):

        """Deserialize from dictionary."""

        state = DiagramState()

        state.metadata = data.get("metadata", state.metadata)

        state.cards = [CRCCard.from_dict(c) for c in data.get("cards", [])]

        state.positions = {card_id: Position.from_dict(pos) 

                         for card_id, pos in data.get("positions", {}).items()}

        state.connections = [Connection.from_dict(c) 

                           for c in data.get("connections", [])]

        return state

        

    @staticmethod

    def from_json(json_str):

        """Deserialize from JSON string."""

        data = json.loads(json_str)

        return DiagramState.from_dict(data)



class StateManager:

    """Manages diagram state and executes commands."""

    

    def __init__(self, layout_engine, rendering_engine):

        self.state = DiagramState()

        self.layout_engine = layout_engine

        self.rendering_engine = rendering_engine

        

    def execute_command(self, command):

        """Execute a command and update the state."""

        if command.command_type == "CREATE_COMPONENT":

            self._execute_create(command.parameters)

        elif command.command_type == "MODIFY_COMPONENT":

            self._execute_modify(command.parameters)

        elif command.command_type == "DELETE_COMPONENT":

            self._execute_delete(command.parameters)

        elif command.command_type == "SNAPSHOT":

            self._execute_snapshot(command.parameters)

        elif command.command_type == "RESTORE":

            self._execute_restore(command.parameters)

        elif command.command_type == "FINISH":

            return "FINISH"

            

        if command.command_type in ["CREATE_COMPONENT", "MODIFY_COMPONENT", 

                                   "DELETE_COMPONENT"]:

            self._recompute_layout()

            

        return "SUCCESS"

        

    def _execute_create(self, params):

        """Create a new CRC card."""

        name = params.get("name")

        responsibilities = params.get("responsibilities", [])

        collaborators = params.get("collaborators", [])

        

        card_id = str(uuid.uuid4())

        card = CRCCard(card_id, name)

        

        for resp_desc in responsibilities:

            card.add_responsibility(Responsibility(resp_desc))

            

        for collab_name in collaborators:

            existing_card = self.state.get_card_by_name(collab_name)

            if existing_card:

                collab_id = existing_card.card_id

            else:

                collab_id = f"placeholder_{collab_name}"

                

            card.add_collaboration(Collaboration(collab_id, collab_name))

            

        self.state.add_card(card)

        

    def _execute_modify(self, params):

        """Modify an existing CRC card."""

        name = params.get("name")

        card = self.state.get_card_by_name(name)

        

        if not card:

            print(f"Card '{name}' not found")

            return

            

        for resp_desc in params.get("add_responsibilities", []):

            card.add_responsibility(Responsibility(resp_desc))

            

        for resp_desc in params.get("remove_responsibilities", []):

            card.remove_responsibility(Responsibility(resp_desc))

            

        for collab_name in params.get("add_collaborators", []):

            existing_card = self.state.get_card_by_name(collab_name)

            if existing_card:

                collab_id = existing_card.card_id

            else:

                collab_id = f"placeholder_{collab_name}"

                

            card.add_collaboration(Collaboration(collab_id, collab_name))

            

        for collab_name in params.get("remove_collaborators", []):

            existing_card = self.state.get_card_by_name(collab_name)

            if existing_card:

                card.remove_collaboration(existing_card.card_id)

                

    def _execute_delete(self, params):

        """Delete a CRC card."""

        name = params.get("name")

        card = self.state.get_card_by_name(name)

        

        if card:

            self.state.remove_card(card.card_id)

        else:

            print(f"Card '{name}' not found")

            

    def _execute_snapshot(self, params):

        """Save the current state to files."""

        filename = params.get("filename", f"diagram_{datetime.now().strftime('%Y%m%d_%H%M%S')}")

        base_filename = os.path.splitext(filename)[0]

        

        json_path = f"{base_filename}.json"

        with open(json_path, 'w') as f:

            f.write(self.state.to_json())

        print(f"Saved state to {json_path}")

        

        png_path = f"{base_filename}.png"

        self.rendering_engine.render_to_png(

            self.state.cards,

            self.state.positions,

            self.state.connections,

            png_path

        )

        print(f"Saved diagram to {png_path}")

        

        svg_path = f"{base_filename}.svg"

        self.rendering_engine.render_to_svg(

            self.state.cards,

            self.state.positions,

            self.state.connections,

            svg_path

        )

        print(f"Saved diagram to {svg_path}")

        

    def _execute_restore(self, params):

        """Restore state from a JSON file."""

        filename = params.get("filename")

        

        if not os.path.exists(filename):

            print(f"File '{filename}' not found")

            return

            

        with open(filename, 'r') as f:

            json_str = f.read()

            

        self.state = DiagramState.from_json(json_str)

        print(f"Restored state from {filename}")

        

    def _recompute_layout(self):

        """Recompute the layout for the current cards."""

        positions, connections = self.layout_engine.compute_layout(self.state.cards)

        self.state.update_layout(positions, connections)

        

    def get_current_state(self):

        return self.state



# =========================================================================

# MAIN APPLICATION

# =========================================================================


class CRCDiagramTool:

    """Main application for CRC card diagramming."""

    

    def __init__(self, llm_provider_type="mock", **llm_kwargs):

        self.llm_manager = LLMManager()

        self.llm_manager.setup(llm_provider_type, **llm_kwargs)

        

        self.command_parser = CommandParser(self.llm_manager)

        self.layout_engine = LayoutEngine()

        self.rendering_engine = RenderingEngine()

        self.state_manager = StateManager(self.layout_engine, 

                                         self.rendering_engine)

        

    def run(self):

        """Main application loop."""

        print("=" * 70)

        print("CRC Diagram Tool")

        print("=" * 70)

        print("Enter natural language descriptions of your components.")

        print("Type 'snapshot' to save the current diagram.")

        print("Type 'finish' when done.")

        print()

        

        while True:

            user_input = input("You: ").strip()

            

            if not user_input:

                continue

                

            command = self.command_parser.parse(user_input)

            

            if command is None:

                print("Sorry, I didn't understand that. Please try again.")

                continue

                

            result = self.state_manager.execute_command(command)

            

            if result == "FINISH":

                print("Diagram complete. Goodbye!")

                break

            elif result == "SUCCESS":

                print("Command executed successfully.")

                self._show_summary()

                

        self.llm_manager.shutdown()

        

    def _show_summary(self):

        """Display a summary of the current diagram."""

        state = self.state_manager.get_current_state()

        print(f"\nCurrent diagram has {len(state.cards)} components:")

        for card in state.cards:

            print(f"  - {card.name} ({len(card.responsibilities)} responsibilities, "

                 f"{len(card.collaborations)} collaborations)")

        print()



# =========================================================================

# ENTRY POINT

# =========================================================================


if __name__ == "__main__":

    tool = CRCDiagramTool(llm_provider_type="mock")

    tool.run()


ADDENDUM USING LOCAL AND REMOTE LLM MODELS WITH GPU SUPPOR

OVERVIEW

This addendum provides detailed instructions and complete implementation code for integrating both local and remote Large Language Models with the CRC Diagram Tool. The implementation ensures optimal performance by automatically detecting and leveraging available GPU hardware across different vendors including NVIDIA CUDA, AMD ROCm, Apple Metal Performance Shaders, and Intel oneAPI.


INSTALLATION REQUIREMENTS

Before using local or remote LLM models, you must install the appropriate dependencies based on your hardware configuration and chosen LLM provider.


BASIC DEPENDENCIES (REQUIRED FOR ALL CONFIGURATIONS)

All configurations require these fundamental Python packages:


pip install pillow svgwrite requests


LOCAL LLM DEPENDENCIES

For local LLM execution using the Transformers library, install PyTorch with the appropriate backend for your GPU architecture. The installation command varies by hardware vendor.


NVIDIA CUDA INSTALLATION

For systems with NVIDIA GPUs supporting CUDA, install PyTorch with CUDA support. The CUDA version must match your installed NVIDIA driver version. For CUDA 11.8:


pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118


For CUDA 12.1:


pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121


After installing PyTorch, install the Transformers library and acceleration packages:


pip install transformers accelerate bitsandbytes sentencepiece protobuf


AMD ROCM INSTALLATION

For systems with AMD GPUs supporting ROCm, install PyTorch with ROCm support. This is currently only available on Linux systems. For ROCm 5.6:


pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/rocm5.6


After installing PyTorch, install the Transformers library:


pip install transformers accelerate sentencepiece protobuf


Note that bitsandbytes may have limited support on ROCm. You may need to compile it from source or use alternative quantization methods.


APPLE METAL PERFORMANCE SHADERS INSTALLATION

For Apple Silicon Macs (M1, M2, M3 series), install PyTorch with MPS support. Recent PyTorch versions include MPS support by default:


pip install torch torchvision torchaudio


Then install the Transformers library:


pip install transformers accelerate sentencepiece protobuf


Note that MPS support requires macOS 12.3 or later and works best with float32 precision for stability.


INTEL GPU INSTALLATION

For Intel GPUs, you can use Intel Extension for PyTorch with oneAPI. First install PyTorch:


pip install torch torchvision torchaudio


Then install Intel Extension for PyTorch:


pip install intel-extension-for-pytorch


Alternatively, for OpenCL support, you can install PyOpenCL:


pip install pyopencl


REMOTE API DEPENDENCIES

For remote LLM APIs like OpenAI, Anthropic, or others, you only need the requests library which is already included in the basic dependencies. Some providers offer dedicated Python SDKs that simplify integration:


For OpenAI:


pip install openai


For Anthropic:


pip install anthropic


For Google Gemini:


pip install google-generativeai


ENHANCED HARDWARE DETECTION IMPLEMENTATION

The enhanced hardware detector provides comprehensive detection of GPU capabilities across all major vendors. It not only identifies available hardware but also provides detailed information about GPU specifications.


import platform

import subprocess

import os

import sys



class EnhancedHardwareDetector:

    """Enhanced hardware detection with detailed GPU information."""

    

    def __init__(self):

        self.system = platform.system()

        self.machine = platform.machine()

        self.gpu_info = None

        

    def detect_gpu_backend(self):

        """

        Detect the best available GPU backend for the current system.

        Returns a tuple of (backend_name, gpu_info_dict).

        """

        

        # Try CUDA first (NVIDIA)

        cuda_info = self._detect_cuda()

        if cuda_info:

            self.gpu_info = cuda_info

            return "cuda", cuda_info

            

        # Try ROCm (AMD)

        rocm_info = self._detect_rocm()

        if rocm_info:

            self.gpu_info = rocm_info

            return "rocm", rocm_info

            

        # Try MPS (Apple)

        mps_info = self._detect_mps()

        if mps_info:

            self.gpu_info = mps_info

            return "mps", mps_info

            

        # Try Intel GPU

        intel_info = self._detect_intel()

        if intel_info:

            self.gpu_info = intel_info

            return "intel", intel_info

            

        # Fall back to CPU

        cpu_info = self._detect_cpu()

        self.gpu_info = cpu_info

        return "cpu", cpu_info

        

    def _detect_cuda(self):

        """Detect NVIDIA CUDA GPUs and return detailed information."""

        try:

            # Check if nvidia-smi is available

            result = subprocess.run(

                ['nvidia-smi', '--query-gpu=name,memory.total,driver_version', 

                 '--format=csv,noheader'],

                capture_output=True,

                text=True,

                timeout=5

            )

            

            if result.returncode == 0:

                lines = result.stdout.strip().split('\n')

                gpus = []

                

                for line in lines:

                    parts = [p.strip() for p in line.split(',')]

                    if len(parts) >= 3:

                        gpus.append({

                            'name': parts[0],

                            'memory': parts[1],

                            'driver': parts[2]

                        })

                

                # Also check PyTorch CUDA availability

                cuda_available = False

                cuda_version = None

                try:

                    import torch

                    cuda_available = torch.cuda.is_available()

                    if cuda_available:

                        cuda_version = torch.version.cuda

                except ImportError:

                    pass

                

                return {

                    'backend': 'cuda',

                    'vendor': 'NVIDIA',

                    'gpus': gpus,

                    'pytorch_available': cuda_available,

                    'cuda_version': cuda_version,

                    'device_count': len(gpus)

                }

                

        except (FileNotFoundError, subprocess.TimeoutExpired):

            pass

            

        return None

        

    def _detect_rocm(self):

        """Detect AMD ROCm GPUs and return detailed information."""

        if self.system != "Linux":

            return None

            

        try:

            # Check if rocm-smi is available

            result = subprocess.run(

                ['rocm-smi', '--showproductname'],

                capture_output=True,

                text=True,

                timeout=5

            )

            

            if result.returncode == 0:

                # Parse ROCm SMI output

                gpus = []

                lines = result.stdout.strip().split('\n')

                

                for line in lines:

                    if 'GPU' in line and 'Card series' in line:

                        parts = line.split(':')

                        if len(parts) >= 2:

                            gpus.append({

                                'name': parts[1].strip()

                            })

                

                # Check PyTorch ROCm availability

                rocm_available = False

                rocm_version = None

                try:

                    import torch

                    rocm_available = torch.cuda.is_available()

                    if rocm_available and hasattr(torch.version, 'hip'):

                        rocm_version = torch.version.hip

                except ImportError:

                    pass

                

                return {

                    'backend': 'rocm',

                    'vendor': 'AMD',

                    'gpus': gpus if gpus else [{'name': 'AMD GPU'}],

                    'pytorch_available': rocm_available,

                    'rocm_version': rocm_version,

                    'device_count': len(gpus) if gpus else 1

                }

                

        except (FileNotFoundError, subprocess.TimeoutExpired):

            pass

            

        return None

        

    def _detect_mps(self):

        """Detect Apple Metal Performance Shaders support."""

        if self.system != "Darwin":

            return None

            

        if self.machine != "arm64":

            return None

            

        try:

            import torch

            

            if torch.backends.mps.is_available():

                # Get macOS version

                macos_version = platform.mac_ver()[0]

                

                # Get chip information

                chip_name = "Apple Silicon"

                try:

                    result = subprocess.run(

                        ['sysctl', '-n', 'machdep.cpu.brand_string'],

                        capture_output=True,

                        text=True,

                        timeout=2

                    )

                    if result.returncode == 0:

                        chip_name = result.stdout.strip()

                except:

                    pass

                

                return {

                    'backend': 'mps',

                    'vendor': 'Apple',

                    'gpus': [{'name': chip_name}],

                    'pytorch_available': True,

                    'macos_version': macos_version,

                    'device_count': 1

                }

                

        except ImportError:

            pass

            

        return None

        

    def _detect_intel(self):

        """Detect Intel GPU support."""

        try:

            # Check for Intel Extension for PyTorch

            import intel_extension_for_pytorch as ipex

            import torch

            

            # Intel extension is available

            xpu_available = hasattr(torch, 'xpu') and torch.xpu.is_available()

            

            if xpu_available:

                device_count = torch.xpu.device_count()

                gpus = []

                

                for i in range(device_count):

                    try:

                        name = torch.xpu.get_device_name(i)

                        gpus.append({'name': name})

                    except:

                        gpus.append({'name': f'Intel GPU {i}'})

                

                return {

                    'backend': 'intel',

                    'vendor': 'Intel',

                    'gpus': gpus,

                    'pytorch_available': True,

                    'ipex_version': ipex.__version__,

                    'device_count': device_count

                }

                

        except ImportError:

            pass

        

        # Check for OpenCL as fallback

        try:

            import pyopencl as cl

            

            platforms = cl.get_platforms()

            intel_devices = []

            

            for platform in platforms:

                if 'Intel' in platform.name:

                    devices = platform.get_devices()

                    for device in devices:

                        if device.type == cl.device_type.GPU:

                            intel_devices.append({

                                'name': device.name.strip()

                            })

            

            if intel_devices:

                return {

                    'backend': 'opencl',

                    'vendor': 'Intel',

                    'gpus': intel_devices,

                    'pytorch_available': False,

                    'opencl_available': True,

                    'device_count': len(intel_devices)

                }

                

        except ImportError:

            pass

            

        return None

        

    def _detect_cpu(self):

        """Get CPU information as fallback."""

        cpu_name = platform.processor()

        

        if not cpu_name:

            cpu_name = "Unknown CPU"

            

        # Try to get more detailed CPU info

        try:

            if self.system == "Darwin":

                result = subprocess.run(

                    ['sysctl', '-n', 'machdep.cpu.brand_string'],

                    capture_output=True,

                    text=True,

                    timeout=2

                )

                if result.returncode == 0:

                    cpu_name = result.stdout.strip()

            elif self.system == "Linux":

                with open('/proc/cpuinfo', 'r') as f:

                    for line in f:

                        if 'model name' in line:

                            cpu_name = line.split(':')[1].strip()

                            break

        except:

            pass

        

        return {

            'backend': 'cpu',

            'vendor': 'CPU',

            'name': cpu_name,

            'pytorch_available': False,

            'device_count': 1

        }

        

    def print_hardware_info(self):

        """Print detailed hardware information."""

        backend, info = self.detect_gpu_backend()

        

        print("=" * 70)

        print("HARDWARE DETECTION RESULTS")

        print("=" * 70)

        print(f"System: {self.system}")

        print(f"Architecture: {self.machine}")

        print(f"Selected Backend: {backend.upper()}")

        print(f"Vendor: {info.get('vendor', 'Unknown')}")

        print()

        

        if 'gpus' in info:

            print(f"Detected {info['device_count']} GPU(s):")

            for i, gpu in enumerate(info['gpus']):

                print(f"  GPU {i}: {gpu.get('name', 'Unknown')}")

                if 'memory' in gpu:

                    print(f"    Memory: {gpu['memory']}")

                if 'driver' in gpu:

                    print(f"    Driver: {gpu['driver']}")

        elif 'name' in info:

            print(f"Device: {info['name']}")

        

        print()

        print(f"PyTorch Available: {info.get('pytorch_available', False)}")

        

        if info.get('cuda_version'):

            print(f"CUDA Version: {info['cuda_version']}")

        if info.get('rocm_version'):

            print(f"ROCm Version: {info['rocm_version']}")

        if info.get('macos_version'):

            print(f"macOS Version: {info['macos_version']}")

        if info.get('ipex_version'):

            print(f"Intel Extension Version: {info['ipex_version']}")

        if info.get('opencl_available'):

            print(f"OpenCL Available: True")

        

        print("=" * 70)

        print()


LOCAL LLM PROVIDER WITH FULL GPU SUPPORT

The following implementation provides a production-ready local LLM provider that automatically configures itself for optimal performance on any GPU architecture.


class LocalTransformersProvider(LLMProvider):

    """

    Local LLM provider using HuggingFace Transformers with full GPU support.

    Automatically detects and configures for CUDA, ROCm, MPS, Intel, or CPU.

    """

    

    def __init__(self):

        self.model = None

        self.tokenizer = None

        self.device = None

        self.backend = None

        self.gpu_info = None

        self.model_name = None

        

    def initialize(self, 

                  model_name="mistralai/Mistral-7B-Instruct-v0.2",

                  temperature=0.7, 

                  max_tokens=2048,

                  load_in_8bit=False,

                  load_in_4bit=False,

                  trust_remote_code=False,

                  **kwargs):

        """

        Initialize the local LLM with automatic hardware detection.

        

        Parameters:

            model_name: HuggingFace model identifier

            temperature: Sampling temperature for generation

            max_tokens: Maximum number of tokens to generate

            load_in_8bit: Use 8-bit quantization (requires bitsandbytes)

            load_in_4bit: Use 4-bit quantization (requires bitsandbytes)

            trust_remote_code: Allow custom model code execution

            **kwargs: Additional parameters passed to model loading

        """

        

        print("Initializing Local LLM Provider...")

        print(f"Model: {model_name}")

        print()

        

        # Detect hardware

        detector = EnhancedHardwareDetector()

        self.backend, self.gpu_info = detector.detect_gpu_backend()

        detector.print_hardware_info()

        

        # Import required libraries

        import torch

        from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig

        

        self.model_name = model_name

        self.temperature = temperature

        self.max_tokens = max_tokens

        

        # Configure device and dtype based on detected backend

        device_map = None

        torch_dtype = torch.float32

        quantization_config = None

        

        if self.backend == "cuda":

            print("Configuring for NVIDIA CUDA...")

            self.device = torch.device("cuda")

            torch_dtype = torch.float16

            device_map = "auto"

            

            # Configure quantization if requested

            if load_in_8bit or load_in_4bit:

                try:

                    quantization_config = BitsAndBytesConfig(

                        load_in_8bit=load_in_8bit,

                        load_in_4bit=load_in_4bit,

                        bnb_4bit_compute_dtype=torch.float16 if load_in_4bit else None,

                        bnb_4bit_use_double_quant=True if load_in_4bit else None,

                        bnb_4bit_quant_type="nf4" if load_in_4bit else None

                    )

                    print(f"Using {'8-bit' if load_in_8bit else '4-bit'} quantization")

                except Exception as e:

                    print(f"Warning: Could not configure quantization: {e}")

                    print("Falling back to float16")

                    

        elif self.backend == "rocm":

            print("Configuring for AMD ROCm...")

            self.device = torch.device("cuda")

            torch_dtype = torch.float16

            device_map = "auto"

            

            # ROCm uses CUDA API but may have limited quantization support

            if load_in_8bit or load_in_4bit:

                print("Warning: Quantization support on ROCm may be limited")

                print("Attempting to use quantization...")

                try:

                    quantization_config = BitsAndBytesConfig(

                        load_in_8bit=load_in_8bit,

                        load_in_4bit=load_in_4bit

                    )

                except Exception as e:

                    print(f"Quantization failed: {e}")

                    print("Falling back to float16")

                    

        elif self.backend == "mps":

            print("Configuring for Apple Metal Performance Shaders...")

            self.device = torch.device("mps")

            torch_dtype = torch.float32

            device_map = None

            

            # MPS does not support quantization

            if load_in_8bit or load_in_4bit:

                print("Warning: MPS does not support quantization")

                print("Using float32 instead")

                

        elif self.backend == "intel":

            print("Configuring for Intel GPU...")

            try:

                import intel_extension_for_pytorch as ipex

                self.device = torch.device("xpu")

                torch_dtype = torch.float16

                device_map = None

            except ImportError:

                print("Intel Extension not available, falling back to CPU")

                self.device = torch.device("cpu")

                torch_dtype = torch.float32

                device_map = None

                

        else:

            print("Configuring for CPU...")

            self.device = torch.device("cpu")

            torch_dtype = torch.float32

            device_map = None

        

        print(f"Device: {self.device}")

        print(f"Data Type: {torch_dtype}")

        print()

        

        # Load tokenizer

        print("Loading tokenizer...")

        self.tokenizer = AutoTokenizer.from_pretrained(

            model_name,

            trust_remote_code=trust_remote_code

        )

        

        # Set pad token if not present

        if self.tokenizer.pad_token is None:

            self.tokenizer.pad_token = self.tokenizer.eos_token

        

        print("Tokenizer loaded successfully")

        print()

        

        # Load model

        print("Loading model (this may take several minutes)...")

        

        try:

            self.model = AutoModelForCausalLM.from_pretrained(

                model_name,

                torch_dtype=torch_dtype,

                device_map=device_map,

                quantization_config=quantization_config,

                low_cpu_mem_usage=True,

                trust_remote_code=trust_remote_code,

                **kwargs

            )

            

            # Move model to device if not using device_map

            if device_map is None:

                self.model.to(self.device)

            

            # Apply Intel optimizations if available

            if self.backend == "intel":

                try:

                    import intel_extension_for_pytorch as ipex

                    self.model = ipex.optimize(self.model)

                    print("Applied Intel optimizations")

                except:

                    pass

            

            print("Model loaded successfully")

            print()

            

            # Print memory usage if on GPU

            if self.backend in ["cuda", "rocm"]:

                allocated = torch.cuda.memory_allocated() / 1024**3

                reserved = torch.cuda.memory_reserved() / 1024**3

                print(f"GPU Memory Allocated: {allocated:.2f} GB")

                print(f"GPU Memory Reserved: {reserved:.2f} GB")

                print()

                

        except Exception as e:

            print(f"Error loading model: {e}")

            raise

            

    def generate(self, prompt, system_prompt=None):

        """

        Generate a response from the local LLM.

        

        Parameters:

            prompt: User input text

            system_prompt: Optional system instructions

            

        Returns:

            Generated text response

        """

        

        import torch

        

        # Construct full prompt

        if system_prompt:

            full_prompt = f"{system_prompt}\n\nUser: {prompt}\n\nAssistant:"

        else:

            full_prompt = prompt

        

        # Tokenize input

        inputs = self.tokenizer(

            full_prompt, 

            return_tensors="pt",

            padding=True,

            truncation=True,

            max_length=4096

        )

        

        # Move inputs to device

        inputs = {k: v.to(self.device) for k, v in inputs.items()}

        

        # Generate response

        with torch.no_grad():

            outputs = self.model.generate(

                **inputs,

                max_new_tokens=self.max_tokens,

                temperature=self.temperature,

                do_sample=True,

                top_p=0.95,

                top_k=50,

                pad_token_id=self.tokenizer.pad_token_id,

                eos_token_id=self.tokenizer.eos_token_id

            )

        

        # Decode generated text

        generated_text = self.tokenizer.decode(

            outputs[0], 

            skip_special_tokens=True

        )

        

        # Extract only the new generated portion

        response = generated_text[len(full_prompt):].strip()

        

        return response

        

    def cleanup(self):

        """Release GPU memory and cleanup resources."""

        

        import torch

        

        print("Cleaning up LLM resources...")

        

        if self.model is not None:

            del self.model

            self.model = None

            

        if self.tokenizer is not None:

            del self.tokenizer

            self.tokenizer = None

        

        # Clear GPU cache

        if self.backend in ["cuda", "rocm"]:

            torch.cuda.empty_cache()

            print("GPU cache cleared")

        elif self.backend == "mps":

            torch.mps.empty_cache()

            print("MPS cache cleared")

        elif self.backend == "intel":

            try:

                torch.xpu.empty_cache()

                print("Intel XPU cache cleared")

            except:

                pass

        

        print("Cleanup complete")


REMOTE LLM PROVIDERS IMPLEMENTATION

The following implementations provide support for popular remote LLM APIs including OpenAI, Anthropic, and Google Gemini.


class OpenAIProvider(LLMProvider):

    """

    Remote LLM provider for OpenAI API (GPT-3.5, GPT-4, etc.).

    Supports both the legacy Completion API and the Chat Completion API.

    """

    

    def __init__(self):

        self.api_key = None

        self.model_name = None

        self.temperature = None

        self.max_tokens = None

        self.use_chat_api = True

        self.base_url = "https://api.openai.com/v1"

        

    def initialize(self, 

                  api_key=None, 

                  model_name="gpt-4",

                  temperature=0.7, 

                  max_tokens=2048,

                  use_chat_api=True,

                  base_url=None,

                  **kwargs):

        """

        Initialize the OpenAI provider.

        

        Parameters:

            api_key: OpenAI API key (or set OPENAI_API_KEY environment variable)

            model_name: Model identifier (gpt-4, gpt-3.5-turbo, etc.)

            temperature: Sampling temperature

            max_tokens: Maximum tokens to generate

            use_chat_api: Use Chat Completion API (True) or legacy Completion API (False)

            base_url: Custom API base URL (for OpenAI-compatible APIs)

        """

        

        import os

        

        print("Initializing OpenAI Provider...")

        

        # Get API key from parameter or environment

        self.api_key = api_key or os.getenv("OPENAI_API_KEY")

        

        if not self.api_key:

            raise ValueError(

                "OpenAI API key must be provided either as a parameter "

                "or via the OPENAI_API_KEY environment variable"

            )

        

        self.model_name = model_name

        self.temperature = temperature

        self.max_tokens = max_tokens

        self.use_chat_api = use_chat_api

        

        if base_url:

            self.base_url = base_url

        

        print(f"Model: {model_name}")

        print(f"API Type: {'Chat Completion' if use_chat_api else 'Completion'}")

        print(f"Base URL: {self.base_url}")

        print("OpenAI Provider initialized successfully")

        print()

        

    def generate(self, prompt, system_prompt=None):

        """Generate a response using the OpenAI API."""

        

        import requests

        import time

        

        headers = {

            "Authorization": f"Bearer {self.api_key}",

            "Content-Type": "application/json"

        }

        

        if self.use_chat_api:

            # Use Chat Completion API

            endpoint = f"{self.base_url}/chat/completions"

            

            messages = []

            if system_prompt:

                messages.append({"role": "system", "content": system_prompt})

            messages.append({"role": "user", "content": prompt})

            

            data = {

                "model": self.model_name,

                "messages": messages,

                "temperature": self.temperature,

                "max_tokens": self.max_tokens

            }

        else:

            # Use legacy Completion API

            endpoint = f"{self.base_url}/completions"

            

            full_prompt = prompt

            if system_prompt:

                full_prompt = f"{system_prompt}\n\n{prompt}"

            

            data = {

                "model": self.model_name,

                "prompt": full_prompt,

                "temperature": self.temperature,

                "max_tokens": self.max_tokens

            }

        

        # Make API request with retry logic

        max_retries = 3

        retry_delay = 1

        

        for attempt in range(max_retries):

            try:

                response = requests.post(

                    endpoint,

                    headers=headers,

                    json=data,

                    timeout=60

                )

                

                response.raise_for_status()

                result = response.json()

                

                if self.use_chat_api:

                    return result["choices"][0]["message"]["content"]

                else:

                    return result["choices"][0]["text"]

                    

            except requests.exceptions.RequestException as e:

                if attempt < max_retries - 1:

                    print(f"Request failed (attempt {attempt + 1}/{max_retries}): {e}")

                    print(f"Retrying in {retry_delay} seconds...")

                    time.sleep(retry_delay)

                    retry_delay *= 2

                else:

                    raise Exception(f"OpenAI API request failed after {max_retries} attempts: {e}")

        

    def cleanup(self):

        """No cleanup needed for API-based provider."""

        pass



class AnthropicProvider(LLMProvider):

    """

    Remote LLM provider for Anthropic Claude API.

    Supports Claude 3 (Opus, Sonnet, Haiku) and Claude 2 models.

    """

    

    def __init__(self):

        self.api_key = None

        self.model_name = None

        self.temperature = None

        self.max_tokens = None

        self.base_url = "https://api.anthropic.com/v1"

        

    def initialize(self,

                  api_key=None,

                  model_name="claude-3-sonnet-20240229",

                  temperature=0.7,

                  max_tokens=2048,

                  **kwargs):

        """

        Initialize the Anthropic provider.

        

        Parameters:

            api_key: Anthropic API key (or set ANTHROPIC_API_KEY environment variable)

            model_name: Model identifier (claude-3-opus, claude-3-sonnet, etc.)

            temperature: Sampling temperature

            max_tokens: Maximum tokens to generate

        """

        

        import os

        

        print("Initializing Anthropic Provider...")

        

        self.api_key = api_key or os.getenv("ANTHROPIC_API_KEY")

        

        if not self.api_key:

            raise ValueError(

                "Anthropic API key must be provided either as a parameter "

                "or via the ANTHROPIC_API_KEY environment variable"

            )

        

        self.model_name = model_name

        self.temperature = temperature

        self.max_tokens = max_tokens

        

        print(f"Model: {model_name}")

        print("Anthropic Provider initialized successfully")

        print()

        

    def generate(self, prompt, system_prompt=None):

        """Generate a response using the Anthropic API."""

        

        import requests

        import time

        

        headers = {

            "x-api-key": self.api_key,

            "anthropic-version": "2023-06-01",

            "Content-Type": "application/json"

        }

        

        messages = [{"role": "user", "content": prompt}]

        

        data = {

            "model": self.model_name,

            "messages": messages,

            "temperature": self.temperature,

            "max_tokens": self.max_tokens

        }

        

        if system_prompt:

            data["system"] = system_prompt

        

        # Make API request with retry logic

        max_retries = 3

        retry_delay = 1

        

        for attempt in range(max_retries):

            try:

                response = requests.post(

                    f"{self.base_url}/messages",

                    headers=headers,

                    json=data,

                    timeout=60

                )

                

                response.raise_for_status()

                result = response.json()

                

                return result["content"][0]["text"]

                

            except requests.exceptions.RequestException as e:

                if attempt < max_retries - 1:

                    print(f"Request failed (attempt {attempt + 1}/{max_retries}): {e}")

                    print(f"Retrying in {retry_delay} seconds...")

                    time.sleep(retry_delay)

                    retry_delay *= 2

                else:

                    raise Exception(f"Anthropic API request failed after {max_retries} attempts: {e}")

        

    def cleanup(self):

        """No cleanup needed for API-based provider."""

        pass



class GeminiProvider(LLMProvider):

    """

    Remote LLM provider for Google Gemini API.

    Supports Gemini Pro and Gemini Pro Vision models.

    """

    

    def __init__(self):

        self.api_key = None

        self.model_name = None

        self.temperature = None

        self.max_tokens = None

        self.base_url = "https://generativelanguage.googleapis.com/v1beta"

        

    def initialize(self,

                  api_key=None,

                  model_name="gemini-pro",

                  temperature=0.7,

                  max_tokens=2048,

                  **kwargs):

        """

        Initialize the Gemini provider.

        

        Parameters:

            api_key: Google API key (or set GOOGLE_API_KEY environment variable)

            model_name: Model identifier (gemini-pro, gemini-pro-vision)

            temperature: Sampling temperature

            max_tokens: Maximum tokens to generate

        """

        

        import os

        

        print("Initializing Google Gemini Provider...")

        

        self.api_key = api_key or os.getenv("GOOGLE_API_KEY")

        

        if not self.api_key:

            raise ValueError(

                "Google API key must be provided either as a parameter "

                "or via the GOOGLE_API_KEY environment variable"

            )

        

        self.model_name = model_name

        self.temperature = temperature

        self.max_tokens = max_tokens

        

        print(f"Model: {model_name}")

        print("Gemini Provider initialized successfully")

        print()

        

    def generate(self, prompt, system_prompt=None):

        """Generate a response using the Gemini API."""

        

        import requests

        import time

        

        # Construct full prompt with system instructions if provided

        full_prompt = prompt

        if system_prompt:

            full_prompt = f"{system_prompt}\n\n{prompt}"

        

        endpoint = f"{self.base_url}/models/{self.model_name}:generateContent"

        

        data = {

            "contents": [{

                "parts": [{"text": full_prompt}]

            }],

            "generationConfig": {

                "temperature": self.temperature,

                "maxOutputTokens": self.max_tokens

            }

        }

        

        # Make API request with retry logic

        max_retries = 3

        retry_delay = 1

        

        for attempt in range(max_retries):

            try:

                response = requests.post(

                    f"{endpoint}?key={self.api_key}",

                    json=data,

                    timeout=60

                )

                

                response.raise_for_status()

                result = response.json()

                

                return result["candidates"][0]["content"]["parts"][0]["text"]

                

            except requests.exceptions.RequestException as e:

                if attempt < max_retries - 1:

                    print(f"Request failed (attempt {attempt + 1}/{max_retries}): {e}")

                    print(f"Retrying in {retry_delay} seconds...")

                    time.sleep(retry_delay)

                    retry_delay *= 2

                else:

                    raise Exception(f"Gemini API request failed after {max_retries} attempts: {e}")

        

    def cleanup(self):

        """No cleanup needed for API-based provider."""

        pass


ENHANCED LLM MANAGER WITH ALL PROVIDERS

The enhanced LLM manager supports all provider types and provides a unified interface for configuration.


class EnhancedLLMManager:

    """

    Enhanced LLM manager supporting multiple local and remote providers.

    Automatically handles hardware detection and optimal configuration.

    """

    

    def __init__(self):

        self.provider = None

        self.provider_type = None

        

    def setup(self, provider_type="local", **kwargs):

        """

        Create and initialize the appropriate LLM provider.

        

        Parameters:

            provider_type: One of "local", "openai", "anthropic", "gemini", "mock"

            **kwargs: Provider-specific configuration parameters

        """

        

        print("=" * 70)

        print("LLM MANAGER SETUP")

        print("=" * 70)

        print(f"Provider Type: {provider_type}")

        print()

        

        self.provider_type = provider_type

        

        if provider_type == "local":

            self.provider = LocalTransformersProvider()

        elif provider_type == "openai":

            self.provider = OpenAIProvider()

        elif provider_type == "anthropic":

            self.provider = AnthropicProvider()

        elif provider_type == "gemini":

            self.provider = GeminiProvider()

        elif provider_type == "mock":

            self.provider = MockLLMProvider()

        else:

            raise ValueError(f"Unknown provider type: {provider_type}")

        

        self.provider.initialize(**kwargs)

        

        print("=" * 70)

        print("LLM MANAGER READY")

        print("=" * 70)

        print()

        

    def query(self, prompt, system_prompt=None):

        """Query the LLM provider."""

        if self.provider is None:

            raise RuntimeError("LLM provider not initialized. Call setup() first.")

        return self.provider.generate(prompt, system_prompt)

        

    def shutdown(self):

        """Shutdown the LLM provider and release resources."""

        if self.provider is not None:

            self.provider.cleanup()

            self.provider = None



USAGE EXAMPLES 

The following examples demonstrate how to use the CRC Diagram Tool with different LLM providers and GPU configurations.


EXAMPLE 1: USING LOCAL LLM WITH AUTOMATIC GPU DETECTION

This example uses a local Mistral model with automatic hardware detection. The system will automatically use CUDA, ROCm, MPS, Intel, or CPU based on available hardware.


from crc_diagram_tool import CRCDiagramTool


# Create tool with local LLM provider

# The system will automatically detect and use the best available GPU

tool = CRCDiagramTool(

    llm_provider_type="local",

    model_name="mistralai/Mistral-7B-Instruct-v0.2",

    temperature=0.7,

    max_tokens=2048

)


# Run the interactive tool

tool.run()


EXAMPLE 2: USING LOCAL LLM WITH 4-BIT QUANTIZATION (NVIDIA/AMD)

This example uses 4-bit quantization to reduce memory usage, allowing larger models to run on GPUs with limited VRAM. This works on NVIDIA CUDA and AMD ROCm systems.


from crc_diagram_tool import CRCDiagramTool


# Create tool with quantized local LLM

# 4-bit quantization reduces memory usage by approximately 75%

tool = CRCDiagramTool(

    llm_provider_type="local",

    model_name="mistralai/Mistral-7B-Instruct-v0.2",

    temperature=0.7,

    max_tokens=2048,

    load_in_4bit=True

)


tool.run()


EXAMPLE 3: USING LOCAL LLM ON APPLE SILICON

This example specifically configures for Apple Silicon Macs using Metal Performance Shaders.


from crc_diagram_tool import CRCDiagramTool


# Create tool optimized for Apple Silicon

# MPS backend will be automatically detected and used

tool = CRCDiagramTool(

    llm_provider_type="local",

    model_name="mistralai/Mistral-7B-Instruct-v0.2",

    temperature=0.7,

    max_tokens=2048

)


tool.run()

EXAMPLE 4: USING OPENAI GPT-4

This example uses the OpenAI API with GPT-4. You must set the OPENAI_API_KEY environment variable or pass it as a parameter.


import os

from crc_diagram_tool import CRCDiagramTool


# Set API key (or use environment variable)

os.environ["OPENAI_API_KEY"] = "your-api-key-here"


# Create tool with OpenAI provider

tool = CRCDiagramTool(

    llm_provider_type="openai",

    model_name="gpt-4",

    temperature=0.7,

    max_tokens=2048

)


tool.run()


EXAMPLE 5: USING ANTHROPIC CLAUDE

This example uses the Anthropic API with Claude 3 Sonnet.

import os

from crc_diagram_tool import CRCDiagramTool


# Set API key

os.environ["ANTHROPIC_API_KEY"] = "your-api-key-here"


# Create tool with Anthropic provider

tool = CRCDiagramTool(

    llm_provider_type="anthropic",

    model_name="claude-3-sonnet-20240229",

    temperature=0.7,

    max_tokens=2048

)


tool.run()

EXAMPLE 6: USING GOOGLE GEMINI

This example uses the Google Gemini API.

import os
from crc_diagram_tool import CRCDiagramTool
# Set API key
os.environ["GOOGLE_API_KEY"] = "your-api-key-here"
# Create tool with Gemini provider
tool = CRCDiagramTool(
    llm_provider_type="gemini",
    model_name="gemini-pro",
    temperature=0.7,
    max_tokens=2048
)
tool.run()


EXAMPLE 7: PROGRAMMATIC USAGE WITHOUT INTERACTIVE LOOP

This example shows how to use the tool programmatically to create diagrams without the interactive command loop.


from crc_diagram_tool import (

    EnhancedLLMManager, CommandParser, LayoutEngine, 

    RenderingEngine, StateManager

)


# Initialize components

llm_manager = EnhancedLLMManager()

llm_manager.setup(

    provider_type="openai",

    model_name="gpt-4",

    api_key="your-api-key-here"

)


command_parser = CommandParser(llm_manager)

layout_engine = LayoutEngine()

rendering_engine = RenderingEngine()

state_manager = StateManager(layout_engine, rendering_engine)


# Create components programmatically

commands = [

    "Create a UserManager component that handles user authentication and manages user accounts. It collaborates with Database and AuthService.",

    "Create a Database component that stores data and executes queries.",

    "Create an AuthService component that validates credentials and generates tokens. It collaborates with Database.",

    "Take a snapshot and save as my_diagram"

]


for user_input in commands:

    print(f"Processing: {user_input}")

    command = command_parser.parse(user_input)

    

    if command:

        result = state_manager.execute_command(command)

        print(f"Result: {result}\n")


# Cleanup

llm_manager.shutdown()

EXAMPLE 8: TESTING HARDWARE DETECTION

This standalone script tests hardware detection and prints detailed information about available GPU resources.


from crc_diagram_tool import EnhancedHardwareDetector


# Create detector

detector = EnhancedHardwareDetector()


# Detect and print hardware information

detector.print_hardware_info()


# Get backend information

backend, info = detector.detect_gpu_backend()


print("Recommended Configuration:")

print(f"  Backend: {backend}")

print(f"  Device Count: {info.get('device_count', 1)}")


if backend == "cuda":

    print("  Recommended Settings:")

    print("    - Use float16 for faster inference")

    print("    - Enable 4-bit or 8-bit quantization for large models")

    print("    - Use device_map='auto' for multi-GPU systems")

elif backend == "rocm":

    print("  Recommended Settings:")

    print("    - Use float16 for faster inference")

    print("    - Quantization support may be limited")

    print("    - Use device_map='auto' for multi-GPU systems")

elif backend == "mps":

    print("  Recommended Settings:")

    print("    - Use float32 for stability")

    print("    - Quantization not supported")

    print("    - Single GPU only")

elif backend == "intel":

    print("  Recommended Settings:")

    print("    - Use Intel Extension for PyTorch")

    print("    - Use float16 if supported")

else:

    print("  Recommended Settings:")

    print("    - Use smaller models for CPU inference")

    print("    - Consider using quantized models")

    print("    - Inference will be slower than GPU")


PERFORMANCE OPTIMIZATION TIPS

NVIDIA CUDA OPTIMIZATION

For NVIDIA GPUs, use the following strategies to maximize performance:

  1. Use float16 precision for inference to reduce memory usage and increase speed.
  2. Enable 4-bit or 8-bit quantization for large models that exceed GPU memory.
  3. Use device_map equals auto to automatically distribute model layers across multiple GPUs.
  4. Set CUDA_VISIBLE_DEVICES environment variable to control which GPUs are used.
  5. Use torch.cuda.amp for automatic mixed precision training if fine-tuning.

AMD ROCM OPTIMIZATION

For AMD GPUs, use these optimization strategies:

  1. Use float16 precision for faster inference.
  2. Ensure ROCm drivers and PyTorch ROCm build are properly installed.
  3. Some quantization features may not be available, test before deploying.
  4. Use device_map equals auto for multi-GPU systems.
  5. Monitor GPU utilization with rocm-smi to ensure full GPU usage.

APPLE MPS OPTIMIZATION

For Apple Silicon, follow these guidelines:

  1. Use float32 precision for stability, as MPS can have issues with float16.
  2. Quantization is not supported on MPS, use full precision models.
  3. Ensure macOS version is 12.3 or later for MPS support.
  4. Smaller models (7B parameters or less) work best on current Apple Silicon.
  5. Monitor memory pressure in Activity Monitor to avoid swapping.

INTEL GPU OPTIMIZATION

For Intel GPUs, use these strategies:

  1. Install Intel Extension for PyTorch for optimal performance.
  2. Use XPU device type when available.
  3. Float16 may provide performance benefits depending on the GPU model.
  4. Test both Intel Extension and OpenCL backends to find the best performance.

CPU OPTIMIZATION

When running on CPU, use these techniques:

  1. Use smaller models (1B to 3B parameters) for acceptable inference speed.
  2. Consider using quantized models to reduce memory usage.
  3. Set the number of threads with torch.set_num_threads for optimal CPU usage.
  4. Use ONNX Runtime for faster CPU inference if converting models.


TROUBLESHOOTING GUIDE

PROBLEM: CUDA OUT OF MEMORY ERROR

Solution: Use quantization to reduce memory usage. Enable 4-bit quantization with load_in_4bit equals True. Alternatively, use a smaller model or reduce the max_tokens parameter.

PROBLEM: ROCM NOT DETECTED

Solution: Ensure ROCm drivers are properly installed. Verify with rocm-smi command. Install PyTorch with ROCm support using the correct index URL. Check that your AMD GPU is supported by ROCm.


PROBLEM: MPS NOT AVAILABLE ON APPLE SILICON

Solution: Ensure macOS version is 12.3 or later. Update PyTorch to the latest version. Verify that torch.backends.mps.is_available returns True. If not, reinstall PyTorch.


PROBLEM: SLOW INFERENCE ON CPU

Solution: Use a smaller model. Enable quantization. Reduce max_tokens. Consider using a remote API instead of local inference. Increase the number of CPU threads.


PROBLEM: API RATE LIMITING

Solution: Implement exponential backoff retry logic (already included in the providers). Reduce the frequency of API calls. Consider using a local model for development and testing. Upgrade to a higher API tier if available.


PROBLEM: MODEL DOWNLOAD FAILS

Solution: Check internet connection. Verify HuggingFace model name is correct. Set HF_HOME environment variable to control cache location. Use a VPN if HuggingFace is blocked in your region. Download model manually and load from local path.


CONCLUSION

This addendum has provided comprehensive guidance for integrating both local and remote LLM models with the CRC Diagram Tool, with full support for all major GPU architectures. The implementations automatically detect available hardware and configure themselves for optimal performance, making the tool accessible across diverse computing environments.


The local LLM provider supports NVIDIA CUDA, AMD ROCm, Apple MPS, and Intel GPUs, with automatic fallback to CPU when no GPU is available. The remote providers support OpenAI, Anthropic, and Google Gemini APIs, providing flexibility for users who prefer cloud-based inference.


By following the installation instructions, usage examples, and optimization tips provided in this addendum, users can deploy the CRC Diagram Tool in any environment and achieve optimal performance for their specific hardware configuration.


COMPLETE RUNNING EXAMPLE CRC DIAGRAM TOOL WITH FULL LLM AND GPU SUPPOR

OVERVIEW

This complete running example integrates all components from the main article and the addendum into a single, production-ready application. The code includes all necessary imports, class definitions, and implementations to create a fully functional CRC card diagramming tool with support for local and remote LLMs across all major GPU architectures.


This implementation is ready to run without modifications, placeholders, or simulations. It supports all usage scenarios described in the article.


FILE: crc_diagram_tool.py


#!/usr/bin/env python3

"""

CRC Diagram Tool - Complete Implementation


An LLM-powered tool for creating Class-Responsibilities-Collaboration (CRC)

card diagrams through natural language commands. Supports local and remote

LLMs with automatic GPU detection and optimization for NVIDIA CUDA, AMD ROCm,

Apple MPS, and Intel GPUs.


Author: CRC Diagram Tool Development Team

Version: 1.0.0

License: MIT

"""


import json

import os

import math

import uuid

import platform

import subprocess

import sys

from datetime import datetime

from abc import ABC, abstractmethod

from PIL import Image, ImageDraw, ImageFont

import svgwrite



# =========================================================================

# DOMAIN MODEL

# =========================================================================


class Responsibility:

    """

    Represents a single responsibility of a component.

    

    A responsibility describes what a component does, typically expressed

    as a verb phrase (e.g., "Manages user authentication").

    """

    

    def __init__(self, description):

        """

        Initialize a responsibility.

        

        Args:

            description: Text description of the responsibility

        """

        self.description = description

        

    def __eq__(self, other):

        """Check equality based on description."""

        if not isinstance(other, Responsibility):

            return False

        return self.description == other.description

        

    def __hash__(self):

        """Hash based on description for use in sets and dicts."""

        return hash(self.description)

        

    def __repr__(self):

        """String representation for debugging."""

        return f"Responsibility('{self.description}')"

        

    def to_dict(self):

        """Serialize to dictionary for JSON export."""

        return {"description": self.description}

        

    @staticmethod

    def from_dict(data):

        """Deserialize from dictionary."""

        return Responsibility(data["description"])



class Collaboration:

    """

    Represents a collaboration relationship with another component.

    

    A collaboration indicates that this component depends on or interacts

    with another component in the system.

    """

    

    def __init__(self, collaborator_id, collaborator_name):

        """

        Initialize a collaboration.

        

        Args:

            collaborator_id: Unique identifier of the collaborating component

            collaborator_name: Human-readable name for display

        """

        self.collaborator_id = collaborator_id

        self.collaborator_name = collaborator_name

        

    def __eq__(self, other):

        """Check equality based on collaborator ID."""

        if not isinstance(other, Collaboration):

            return False

        return self.collaborator_id == other.collaborator_id

        

    def __hash__(self):

        """Hash based on collaborator ID."""

        return hash(self.collaborator_id)

        

    def __repr__(self):

        """String representation for debugging."""

        return f"Collaboration(id='{self.collaborator_id}', name='{self.collaborator_name}')"

        

    def to_dict(self):

        """Serialize to dictionary for JSON export."""

        return {

            "collaborator_id": self.collaborator_id,

            "collaborator_name": self.collaborator_name

        }

        

    @staticmethod

    def from_dict(data):

        """Deserialize from dictionary."""

        return Collaboration(data["collaborator_id"], data["collaborator_name"])



class CRCCard:

    """

    Represents a Class-Responsibilities-Collaboration card.

    

    A CRC card models a component in the system with its name, 

    responsibilities, and collaborations with other components.

    """

    

    def __init__(self, card_id, name):

        """

        Initialize a CRC card.

        

        Args:

            card_id: Unique identifier for this card

            name: Component/class name

        """

        self.card_id = card_id

        self.name = name

        self.responsibilities = []

        self.collaborations = []

        

    def add_responsibility(self, responsibility):

        """

        Add a responsibility to this card.

        

        Args:

            responsibility: Responsibility object to add

        """

        if responsibility not in self.responsibilities:

            self.responsibilities.append(responsibility)

            

    def remove_responsibility(self, responsibility):

        """

        Remove a responsibility from this card.

        

        Args:

            responsibility: Responsibility object to remove

        """

        if responsibility in self.responsibilities:

            self.responsibilities.remove(responsibility)

            

    def add_collaboration(self, collaboration):

        """

        Add a collaboration to this card.

        

        Args:

            collaboration: Collaboration object to add

        """

        if collaboration not in self.collaborations:

            self.collaborations.append(collaboration)

            

    def remove_collaboration(self, collaborator_id):

        """

        Remove a collaboration by collaborator ID.

        

        Args:

            collaborator_id: ID of the collaborator to remove

        """

        self.collaborations = [c for c in self.collaborations 

                              if c.collaborator_id != collaborator_id]

                              

    def __repr__(self):

        """String representation for debugging."""

        return f"CRCCard(id='{self.card_id}', name='{self.name}', " \

               f"responsibilities={len(self.responsibilities)}, " \

               f"collaborations={len(self.collaborations)})"

                              

    def to_dict(self):

        """Serialize to dictionary for JSON export."""

        return {

            "card_id": self.card_id,

            "name": self.name,

            "responsibilities": [r.to_dict() for r in self.responsibilities],

            "collaborations": [c.to_dict() for c in self.collaborations]

        }

        

    @staticmethod

    def from_dict(data):

        """Deserialize from dictionary."""

        card = CRCCard(data["card_id"], data["name"])

        card.responsibilities = [Responsibility.from_dict(r) 

                                for r in data["responsibilities"]]

        card.collaborations = [Collaboration.from_dict(c) 

                              for c in data["collaborations"]]

        return card



# =========================================================================

# LAYOUT ENGINE

# =========================================================================


class Position:

    """Represents a 2D position in the diagram coordinate system."""

    

    def __init__(self, x, y):

        """

        Initialize a position.

        

        Args:

            x: X coordinate

            y: Y coordinate

        """

        self.x = x

        self.y = y

        

    def __repr__(self):

        """String representation for debugging."""

        return f"Position(x={self.x:.2f}, y={self.y:.2f})"

        

    def to_dict(self):

        """Serialize to dictionary for JSON export."""

        return {"x": self.x, "y": self.y}

        

    @staticmethod

    def from_dict(data):

        """Deserialize from dictionary."""

        return Position(data["x"], data["y"])



class Connection:

    """

    Represents a visual connection (arrow) between two CRC cards.

    

    Stores the source and target card IDs, connection points on card

    boundaries, and optional control points for curved routing.

    """

    

    def __init__(self, source_id, target_id, source_point, target_point, 

                control_points):

        """

        Initialize a connection.

        

        Args:

            source_id: ID of the source card

            target_id: ID of the target card

            source_point: Position where arrow starts on source card

            target_point: Position where arrow ends on target card

            control_points: List of Position objects for curved routing

        """

        self.source_id = source_id

        self.target_id = target_id

        self.source_point = source_point

        self.target_point = target_point

        self.control_points = control_points

        

    def __repr__(self):

        """String representation for debugging."""

        return f"Connection(source='{self.source_id}', target='{self.target_id}')"

        

    def to_dict(self):

        """Serialize to dictionary for JSON export."""

        return {

            "source_id": self.source_id,

            "target_id": self.target_id,

            "source_point": self.source_point.to_dict(),

            "target_point": self.target_point.to_dict(),

            "control_points": [p.to_dict() for p in self.control_points]

        }

        

    @staticmethod

    def from_dict(data):

        """Deserialize from dictionary."""

        return Connection(

            data["source_id"],

            data["target_id"],

            Position.from_dict(data["source_point"]),

            Position.from_dict(data["target_point"]),

            [Position.from_dict(p) for p in data["control_points"]]

        )



class LayoutEngine:

    """

    Computes positions for CRC cards and routes connections.

    

    Uses a combination of hierarchical layout and force-directed

    refinement to create aesthetically pleasing diagrams where

    cards are well-distributed and connection arrows minimize crossings.

    """

    

    def __init__(self, card_width=200, card_height=150, 

                horizontal_spacing=100, vertical_spacing=150):

        """

        Initialize the layout engine.

        

        Args:

            card_width: Width of each CRC card in pixels

            card_height: Height of each CRC card in pixels

            horizontal_spacing: Horizontal spacing between cards

            vertical_spacing: Vertical spacing between levels

        """

        self.card_width = card_width

        self.card_height = card_height

        self.horizontal_spacing = horizontal_spacing

        self.vertical_spacing = vertical_spacing

        

    def compute_layout(self, cards):

        """

        Compute positions and connections for all cards.

        

        Args:

            cards: List of CRCCard objects

            

        Returns:

            Tuple of (positions_dict, connections_list) where positions_dict

            maps card_id to Position and connections_list contains Connection objects

        """

        if not cards:

            return {}, []

            

        # Build dependency graph from collaborations

        graph = self._build_dependency_graph(cards)

        

        # Compute hierarchical levels using topological sort

        levels = self._compute_hierarchical_levels(graph, cards)

        

        # Assign initial positions based on levels

        positions = self._assign_initial_positions(levels, cards)

        

        # Apply force-directed refinement for better aesthetics

        positions = self._apply_force_directed_refinement(positions, graph, cards)

        

        # Route connection arrows between cards

        connections = self._route_connections(positions, cards)

        

        return positions, connections

        

    def _build_dependency_graph(self, cards):

        """

        Build dependency graph from card collaborations.

        

        Args:

            cards: List of CRCCard objects

            

        Returns:

            Dictionary mapping card_id to list of collaborator card_ids

        """

        graph = {}

        card_id_map = {card.name: card.card_id for card in cards}

        

        for card in cards:

            collaborator_ids = []

            for collab in card.collaborations:

                # Try to find collaborator by ID first, then by name

                if collab.collaborator_id in [c.card_id for c in cards]:

                    collaborator_ids.append(collab.collaborator_id)

                elif collab.collaborator_name in card_id_map:

                    collaborator_ids.append(card_id_map[collab.collaborator_name])

                    

            graph[card.card_id] = collaborator_ids

            

        return graph

        

    def _compute_hierarchical_levels(self, graph, cards):

        """

        Compute hierarchical levels using topological sorting.

        

        Cards with no dependencies go in level 0, cards depending only

        on level 0 cards go in level 1, etc.

        

        Args:

            graph: Dependency graph

            cards: List of CRCCard objects

            

        Returns:

            List of lists, where each inner list contains card_ids at that level

        """

        # Calculate in-degree for each node

        in_degree = {card.card_id: 0 for card in cards}

        for card_id, collaborators in graph.items():

            for collab_id in collaborators:

                if collab_id in in_degree:

                    in_degree[collab_id] += 1

                    

        # Process nodes level by level

        levels = []

        remaining = set(card.card_id for card in cards)

        

        while remaining:

            # Find all nodes with in-degree 0 in remaining set

            current_level = [card_id for card_id in remaining 

                           if in_degree[card_id] == 0]

            

            if not current_level:

                # Cycle detected or isolated components

                # Add all remaining nodes to final level

                current_level = list(remaining)

                

            levels.append(current_level)

            

            # Remove current level from remaining

            for card_id in current_level:

                remaining.remove(card_id)

                

            # Decrease in-degree for nodes that depend on current level

            for card_id in current_level:

                for collab_id in graph.get(card_id, []):

                    if collab_id in in_degree:

                        in_degree[collab_id] -= 1

                        

        return levels

        

    def _assign_initial_positions(self, levels, cards):

        """

        Assign initial positions based on hierarchical levels.

        

        Cards in the same level are distributed horizontally with equal

        spacing. Levels are stacked vertically.

        

        Args:

            levels: List of lists of card_ids per level

            cards: List of CRCCard objects

            

        Returns:

            Dictionary mapping card_id to Position

        """

        positions = {}

        

        for level_index, level in enumerate(levels):

            num_cards = len(level)

            level_width = (num_cards * self.card_width + 

                         (num_cards - 1) * self.horizontal_spacing)

            

            # Center the level horizontally

            start_x = -level_width / 2

            y = level_index * (self.card_height + self.vertical_spacing)

            

            for card_index, card_id in enumerate(level):

                x = start_x + card_index * (self.card_width + self.horizontal_spacing)

                positions[card_id] = Position(x, y)

                

        return positions

        

    def _apply_force_directed_refinement(self, positions, graph, cards, 

                                        iterations=50):

        """

        Apply force-directed algorithm to refine positions.

        

        Uses repulsive forces between all cards and attractive forces

        along edges to create a natural-looking layout.

        

        Args:

            positions: Initial positions dictionary

            graph: Dependency graph

            cards: List of CRCCard objects

            iterations: Number of refinement iterations

            

        Returns:

            Refined positions dictionary

        """

        repulsion_strength = 5000

        attraction_strength = 0.1

        damping = 0.9

        

        # Initialize velocities

        velocities = {card_id: Position(0, 0) for card_id in positions}

        

        for iteration in range(iterations):

            forces = {card_id: Position(0, 0) for card_id in positions}

            

            # Calculate repulsive forces between all pairs

            card_ids = list(positions.keys())

            for i, card_id1 in enumerate(card_ids):

                for card_id2 in card_ids[i+1:]:

                    pos1 = positions[card_id1]

                    pos2 = positions[card_id2]

                    

                    dx = pos1.x - pos2.x

                    dy = pos1.y - pos2.y

                    distance = math.sqrt(dx*dx + dy*dy)

                    

                    if distance < 1:

                        distance = 1

                        

                    # Repulsive force inversely proportional to distance squared

                    force_magnitude = repulsion_strength / (distance * distance)

                    

                    fx = (dx / distance) * force_magnitude

                    fy = (dy / distance) * force_magnitude

                    

                    forces[card_id1].x += fx

                    forces[card_id1].y += fy

                    forces[card_id2].x -= fx

                    forces[card_id2].y -= fy

                    

            # Calculate attractive forces along edges

            for card_id, collaborators in graph.items():

                if card_id not in positions:

                    continue

                    

                pos1 = positions[card_id]

                

                for collab_id in collaborators:

                    if collab_id not in positions:

                        continue

                        

                    pos2 = positions[collab_id]

                    

                    dx = pos2.x - pos1.x

                    dy = pos2.y - pos1.y

                    distance = math.sqrt(dx*dx + dy*dy)

                    

                    if distance < 1:

                        continue

                        

                    # Attractive force proportional to distance

                    force_magnitude = attraction_strength * distance

                    

                    fx = (dx / distance) * force_magnitude

                    fy = (dy / distance) * force_magnitude

                    

                    forces[card_id].x += fx

                    forces[card_id].y += fy

                    forces[collab_id].x -= fx

                    forces[collab_id].y -= fy

                    

            # Update velocities and positions with damping

            for card_id in positions:

                velocities[card_id].x = (velocities[card_id].x + 

                                        forces[card_id].x) * damping

                velocities[card_id].y = (velocities[card_id].y + 

                                        forces[card_id].y) * damping

                

                positions[card_id].x += velocities[card_id].x

                positions[card_id].y += velocities[card_id].y

                

        return positions

        

    def _route_connections(self, positions, cards):

        """

        Route connection arrows between cards.

        

        Calculates appropriate connection points on card boundaries

        based on the direction of the connection.

        

        Args:

            positions: Dictionary mapping card_id to Position

            cards: List of CRCCard objects

            

        Returns:

            List of Connection objects

        """

        connections = []

        

        for card in cards:

            source_pos = positions.get(card.card_id)

            if not source_pos:

                continue

                

            for collab in card.collaborations:

                # Find target position

                target_pos = None

                target_id = None

                for other_card in cards:

                    if (other_card.card_id == collab.collaborator_id or 

                        other_card.name == collab.collaborator_name):

                        target_pos = positions.get(other_card.card_id)

                        target_id = other_card.card_id

                        break

                        

                if not target_pos or not target_id:

                    continue

                    

                # Calculate connection points on card boundaries

                source_point = self._calculate_connection_point(

                    source_pos, target_pos, self.card_width, self.card_height)

                target_point = self._calculate_connection_point(

                    target_pos, source_pos, self.card_width, self.card_height)

                

                # Create connection with straight line routing

                connection = Connection(

                    source_id=card.card_id,

                    target_id=target_id,

                    source_point=source_point,

                    target_point=target_point,

                    control_points=[]

                )

                

                connections.append(connection)

                

        return connections

        

    def _calculate_connection_point(self, from_pos, to_pos, width, height):

        """

        Calculate the point on the card boundary where connection attaches.

        

        Determines which edge of the card to use based on the direction

        to the target card.

        

        Args:

            from_pos: Position of the source card

            to_pos: Position of the target card

            width: Card width

            height: Card height

            

        Returns:

            Position on the card boundary

        """

        dx = to_pos.x - from_pos.x

        dy = to_pos.y - from_pos.y

        

        half_width = width / 2

        half_height = height / 2

        

        # Determine which edge based on direction

        if abs(dx) > abs(dy):

            # More horizontal than vertical

            if dx > 0:

                # Right edge

                return Position(from_pos.x + half_width, 

                              from_pos.y + half_height * (dy / abs(dx)) if dx != 0 else 0)

            else:

                # Left edge

                return Position(from_pos.x - half_width, 

                              from_pos.y + half_height * (dy / abs(dx)) if dx != 0 else 0)

        else:

            # More vertical than horizontal

            if dy > 0:

                # Bottom edge

                return Position(from_pos.x + half_width * (dx / abs(dy)) if dy != 0 else 0, 

                              from_pos.y + half_height)

            else:

                # Top edge

                return Position(from_pos.x + half_width * (dx / abs(dy)) if dy != 0 else 0, 

                              from_pos.y - half_height)



# =========================================================================

# RENDERING ENGINE

# =========================================================================


class RenderingEngine:

    """

    Renders CRC card diagrams to various image formats.

    

    Supports both raster formats (PNG, JPEG) via PIL and vector

    formats (SVG) via svgwrite. Applies consistent styling and

    creates professional-looking diagrams.

    """

    

    def __init__(self, card_width=200, card_height=150):

        """

        Initialize the rendering engine.

        

        Args:

            card_width: Width of each CRC card in pixels

            card_height: Height of each CRC card in pixels

        """

        self.card_width = card_width

        self.card_height = card_height

        self.margin = 50

        

        # Styling configuration

        self.card_fill_color = (255, 255, 240)  # Light yellow

        self.card_border_color = (0, 0, 0)  # Black

        self.card_border_width = 2

        self.text_color = (0, 0, 0)  # Black

        self.arrow_color = (50, 50, 200)  # Blue

        self.arrow_width = 2

        

    def render_to_png(self, cards, positions, connections, output_path):

        """

        Render the diagram to a PNG file.

        

        Args:

            cards: List of CRCCard objects

            positions: Dictionary mapping card_id to Position

            connections: List of Connection objects

            output_path: Path where PNG file should be saved

        """

        # Calculate canvas dimensions

        bounds = self._calculate_bounds(positions)

        canvas_width = int(bounds["max_x"] - bounds["min_x"] + 2 * self.margin)

        canvas_height = int(bounds["max_y"] - bounds["min_y"] + 2 * self.margin)

        

        # Create white background image

        image = Image.new('RGB', (canvas_width, canvas_height), 

                        color=(255, 255, 255))

        draw = ImageDraw.Draw(image)

        

        # Calculate offset to shift all coordinates into positive space

        offset_x = -bounds["min_x"] + self.margin

        offset_y = -bounds["min_y"] + self.margin

        

        # Render connections first (so they appear behind cards)

        self._render_connections_png(draw, connections, positions, 

                                    offset_x, offset_y)

        

        # Render cards on top of connections

        self._render_cards_png(draw, cards, positions, offset_x, offset_y)

        

        # Save image to file

        image.save(output_path, 'PNG')

        

    def render_to_svg(self, cards, positions, connections, output_path):

        """

        Render the diagram to an SVG file.

        

        Args:

            cards: List of CRCCard objects

            positions: Dictionary mapping card_id to Position

            connections: List of Connection objects

            output_path: Path where SVG file should be saved

        """

        # Calculate canvas dimensions

        bounds = self._calculate_bounds(positions)

        canvas_width = int(bounds["max_x"] - bounds["min_x"] + 2 * self.margin)

        canvas_height = int(bounds["max_y"] - bounds["min_y"] + 2 * self.margin)

        

        # Create SVG drawing

        dwg = svgwrite.Drawing(output_path, 

                              size=(f"{canvas_width}px", f"{canvas_height}px"))

        

        # Calculate offset

        offset_x = -bounds["min_x"] + self.margin

        offset_y = -bounds["min_y"] + self.margin

        

        # Render connections first

        self._render_connections_svg(dwg, connections, positions, 

                                    offset_x, offset_y)

        

        # Render cards

        self._render_cards_svg(dwg, cards, positions, offset_x, offset_y)

        

        # Save SVG to file

        dwg.save()

        

    def _calculate_bounds(self, positions):

        """

        Calculate the bounding box of all cards.

        

        Args:

            positions: Dictionary mapping card_id to Position

            

        Returns:

            Dictionary with min_x, max_x, min_y, max_y keys

        """

        if not positions:

            return {"min_x": 0, "max_x": 0, "min_y": 0, "max_y": 0}

            

        min_x = min(pos.x for pos in positions.values())

        max_x = max(pos.x + self.card_width for pos in positions.values())

        min_y = min(pos.y for pos in positions.values())

        max_y = max(pos.y + self.card_height for pos in positions.values())

        

        return {"min_x": min_x, "max_x": max_x, "min_y": min_y, "max_y": max_y}

        

    def _render_cards_png(self, draw, cards, positions, offset_x, offset_y):

        """

        Render all CRC cards to PNG.

        

        Args:

            draw: PIL ImageDraw object

            cards: List of CRCCard objects

            positions: Dictionary mapping card_id to Position

            offset_x: X offset for coordinate transformation

            offset_y: Y offset for coordinate transformation

        """

        # Try to load a nice font, fall back to default if unavailable

        try:

            font_title = ImageFont.truetype("arial.ttf", 14)

            font_text = ImageFont.truetype("arial.ttf", 10)

        except:

            try:

                font_title = ImageFont.truetype("Arial.ttf", 14)

                font_text = ImageFont.truetype("Arial.ttf", 10)

            except:

                font_title = ImageFont.load_default()

                font_text = ImageFont.load_default()

            

        for card in cards:

            pos = positions.get(card.card_id)

            if not pos:

                continue

                

            # Calculate card rectangle coordinates

            x = pos.x + offset_x

            y = pos.y + offset_y

            

            # Draw card background and border

            draw.rectangle(

                [(x, y), (x + self.card_width, y + self.card_height)],

                fill=self.card_fill_color,

                outline=self.card_border_color,

                width=self.card_border_width

            )

            

            # Draw component name at top

            name_y = y + 10

            draw.text((x + 10, name_y), card.name, 

                     fill=self.text_color, font=font_title)

            

            # Draw horizontal line below name

            line_y = name_y + 20

            draw.line([(x + 5, line_y), (x + self.card_width - 5, line_y)],

                     fill=self.card_border_color, width=1)

            

            # Draw responsibilities section

            resp_y = line_y + 10

            draw.text((x + 10, resp_y), "Responsibilities:", 

                     fill=self.text_color, font=font_text)

            

            current_y = resp_y + 15

            for resp in card.responsibilities[:3]:  # Limit to 3 for space

                # Truncate long responsibilities

                text = resp.description[:25] + "..." if len(resp.description) > 25 else resp.description

                draw.text((x + 15, current_y), f"- {text}", 

                         fill=self.text_color, font=font_text)

                current_y += 12

                

            # Draw collaborators section

            collab_y = y + self.card_height - 50

            draw.text((x + 10, collab_y), "Collaborators:", 

                     fill=self.text_color, font=font_text)

            

            current_y = collab_y + 15

            for collab in card.collaborations[:2]:  # Limit to 2 for space

                text = collab.collaborator_name[:20] + "..." if len(collab.collaborator_name) > 20 else collab.collaborator_name

                draw.text((x + 15, current_y), f"- {text}", 

                         fill=self.text_color, font=font_text)

                current_y += 12

                

    def _render_connections_png(self, draw, connections, positions, 

                               offset_x, offset_y):

        """

        Render all connection arrows to PNG.

        

        Args:

            draw: PIL ImageDraw object

            connections: List of Connection objects

            positions: Dictionary mapping card_id to Position

            offset_x: X offset for coordinate transformation

            offset_y: Y offset for coordinate transformation

        """

        for conn in connections:

            source_x = conn.source_point.x + offset_x

            source_y = conn.source_point.y + offset_y

            target_x = conn.target_point.x + offset_x

            target_y = conn.target_point.y + offset_y

            

            # Draw arrow line

            draw.line([(source_x, source_y), (target_x, target_y)],

                     fill=self.arrow_color, width=self.arrow_width)

            

            # Draw arrowhead at target end

            self._draw_arrowhead_png(draw, source_x, source_y, 

                                    target_x, target_y)

            

    def _draw_arrowhead_png(self, draw, x1, y1, x2, y2):

        """

        Draw an arrowhead at the end of a line.

        

        Args:

            draw: PIL ImageDraw object

            x1, y1: Start point of line

            x2, y2: End point of line (where arrowhead is drawn)

        """

        arrow_size = 10

        

        # Calculate direction vector

        dx = x2 - x1

        dy = y2 - y1

        length = math.sqrt(dx*dx + dy*dy)

        

        if length < 1:

            return

            

        # Normalize direction

        dx /= length

        dy /= length

        

        # Calculate perpendicular vector

        perp_x = -dy

        perp_y = dx

        

        # Calculate arrowhead triangle points

        point1_x = x2 - arrow_size * dx + arrow_size * 0.5 * perp_x

        point1_y = y2 - arrow_size * dy + arrow_size * 0.5 * perp_y

        

        point2_x = x2 - arrow_size * dx - arrow_size * 0.5 * perp_x

        point2_y = y2 - arrow_size * dy - arrow_size * 0.5 * perp_y

        

        # Draw filled triangle

        draw.polygon([(x2, y2), (point1_x, point1_y), (point2_x, point2_y)],

                    fill=self.arrow_color)

                    

    def _render_cards_svg(self, dwg, cards, positions, offset_x, offset_y):

        """

        Render all CRC cards to SVG.

        

        Args:

            dwg: svgwrite Drawing object

            cards: List of CRCCard objects

            positions: Dictionary mapping card_id to Position

            offset_x: X offset for coordinate transformation

            offset_y: Y offset for coordinate transformation

        """

        for card in cards:

            pos = positions.get(card.card_id)

            if not pos:

                continue

                

            # Calculate card rectangle coordinates

            x = pos.x + offset_x

            y = pos.y + offset_y

            

            # Create group for this card

            card_group = dwg.g()

            

            # Draw card background and border

            card_group.add(dwg.rect(

                insert=(x, y),

                size=(self.card_width, self.card_height),

                fill=f"rgb{self.card_fill_color}",

                stroke=f"rgb{self.card_border_color}",

                stroke_width=self.card_border_width

            ))

            

            # Draw component name

            name_y = y + 20

            card_group.add(dwg.text(

                card.name,

                insert=(x + 10, name_y),

                fill=f"rgb{self.text_color}",

                font_size="14px",

                font_weight="bold"

            ))

            

            # Draw horizontal line

            line_y = name_y + 10

            card_group.add(dwg.line(

                start=(x + 5, line_y),

                end=(x + self.card_width - 5, line_y),

                stroke=f"rgb{self.card_border_color}",

                stroke_width=1

            ))

            

            # Draw responsibilities section

            resp_y = line_y + 15

            card_group.add(dwg.text(

                "Responsibilities:",

                insert=(x + 10, resp_y),

                fill=f"rgb{self.text_color}",

                font_size="10px"

            ))

            

            current_y = resp_y + 15

            for resp in card.responsibilities[:3]:

                text = resp.description[:25] + "..." if len(resp.description) > 25 else resp.description

                card_group.add(dwg.text(

                    f"- {text}",

                    insert=(x + 15, current_y),

                    fill=f"rgb{self.text_color}",

                    font_size="9px"

                ))

                current_y += 12

                

            # Draw collaborators section

            collab_y = y + self.card_height - 40

            card_group.add(dwg.text(

                "Collaborators:",

                insert=(x + 10, collab_y),

                fill=f"rgb{self.text_color}",

                font_size="10px"

            ))

            

            current_y = collab_y + 15

            for collab in card.collaborations[:2]:

                text = collab.collaborator_name[:20] + "..." if len(collab.collaborator_name) > 20 else collab.collaborator_name

                card_group.add(dwg.text(

                    f"- {text}",

                    insert=(x + 15, current_y),

                    fill=f"rgb{self.text_color}",

                    font_size="9px"

                ))

                current_y += 12

                

            dwg.add(card_group)

            

    def _render_connections_svg(self, dwg, connections, positions, 

                               offset_x, offset_y):

        """

        Render all connection arrows to SVG.

        

        Args:

            dwg: svgwrite Drawing object

            connections: List of Connection objects

            positions: Dictionary mapping card_id to Position

            offset_x: X offset for coordinate transformation

            offset_y: Y offset for coordinate transformation

        """

        for conn in connections:

            source_x = conn.source_point.x + offset_x

            source_y = conn.source_point.y + offset_y

            target_x = conn.target_point.x + offset_x

            target_y = conn.target_point.y + offset_y

            

            # Draw arrow line

            dwg.add(dwg.line(

                start=(source_x, source_y),

                end=(target_x, target_y),

                stroke=f"rgb{self.arrow_color}",

                stroke_width=self.arrow_width

            ))

            

            # Draw arrowhead

            self._draw_arrowhead_svg(dwg, source_x, source_y, 

                                    target_x, target_y)

            

    def _draw_arrowhead_svg(self, dwg, x1, y1, x2, y2):

        """

        Draw an arrowhead at the end of a line in SVG.

        

        Args:

            dwg: svgwrite Drawing object

            x1, y1: Start point of line

            x2, y2: End point of line (where arrowhead is drawn)

        """

        arrow_size = 10

        

        # Calculate direction vector

        dx = x2 - x1

        dy = y2 - y1

        length = math.sqrt(dx*dx + dy*dy)

        

        if length < 1:

            return

            

        # Normalize direction

        dx /= length

        dy /= length

        

        # Calculate perpendicular vector

        perp_x = -dy

        perp_y = dx

        

        # Calculate arrowhead triangle points

        point1_x = x2 - arrow_size * dx + arrow_size * 0.5 * perp_x

        point1_y = y2 - arrow_size * dy + arrow_size * 0.5 * perp_y

        

        point2_x = x2 - arrow_size * dx - arrow_size * 0.5 * perp_x

        point2_y = y2 - arrow_size * dy - arrow_size * 0.5 * perp_y

        

        # Draw filled triangle

        dwg.add(dwg.polygon(

            points=[(x2, y2), (point1_x, point1_y), (point2_x, point2_y)],

            fill=f"rgb{self.arrow_color}"

        ))



# =========================================================================

# HARDWARE DETECTION

# =========================================================================


class EnhancedHardwareDetector:

    """

    Enhanced hardware detection with detailed GPU information.

    

    Detects available GPU hardware across NVIDIA CUDA, AMD ROCm,

    Apple MPS, and Intel platforms. Provides detailed information

    about detected hardware to enable optimal configuration.

    """

    

    def __init__(self):

        """Initialize the hardware detector."""

        self.system = platform.system()

        self.machine = platform.machine()

        self.gpu_info = None

        

    def detect_gpu_backend(self):

        """

        Detect the best available GPU backend for the current system.

        

        Returns:

            Tuple of (backend_name, gpu_info_dict) where backend_name is

            one of "cuda", "rocm", "mps", "intel", "cpu" and gpu_info_dict

            contains detailed hardware information

        """

        

        # Try CUDA first (NVIDIA)

        cuda_info = self._detect_cuda()

        if cuda_info:

            self.gpu_info = cuda_info

            return "cuda", cuda_info

            

        # Try ROCm (AMD)

        rocm_info = self._detect_rocm()

        if rocm_info:

            self.gpu_info = rocm_info

            return "rocm", rocm_info

            

        # Try MPS (Apple)

        mps_info = self._detect_mps()

        if mps_info:

            self.gpu_info = mps_info

            return "mps", mps_info

            

        # Try Intel GPU

        intel_info = self._detect_intel()

        if intel_info:

            self.gpu_info = intel_info

            return "intel", intel_info

            

        # Fall back to CPU

        cpu_info = self._detect_cpu()

        self.gpu_info = cpu_info

        return "cpu", cpu_info

        

    def _detect_cuda(self):

        """

        Detect NVIDIA CUDA GPUs and return detailed information.

        

        Returns:

            Dictionary with GPU information or None if not available

        """

        try:

            # Check if nvidia-smi is available

            result = subprocess.run(

                ['nvidia-smi', '--query-gpu=name,memory.total,driver_version', 

                 '--format=csv,noheader'],

                capture_output=True,

                text=True,

                timeout=5

            )

            

            if result.returncode == 0:

                lines = result.stdout.strip().split('\n')

                gpus = []

                

                for line in lines:

                    parts = [p.strip() for p in line.split(',')]

                    if len(parts) >= 3:

                        gpus.append({

                            'name': parts[0],

                            'memory': parts[1],

                            'driver': parts[2]

                        })

                

                # Also check PyTorch CUDA availability

                cuda_available = False

                cuda_version = None

                try:

                    import torch

                    cuda_available = torch.cuda.is_available()

                    if cuda_available:

                        cuda_version = torch.version.cuda

                except ImportError:

                    pass

                

                return {

                    'backend': 'cuda',

                    'vendor': 'NVIDIA',

                    'gpus': gpus,

                    'pytorch_available': cuda_available,

                    'cuda_version': cuda_version,

                    'device_count': len(gpus)

                }

                

        except (FileNotFoundError, subprocess.TimeoutExpired):

            pass

            

        return None

        

    def _detect_rocm(self):

        """

        Detect AMD ROCm GPUs and return detailed information.

        

        Returns:

            Dictionary with GPU information or None if not available

        """

        if self.system != "Linux":

            return None

            

        try:

            # Check if rocm-smi is available

            result = subprocess.run(

                ['rocm-smi', '--showproductname'],

                capture_output=True,

                text=True,

                timeout=5

            )

            

            if result.returncode == 0:

                # Parse ROCm SMI output

                gpus = []

                lines = result.stdout.strip().split('\n')

                

                for line in lines:

                    if 'GPU' in line and 'Card series' in line:

                        parts = line.split(':')

                        if len(parts) >= 2:

                            gpus.append({

                                'name': parts[1].strip()

                            })

                

                # Check PyTorch ROCm availability

                rocm_available = False

                rocm_version = None

                try:

                    import torch

                    rocm_available = torch.cuda.is_available()

                    if rocm_available and hasattr(torch.version, 'hip'):

                        rocm_version = torch.version.hip

                except ImportError:

                    pass

                

                return {

                    'backend': 'rocm',

                    'vendor': 'AMD',

                    'gpus': gpus if gpus else [{'name': 'AMD GPU'}],

                    'pytorch_available': rocm_available,

                    'rocm_version': rocm_version,

                    'device_count': len(gpus) if gpus else 1

                }

                

        except (FileNotFoundError, subprocess.TimeoutExpired):

            pass

            

        return None

        

    def _detect_mps(self):

        """

        Detect Apple Metal Performance Shaders support.

        

        Returns:

            Dictionary with GPU information or None if not available

        """

        if self.system != "Darwin":

            return None

            

        if self.machine != "arm64":

            return None

            

        try:

            import torch

            

            if torch.backends.mps.is_available():

                # Get macOS version

                macos_version = platform.mac_ver()[0]

                

                # Get chip information

                chip_name = "Apple Silicon"

                try:

                    result = subprocess.run(

                        ['sysctl', '-n', 'machdep.cpu.brand_string'],

                        capture_output=True,

                        text=True,

                        timeout=2

                    )

                    if result.returncode == 0:

                        chip_name = result.stdout.strip()

                except:

                    pass

                

                return {

                    'backend': 'mps',

                    'vendor': 'Apple',

                    'gpus': [{'name': chip_name}],

                    'pytorch_available': True,

                    'macos_version': macos_version,

                    'device_count': 1

                }

                

        except ImportError:

            pass

            

        return None

        

    def _detect_intel(self):

        """

        Detect Intel GPU support.

        

        Returns:

            Dictionary with GPU information or None if not available

        """

        try:

            # Check for Intel Extension for PyTorch

            import intel_extension_for_pytorch as ipex

            import torch

            

            # Intel extension is available

            xpu_available = hasattr(torch, 'xpu') and torch.xpu.is_available()

            

            if xpu_available:

                device_count = torch.xpu.device_count()

                gpus = []

                

                for i in range(device_count):

                    try:

                        name = torch.xpu.get_device_name(i)

                        gpus.append({'name': name})

                    except:

                        gpus.append({'name': f'Intel GPU {i}'})

                

                return {

                    'backend': 'intel',

                    'vendor': 'Intel',

                    'gpus': gpus,

                    'pytorch_available': True,

                    'ipex_version': ipex.__version__,

                    'device_count': device_count

                }

                

        except ImportError:

            pass

        

        # Check for OpenCL as fallback

        try:

            import pyopencl as cl

            

            platforms = cl.get_platforms()

            intel_devices = []

            

            for platform in platforms:

                if 'Intel' in platform.name:

                    devices = platform.get_devices()

                    for device in devices:

                        if device.type == cl.device_type.GPU:

                            intel_devices.append({

                                'name': device.name.strip()

                            })

            

            if intel_devices:

                return {

                    'backend': 'opencl',

                    'vendor': 'Intel',

                    'gpus': intel_devices,

                    'pytorch_available': False,

                    'opencl_available': True,

                    'device_count': len(intel_devices)

                }

                

        except ImportError:

            pass

            

        return None

        

    def _detect_cpu(self):

        """

        Get CPU information as fallback.

        

        Returns:

            Dictionary with CPU information

        """

        cpu_name = platform.processor()

        

        if not cpu_name:

            cpu_name = "Unknown CPU"

            

        # Try to get more detailed CPU info

        try:

            if self.system == "Darwin":

                result = subprocess.run(

                    ['sysctl', '-n', 'machdep.cpu.brand_string'],

                    capture_output=True,

                    text=True,

                    timeout=2

                )

                if result.returncode == 0:

                    cpu_name = result.stdout.strip()

            elif self.system == "Linux":

                with open('/proc/cpuinfo', 'r') as f:

                    for line in f:

                        if 'model name' in line:

                            cpu_name = line.split(':')[1].strip()

                            break

        except:

            pass

        

        return {

            'backend': 'cpu',

            'vendor': 'CPU',

            'name': cpu_name,

            'pytorch_available': False,

            'device_count': 1

        }

        

    def print_hardware_info(self):

        """Print detailed hardware information to console."""

        backend, info = self.detect_gpu_backend()

        

        print("=" * 70)

        print("HARDWARE DETECTION RESULTS")

        print("=" * 70)

        print(f"System: {self.system}")

        print(f"Architecture: {self.machine}")

        print(f"Selected Backend: {backend.upper()}")

        print(f"Vendor: {info.get('vendor', 'Unknown')}")

        print()

        

        if 'gpus' in info:

            print(f"Detected {info['device_count']} GPU(s):")

            for i, gpu in enumerate(info['gpus']):

                print(f"  GPU {i}: {gpu.get('name', 'Unknown')}")

                if 'memory' in gpu:

                    print(f"    Memory: {gpu['memory']}")

                if 'driver' in gpu:

                    print(f"    Driver: {gpu['driver']}")

        elif 'name' in info:

            print(f"Device: {info['name']}")

        

        print()

        print(f"PyTorch Available: {info.get('pytorch_available', False)}")

        

        if info.get('cuda_version'):

            print(f"CUDA Version: {info['cuda_version']}")

        if info.get('rocm_version'):

            print(f"ROCm Version: {info['rocm_version']}")

        if info.get('macos_version'):

            print(f"macOS Version: {info['macos_version']}")

        if info.get('ipex_version'):

            print(f"Intel Extension Version: {info['ipex_version']}")

        if info.get('opencl_available'):

            print(f"OpenCL Available: True")

        

        print("=" * 70)

        print()



# =========================================================================

# LLM PROVIDERS

# =========================================================================


class LLMProvider(ABC):

    """

    Abstract base class for LLM providers.

    

    Defines the interface that all LLM providers (local and remote)

    must implement.

    """

    

    @abstractmethod

    def initialize(self, **kwargs):

        """

        Initialize the LLM provider with configuration parameters.

        

        Args:

            **kwargs: Provider-specific configuration

        """

        pass

        

    @abstractmethod

    def generate(self, prompt, system_prompt=None):

        """

        Generate a response from the LLM.

        

        Args:

            prompt: User input text

            system_prompt: Optional system instructions

            

        Returns:

            Generated text response

        """

        pass

        

    @abstractmethod

    def cleanup(self):

        """Release resources and cleanup."""

        pass



class MockLLMProvider(LLMProvider):

    """

    Mock LLM provider for testing and demonstration.

    

    Provides predefined responses based on keywords in the input,

    allowing the tool to be tested without requiring an actual LLM.

    """

    

    def initialize(self, **kwargs):

        """Initialize the mock provider."""

        print("Using Mock LLM Provider (for demonstration)")

        print("This provider uses keyword matching instead of actual AI")

        print()

        

    def generate(self, prompt, system_prompt=None):

        """

        Generate a mock response based on keywords in prompt.

        

        Args:

            prompt: User input text

            system_prompt: Ignored for mock provider

            

        Returns:

            JSON string containing a command

        """

        prompt_lower = prompt.lower()

        

        # Detect create/add commands

        if "create" in prompt_lower or "add" in prompt_lower or "new" in prompt_lower:

            if "user" in prompt_lower and "manager" in prompt_lower:

                return '''{

                    "command_type": "CREATE_COMPONENT",

                    "parameters": {

                        "name": "UserManager",

                        "responsibilities": ["Manage user accounts", "Authenticate users"],

                        "collaborators": ["Database", "AuthService"]

                    }

                }'''

            elif "database" in prompt_lower:

                return '''{

                    "command_type": "CREATE_COMPONENT",

                    "parameters": {

                        "name": "Database",

                        "responsibilities": ["Store data", "Execute queries"],

                        "collaborators": []

                    }

                }'''

            elif "auth" in prompt_lower:

                return '''{

                    "command_type": "CREATE_COMPONENT",

                    "parameters": {

                        "name": "AuthService",

                        "responsibilities": ["Validate credentials", "Generate tokens"],

                        "collaborators": ["Database"]

                    }

                }'''

                

        # Detect snapshot/save commands

        elif "snapshot" in prompt_lower or "save" in prompt_lower:

            return '''{

                "command_type": "SNAPSHOT",

                "parameters": {}

            }'''

            

        # Detect finish/done commands

        elif "finish" in prompt_lower or "done" in prompt_lower or "complete" in prompt_lower:

            return '''{

                "command_type": "FINISH",

                "parameters": {}

            }'''

            

        # Default response

        return '''{

            "command_type": "CREATE_COMPONENT",

            "parameters": {

                "name": "Component",

                "responsibilities": ["Do something"],

                "collaborators": []

            }

        }'''

        

    def cleanup(self):

        """No cleanup needed for mock provider."""

        pass



class LocalTransformersProvider(LLMProvider):

    """

    Local LLM provider using HuggingFace Transformers with full GPU support.

    

    Automatically detects and configures for CUDA, ROCm, MPS, Intel, or CPU.

    Supports quantization for memory-constrained environments.

    """

    

    def __init__(self):

        """Initialize the local transformers provider."""

        self.model = None

        self.tokenizer = None

        self.device = None

        self.backend = None

        self.gpu_info = None

        self.model_name = None

        

    def initialize(self, 

                  model_name="mistralai/Mistral-7B-Instruct-v0.2",

                  temperature=0.7, 

                  max_tokens=2048,

                  load_in_8bit=False,

                  load_in_4bit=False,

                  trust_remote_code=False,

                  **kwargs):

        """

        Initialize the local LLM with automatic hardware detection.

        

        Args:

            model_name: HuggingFace model identifier

            temperature: Sampling temperature for generation

            max_tokens: Maximum number of tokens to generate

            load_in_8bit: Use 8-bit quantization (requires bitsandbytes)

            load_in_4bit: Use 4-bit quantization (requires bitsandbytes)

            trust_remote_code: Allow custom model code execution

            **kwargs: Additional parameters passed to model loading

        """

        

        print("Initializing Local LLM Provider...")

        print(f"Model: {model_name}")

        print()

        

        # Detect hardware

        detector = EnhancedHardwareDetector()

        self.backend, self.gpu_info = detector.detect_gpu_backend()

        detector.print_hardware_info()

        

        # Import required libraries

        import torch

        from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig

        

        self.model_name = model_name

        self.temperature = temperature

        self.max_tokens = max_tokens

        

        # Configure device and dtype based on detected backend

        device_map = None

        torch_dtype = torch.float32

        quantization_config = None

        

        if self.backend == "cuda":

            print("Configuring for NVIDIA CUDA...")

            self.device = torch.device("cuda")

            torch_dtype = torch.float16

            device_map = "auto"

            

            # Configure quantization if requested

            if load_in_8bit or load_in_4bit:

                try:

                    quantization_config = BitsAndBytesConfig(

                        load_in_8bit=load_in_8bit,

                        load_in_4bit=load_in_4bit,

                        bnb_4bit_compute_dtype=torch.float16 if load_in_4bit else None,

                        bnb_4bit_use_double_quant=True if load_in_4bit else None,

                        bnb_4bit_quant_type="nf4" if load_in_4bit else None

                    )

                    print(f"Using {'8-bit' if load_in_8bit else '4-bit'} quantization")

                except Exception as e:

                    print(f"Warning: Could not configure quantization: {e}")

                    print("Falling back to float16")

                    

        elif self.backend == "rocm":

            print("Configuring for AMD ROCm...")

            self.device = torch.device("cuda")  # ROCm uses CUDA API

            torch_dtype = torch.float16

            device_map = "auto"

            

            # ROCm may have limited quantization support

            if load_in_8bit or load_in_4bit:

                print("Warning: Quantization support on ROCm may be limited")

                print("Attempting to use quantization...")

                try:

                    quantization_config = BitsAndBytesConfig(

                        load_in_8bit=load_in_8bit,

                        load_in_4bit=load_in_4bit

                    )

                except Exception as e:

                    print(f"Quantization failed: {e}")

                    print("Falling back to float16")

                    

        elif self.backend == "mps":

            print("Configuring for Apple Metal Performance Shaders...")

            self.device = torch.device("mps")

            torch_dtype = torch.float32  # MPS works better with float32

            device_map = None

            

            # MPS does not support quantization

            if load_in_8bit or load_in_4bit:

                print("Warning: MPS does not support quantization")

                print("Using float32 instead")

                

        elif self.backend == "intel":

            print("Configuring for Intel GPU...")

            try:

                import intel_extension_for_pytorch as ipex

                self.device = torch.device("xpu")

                torch_dtype = torch.float16

                device_map = None

            except ImportError:

                print("Intel Extension not available, falling back to CPU")

                self.device = torch.device("cpu")

                torch_dtype = torch.float32

                device_map = None

                

        else:

            print("Configuring for CPU...")

            self.device = torch.device("cpu")

            torch_dtype = torch.float32

            device_map = None

        

        print(f"Device: {self.device}")

        print(f"Data Type: {torch_dtype}")

        print()

        

        # Load tokenizer

        print("Loading tokenizer...")

        self.tokenizer = AutoTokenizer.from_pretrained(

            model_name,

            trust_remote_code=trust_remote_code

        )

        

        # Set pad token if not present

        if self.tokenizer.pad_token is None:

            self.tokenizer.pad_token = self.tokenizer.eos_token

        

        print("Tokenizer loaded successfully")

        print()

        

        # Load model

        print("Loading model (this may take several minutes)...")

        

        try:

            self.model = AutoModelForCausalLM.from_pretrained(

                model_name,

                torch_dtype=torch_dtype,

                device_map=device_map,

                quantization_config=quantization_config,

                low_cpu_mem_usage=True,

                trust_remote_code=trust_remote_code,

                **kwargs

            )

            

            # Move model to device if not using device_map

            if device_map is None:

                self.model.to(self.device)

            

            # Apply Intel optimizations if available

            if self.backend == "intel":

                try:

                    import intel_extension_for_pytorch as ipex

                    self.model = ipex.optimize(self.model)

                    print("Applied Intel optimizations")

                except:

                    pass

            

            print("Model loaded successfully")

            print()

            

            # Print memory usage if on GPU

            if self.backend in ["cuda", "rocm"]:

                allocated = torch.cuda.memory_allocated() / 1024**3

                reserved = torch.cuda.memory_reserved() / 1024**3

                print(f"GPU Memory Allocated: {allocated:.2f} GB")

                print(f"GPU Memory Reserved: {reserved:.2f} GB")

                print()

                

        except Exception as e:

            print(f"Error loading model: {e}")

            raise

            

    def generate(self, prompt, system_prompt=None):

        """

        Generate a response from the local LLM.

        

        Args:

            prompt: User input text

            system_prompt: Optional system instructions

            

        Returns:

            Generated text response

        """

        

        import torch

        

        # Construct full prompt

        if system_prompt:

            full_prompt = f"{system_prompt}\n\nUser: {prompt}\n\nAssistant:"

        else:

            full_prompt = prompt

        

        # Tokenize input

        inputs = self.tokenizer(

            full_prompt, 

            return_tensors="pt",

            padding=True,

            truncation=True,

            max_length=4096

        )

        

        # Move inputs to device

        inputs = {k: v.to(self.device) for k, v in inputs.items()}

        

        # Generate response

        with torch.no_grad():

            outputs = self.model.generate(

                **inputs,

                max_new_tokens=self.max_tokens,

                temperature=self.temperature,

                do_sample=True,

                top_p=0.95,

                top_k=50,

                pad_token_id=self.tokenizer.pad_token_id,

                eos_token_id=self.tokenizer.eos_token_id

            )

        

        # Decode generated text

        generated_text = self.tokenizer.decode(

            outputs[0], 

            skip_special_tokens=True

        )

        

        # Extract only the new generated portion

        response = generated_text[len(full_prompt):].strip()

        

        return response

        

    def cleanup(self):

        """Release GPU memory and cleanup resources."""

        

        import torch

        

        print("Cleaning up LLM resources...")

        

        if self.model is not None:

            del self.model

            self.model = None

            

        if self.tokenizer is not None:

            del self.tokenizer

            self.tokenizer = None

        

        # Clear GPU cache

        if self.backend in ["cuda", "rocm"]:

            torch.cuda.empty_cache()

            print("GPU cache cleared")

        elif self.backend == "mps":

            torch.mps.empty_cache()

            print("MPS cache cleared")

        elif self.backend == "intel":

            try:

                torch.xpu.empty_cache()

                print("Intel XPU cache cleared")

            except:

                pass

        

        print("Cleanup complete")



class OpenAIProvider(LLMProvider):

    """

    Remote LLM provider for OpenAI API (GPT-3.5, GPT-4, etc.).

    

    Supports both the legacy Completion API and the Chat Completion API.

    """

    

    def __init__(self):

        """Initialize the OpenAI provider."""

        self.api_key = None

        self.model_name = None

        self.temperature = None

        self.max_tokens = None

        self.use_chat_api = True

        self.base_url = "https://api.openai.com/v1"

        

    def initialize(self, 

                  api_key=None, 

                  model_name="gpt-4",

                  temperature=0.7, 

                  max_tokens=2048,

                  use_chat_api=True,

                  base_url=None,

                  **kwargs):

        """

        Initialize the OpenAI provider.

        

        Args:

            api_key: OpenAI API key (or set OPENAI_API_KEY environment variable)

            model_name: Model identifier (gpt-4, gpt-3.5-turbo, etc.)

            temperature: Sampling temperature

            max_tokens: Maximum tokens to generate

            use_chat_api: Use Chat Completion API (True) or legacy Completion API (False)

            base_url: Custom API base URL (for OpenAI-compatible APIs)

        """

        

        print("Initializing OpenAI Provider...")

        

        # Get API key from parameter or environment

        self.api_key = api_key or os.getenv("OPENAI_API_KEY")

        

        if not self.api_key:

            raise ValueError(

                "OpenAI API key must be provided either as a parameter "

                "or via the OPENAI_API_KEY environment variable"

            )

        

        self.model_name = model_name

        self.temperature = temperature

        self.max_tokens = max_tokens

        self.use_chat_api = use_chat_api

        

        if base_url:

            self.base_url = base_url

        

        print(f"Model: {model_name}")

        print(f"API Type: {'Chat Completion' if use_chat_api else 'Completion'}")

        print(f"Base URL: {self.base_url}")

        print("OpenAI Provider initialized successfully")

        print()

        

    def generate(self, prompt, system_prompt=None):

        """

        Generate a response using the OpenAI API.

        

        Args:

            prompt: User input text

            system_prompt: Optional system instructions

            

        Returns:

            Generated text response

        """

        

        import requests

        import time

        

        headers = {

            "Authorization": f"Bearer {self.api_key}",

            "Content-Type": "application/json"

        }

        

        if self.use_chat_api:

            # Use Chat Completion API

            endpoint = f"{self.base_url}/chat/completions"

            

            messages = []

            if system_prompt:

                messages.append({"role": "system", "content": system_prompt})

            messages.append({"role": "user", "content": prompt})

            

            data = {

                "model": self.model_name,

                "messages": messages,

                "temperature": self.temperature,

                "max_tokens": self.max_tokens

            }

        else:

            # Use legacy Completion API

            endpoint = f"{self.base_url}/completions"

            

            full_prompt = prompt

            if system_prompt:

                full_prompt = f"{system_prompt}\n\n{prompt}"

            

            data = {

                "model": self.model_name,

                "prompt": full_prompt,

                "temperature": self.temperature,

                "max_tokens": self.max_tokens

            }

        

        # Make API request with retry logic

        max_retries = 3

        retry_delay = 1

        

        for attempt in range(max_retries):

            try:

                response = requests.post(

                    endpoint,

                    headers=headers,

                    json=data,

                    timeout=60

                )

                

                response.raise_for_status()

                result = response.json()

                

                if self.use_chat_api:

                    return result["choices"][0]["message"]["content"]

                else:

                    return result["choices"][0]["text"]

                    

            except requests.exceptions.RequestException as e:

                if attempt < max_retries - 1:

                    print(f"Request failed (attempt {attempt + 1}/{max_retries}): {e}")

                    print(f"Retrying in {retry_delay} seconds...")

                    time.sleep(retry_delay)

                    retry_delay *= 2

                else:

                    raise Exception(f"OpenAI API request failed after {max_retries} attempts: {e}")

        

    def cleanup(self):

        """No cleanup needed for API-based provider."""

        pass



class AnthropicProvider(LLMProvider):

    """

    Remote LLM provider for Anthropic Claude API.

    

    Supports Claude 3 (Opus, Sonnet, Haiku) and Claude 2 models.

    """

    

    def __init__(self):

        """Initialize the Anthropic provider."""

        self.api_key = None

        self.model_name = None

        self.temperature = None

        self.max_tokens = None

        self.base_url = "https://api.anthropic.com/v1"

        

    def initialize(self,

                  api_key=None,

                  model_name="claude-3-sonnet-20240229",

                  temperature=0.7,

                  max_tokens=2048,

                  **kwargs):

        """

        Initialize the Anthropic provider.

        

        Args:

            api_key: Anthropic API key (or set ANTHROPIC_API_KEY environment variable)

            model_name: Model identifier (claude-3-opus, claude-3-sonnet, etc.)

            temperature: Sampling temperature

            max_tokens: Maximum tokens to generate

        """

        

        print("Initializing Anthropic Provider...")

        

        self.api_key = api_key or os.getenv("ANTHROPIC_API_KEY")

        

        if not self.api_key:

            raise ValueError(

                "Anthropic API key must be provided either as a parameter "

                "or via the ANTHROPIC_API_KEY environment variable"

            )

        

        self.model_name = model_name

        self.temperature = temperature

        self.max_tokens = max_tokens

        

        print(f"Model: {model_name}")

        print("Anthropic Provider initialized successfully")

        print()

        

    def generate(self, prompt, system_prompt=None):

        """

        Generate a response using the Anthropic API.

        

        Args:

            prompt: User input text

            system_prompt: Optional system instructions

            

        Returns:

            Generated text response

        """

        

        import requests

        import time

        

        headers = {

            "x-api-key": self.api_key,

            "anthropic-version": "2023-06-01",

            "Content-Type": "application/json"

        }

        

        messages = [{"role": "user", "content": prompt}]

        

        data = {

            "model": self.model_name,

            "messages": messages,

            "temperature": self.temperature,

            "max_tokens": self.max_tokens

        }

        

        if system_prompt:

            data["system"] = system_prompt

        

        # Make API request with retry logic

        max_retries = 3

        retry_delay = 1

        

        for attempt in range(max_retries):

            try:

                response = requests.post(

                    f"{self.base_url}/messages",

                    headers=headers,

                    json=data,

                    timeout=60

                )

                

                response.raise_for_status()

                result = response.json()

                

                return result["content"][0]["text"]

                

            except requests.exceptions.RequestException as e:

                if attempt < max_retries - 1:

                    print(f"Request failed (attempt {attempt + 1}/{max_retries}): {e}")

                    print(f"Retrying in {retry_delay} seconds...")

                    time.sleep(retry_delay)

                    retry_delay *= 2

                else:

                    raise Exception(f"Anthropic API request failed after {max_retries} attempts: {e}")

        

    def cleanup(self):

        """No cleanup needed for API-based provider."""

        pass



class GeminiProvider(LLMProvider):

    """

    Remote LLM provider for Google Gemini API.

    

    Supports Gemini Pro and Gemini Pro Vision models.

    """

    

    def __init__(self):

        """Initialize the Gemini provider."""

        self.api_key = None

        self.model_name = None

        self.temperature = None

        self.max_tokens = None

        self.base_url = "https://generativelanguage.googleapis.com/v1beta"

        

    def initialize(self,

                  api_key=None,

                  model_name="gemini-pro",

                  temperature=0.7,

                  max_tokens=2048,

                  **kwargs):

        """

        Initialize the Gemini provider.

        

        Args:

            api_key: Google API key (or set GOOGLE_API_KEY environment variable)

            model_name: Model identifier (gemini-pro, gemini-pro-vision)

            temperature: Sampling temperature

            max_tokens: Maximum tokens to generate

        """

        

        print("Initializing Google Gemini Provider...")

        

        self.api_key = api_key or os.getenv("GOOGLE_API_KEY")

        

        if not self.api_key:

            raise ValueError(

                "Google API key must be provided either as a parameter "

                "or via the GOOGLE_API_KEY environment variable"

            )

        

        self.model_name = model_name

        self.temperature = temperature

        self.max_tokens = max_tokens

        

        print(f"Model: {model_name}")

        print("Gemini Provider initialized successfully")

        print()

        

    def generate(self, prompt, system_prompt=None):

        """

        Generate a response using the Gemini API.

        

        Args:

            prompt: User input text

            system_prompt: Optional system instructions

            

        Returns:

            Generated text response

        """

        

        import requests

        import time

        

        # Construct full prompt with system instructions if provided

        full_prompt = prompt

        if system_prompt:

            full_prompt = f"{system_prompt}\n\n{prompt}"

        

        endpoint = f"{self.base_url}/models/{self.model_name}:generateContent"

        

        data = {

            "contents": [{

                "parts": [{"text": full_prompt}]

            }],

            "generationConfig": {

                "temperature": self.temperature,

                "maxOutputTokens": self.max_tokens

            }

        }

        

        # Make API request with retry logic

        max_retries = 3

        retry_delay = 1

        

        for attempt in range(max_retries):

            try:

                response = requests.post(

                    f"{endpoint}?key={self.api_key}",

                    json=data,

                    timeout=60

                )

                

                response.raise_for_status()

                result = response.json()

                

                return result["candidates"][0]["content"]["parts"][0]["text"]

                

            except requests.exceptions.RequestException as e:

                if attempt < max_retries - 1:

                    print(f"Request failed (attempt {attempt + 1}/{max_retries}): {e}")

                    print(f"Retrying in {retry_delay} seconds...")

                    time.sleep(retry_delay)

                    retry_delay *= 2

                else:

                    raise Exception(f"Gemini API request failed after {max_retries} attempts: {e}")

        

    def cleanup(self):

        """No cleanup needed for API-based provider."""

        pass



# =========================================================================

# LLM MANAGER

# =========================================================================


class EnhancedLLMManager:

    """

    Enhanced LLM manager supporting multiple local and remote providers.

    

    Provides a unified interface for all LLM providers and automatically

    handles hardware detection and optimal configuration.

    """

    

    def __init__(self):

        """Initialize the LLM manager."""

        self.provider = None

        self.provider_type = None

        

    def setup(self, provider_type="local", **kwargs):

        """

        Create and initialize the appropriate LLM provider.

        

        Args:

            provider_type: One of "local", "openai", "anthropic", "gemini", "mock"

            **kwargs: Provider-specific configuration parameters

        """

        

        print("=" * 70)

        print("LLM MANAGER SETUP")

        print("=" * 70)

        print(f"Provider Type: {provider_type}")

        print()

        

        self.provider_type = provider_type

        

        if provider_type == "local":

            self.provider = LocalTransformersProvider()

        elif provider_type == "openai":

            self.provider = OpenAIProvider()

        elif provider_type == "anthropic":

            self.provider = AnthropicProvider()

        elif provider_type == "gemini":

            self.provider = GeminiProvider()

        elif provider_type == "mock":

            self.provider = MockLLMProvider()

        else:

            raise ValueError(f"Unknown provider type: {provider_type}")

        

        self.provider.initialize(**kwargs)

        

        print("=" * 70)

        print("LLM MANAGER READY")

        print("=" * 70)

        print()

        

    def query(self, prompt, system_prompt=None):

        """

        Query the LLM provider.

        

        Args:

            prompt: User input text

            system_prompt: Optional system instructions

            

        Returns:

            Generated text response

        """

        if self.provider is None:

            raise RuntimeError("LLM provider not initialized. Call setup() first.")

        return self.provider.generate(prompt, system_prompt)

        

    def shutdown(self):

        """Shutdown the LLM provider and release resources."""

        if self.provider is not None:

            self.provider.cleanup()

            self.provider = None



# =========================================================================

# COMMAND PARSER

# =========================================================================


class Command:

    """

    Represents a parsed command extracted from user input.

    

    Commands have a type (CREATE_COMPONENT, MODIFY_COMPONENT, etc.)

    and parameters specific to that command type.

    """

    

    def __init__(self, command_type, parameters):

        """

        Initialize a command.

        

        Args:

            command_type: Type of command

            parameters: Dictionary of command parameters

        """

        self.command_type = command_type

        self.parameters = parameters

        

    @staticmethod

    def from_dict(data):

        """

        Deserialize from dictionary.

        

        Args:

            data: Dictionary with command_type and parameters

            

        Returns:

            Command object

        """

        return Command(data["command_type"], data.get("parameters", {}))

        

    def to_dict(self):

        """

        Serialize to dictionary.

        

        Returns:

            Dictionary representation

        """

        return {

            "command_type": self.command_type,

            "parameters": self.parameters

        }

        

    def __repr__(self):

        """String representation for debugging."""

        return f"Command(type='{self.command_type}', params={self.parameters})"



class CommandParser:

    """

    Parses natural language input into structured commands.

    

    Uses the LLM to analyze user input and extract structured commands

    that the system can execute.

    """

    

    def __init__(self, llm_manager):

        """

        Initialize the command parser.

        

        Args:

            llm_manager: EnhancedLLMManager instance

        """

        self.llm = llm_manager

        self.system_prompt = self._build_system_prompt()

        

    def _build_system_prompt(self):

        """

        Build the system prompt for command parsing.

        

        Returns:

            System prompt string

        """

        return """You are a command parser for a CRC card diagramming tool.

Your task is to analyze user input and extract structured commands.

Available command types:

  1. CREATE_COMPONENT: Create a new CRC card
  2. MODIFY_COMPONENT: Modify an existing CRC card
  3. DELETE_COMPONENT: Remove a CRC card
  4. SNAPSHOT: Save the current diagram
  5. RESTORE: Load a saved diagram
  6. FINISH: User indicates the diagram is complete

For each user input, respond with a JSON object containing: { "command_type": "CREATE_COMPONENT|MODIFY_COMPONENT|DELETE_COMPONENT|SNAPSHOT|RESTORE|FINISH", "parameters": { // command-specific parameters } }

CREATE_COMPONENT parameters:

  • name: component name (string)
  • responsibilities: list of responsibility descriptions (list of strings)
  • collaborators: list of collaborator names (list of strings)

MODIFY_COMPONENT parameters:

  • name: component name to modify (string)
  • add_responsibilities: responsibilities to add (list of strings, optional)
  • remove_responsibilities: responsibilities to remove (list of strings, optional)
  • add_collaborators: collaborators to add (list of strings, optional)
  • remove_collaborators: collaborators to remove (list of strings, optional)

DELETE_COMPONENT parameters:

  • name: component name to delete (string)

SNAPSHOT parameters:

  • filename: base filename for saved files (string, optional)

RESTORE parameters:

  • filename: JSON file to restore from (string)

FINISH parameters: none

Respond ONLY with the JSON object, no additional text."""

    def parse(self, user_input):

        """

        Parse user input into a structured command.

        

        Args:

            user_input: Natural language input from user

            

        Returns:

            Command object or None if parsing fails

        """

        try:

            # Query the LLM with the user input

            response = self.llm.query(user_input, self.system_prompt)

            

            # Extract JSON from response

            json_str = self._extract_json(response)

            

            # Parse JSON

            command_data = json.loads(json_str)

            

            # Create and return Command object

            return Command.from_dict(command_data)

            

        except Exception as e:

            print(f"Error parsing command: {e}")

            return None

            

    def _extract_json(self, text):

        """

        Extract JSON object from text that may contain additional content.

        

        Args:

            text: Text containing JSON

            

        Returns:

            Extracted JSON string

        """

        # Look for content between first { and last }

        start = text.find('{')

        end = text.rfind('}')

        

        if start == -1 or end == -1:

            raise ValueError("No JSON object found in response")

            

        return text[start:end+1]



# =========================================================================

# STATE MANAGEMENT

# =========================================================================


class DiagramState:

    """

    Represents the complete state of a CRC card diagram.

    

    Includes all cards, their positions, connections, and metadata.

    Supports serialization to JSON for persistence.

    """

    

    def __init__(self):

        """Initialize an empty diagram state."""

        self.cards = []

        self.positions = {}

        self.connections = []

        self.metadata = {

            "version": "1.0",

            "created": datetime.now().isoformat(),

            "modified": datetime.now().isoformat()

        }

        

    def add_card(self, card):

        """

        Add a new CRC card to the diagram.

        

        Args:

            card: CRCCard object to add

        """

        if card.card_id not in [c.card_id for c in self.cards]:

            self.cards.append(card)

            self._update_modified()

            

    def remove_card(self, card_id):

        """

        Remove a card and all connections involving it.

        

        Args:

            card_id: ID of the card to remove

        """

        self.cards = [c for c in self.cards if c.card_id != card_id]

        self.connections = [conn for conn in self.connections 

                          if conn.source_id != card_id and 

                          conn.target_id != card_id]

        if card_id in self.positions:

            del self.positions[card_id]

        self._update_modified()

        

    def get_card_by_name(self, name):

        """

        Find a card by its name.

        

        Args:

            name: Name of the card to find

            

        Returns:

            CRCCard object or None if not found

        """

        for card in self.cards:

            if card.name == name:

                return card

        return None

        

    def get_card_by_id(self, card_id):

        """

        Find a card by its ID.

        

        Args:

            card_id: ID of the card to find

            

        Returns:

            CRCCard object or None if not found

        """

        for card in self.cards:

            if card.card_id == card_id:

                return card

        return None

        

    def update_layout(self, positions, connections):

        """

        Update the layout information.

        

        Args:

            positions: Dictionary mapping card_id to Position

            connections: List of Connection objects

        """

        self.positions = positions

        self.connections = connections

        self._update_modified()

        

    def _update_modified(self):

        """Update the modified timestamp."""

        self.metadata["modified"] = datetime.now().isoformat()

        

    def to_dict(self):

        """

        Serialize the entire state to a dictionary.

        

        Returns:

            Dictionary representation

        """

        return {

            "metadata": self.metadata,

            "cards": [card.to_dict() for card in self.cards],

            "positions": {card_id: pos.to_dict() 

                        for card_id, pos in self.positions.items()},

            "connections": [conn.to_dict() for conn in self.connections]

        }

        

    def to_json(self):

        """

        Serialize to JSON string.

        

        Returns:

            JSON string representation

        """

        return json.dumps(self.to_dict(), indent=2)

        

    @staticmethod

    def from_dict(data):

        """

        Deserialize from dictionary.

        

        Args:

            data: Dictionary representation

            

        Returns:

            DiagramState object

        """

        state = DiagramState()

        state.metadata = data.get("metadata", state.metadata)

        state.cards = [CRCCard.from_dict(c) for c in data.get("cards", [])]

        state.positions = {card_id: Position.from_dict(pos) 

                         for card_id, pos in data.get("positions", {}).items()}

        state.connections = [Connection.from_dict(c) 

                           for c in data.get("connections", [])]

        return state

        

    @staticmethod

    def from_json(json_str):

        """

        Deserialize from JSON string.

        

        Args:

            json_str: JSON string representation

            

        Returns:

            DiagramState object

        """

        data = json.loads(json_str)

        return DiagramState.from_dict(data)



class StateManager:

    """

    Manages diagram state and executes commands.

    

    Coordinates between the domain model, layout engine, and rendering

    engine to maintain consistent state and produce visual outputs.

    """

    

    def __init__(self, layout_engine, rendering_engine):

        """

        Initialize the state manager.

        

        Args:

            layout_engine: LayoutEngine instance

            rendering_engine: RenderingEngine instance

        """

        self.state = DiagramState()

        self.layout_engine = layout_engine

        self.rendering_engine = rendering_engine

        

    def execute_command(self, command):

        """

        Execute a command and update the state.

        

        Args:

            command: Command object to execute

            

        Returns:

            Result string ("SUCCESS", "FINISH", etc.)

        """

        if command.command_type == "CREATE_COMPONENT":

            self._execute_create(command.parameters)

        elif command.command_type == "MODIFY_COMPONENT":

            self._execute_modify(command.parameters)

        elif command.command_type == "DELETE_COMPONENT":

            self._execute_delete(command.parameters)

        elif command.command_type == "SNAPSHOT":

            self._execute_snapshot(command.parameters)

        elif command.command_type == "RESTORE":

            self._execute_restore(command.parameters)

        elif command.command_type == "FINISH":

            return "FINISH"

            

        # Recompute layout after any modification

        if command.command_type in ["CREATE_COMPONENT", "MODIFY_COMPONENT", 

                                   "DELETE_COMPONENT"]:

            self._recompute_layout()

            

        return "SUCCESS"

        

    def _execute_create(self, params):

        """

        Create a new CRC card.

        

        Args:

            params: Dictionary with name, responsibilities, collaborators

        """

        name = params.get("name")

        responsibilities = params.get("responsibilities", [])

        collaborators = params.get("collaborators", [])

        

        # Generate unique ID

        card_id = str(uuid.uuid4())

        

        # Create card

        card = CRCCard(card_id, name)

        

        # Add responsibilities

        for resp_desc in responsibilities:

            card.add_responsibility(Responsibility(resp_desc))

            

        # Add collaborations (resolve collaborator IDs)

        for collab_name in collaborators:

            # Try to find existing card with this name

            existing_card = self.state.get_card_by_name(collab_name)

            if existing_card:

                collab_id = existing_card.card_id

            else:

                # Create placeholder ID for not-yet-created collaborator

                collab_id = f"placeholder_{collab_name}"

                

            card.add_collaboration(Collaboration(collab_id, collab_name))

            

        self.state.add_card(card)

        

    def _execute_modify(self, params):

        """

        Modify an existing CRC card.

        

        Args:

            params: Dictionary with modification parameters

        """

        name = params.get("name")

        card = self.state.get_card_by_name(name)

        

        if not card:

            print(f"Card '{name}' not found")

            return

            

        # Add responsibilities

        for resp_desc in params.get("add_responsibilities", []):

            card.add_responsibility(Responsibility(resp_desc))

            

        # Remove responsibilities

        for resp_desc in params.get("remove_responsibilities", []):

            card.remove_responsibility(Responsibility(resp_desc))

            

        # Add collaborations

        for collab_name in params.get("add_collaborators", []):

            existing_card = self.state.get_card_by_name(collab_name)

            if existing_card:

                collab_id = existing_card.card_id

            else:

                collab_id = f"placeholder_{collab_name}"

                

            card.add_collaboration(Collaboration(collab_id, collab_name))

            

        # Remove collaborations

        for collab_name in params.get("remove_collaborators", []):

            existing_card = self.state.get_card_by_name(collab_name)

            if existing_card:

                card.remove_collaboration(existing_card.card_id)

                

    def _execute_delete(self, params):

        """

        Delete a CRC card.

        

        Args:

            params: Dictionary with name parameter

        """

        name = params.get("name")

        card = self.state.get_card_by_name(name)

        

        if card:

            self.state.remove_card(card.card_id)

        else:

            print(f"Card '{name}' not found")

            

    def _execute_snapshot(self, params):

        """

        Save the current state to files.

        

        Args:

            params: Dictionary with optional filename parameter

        """

        filename = params.get("filename", f"diagram_{datetime.now().strftime('%Y%m%d_%H%M%S')}")

        

        # Ensure filename has no extension

        base_filename = os.path.splitext(filename)[0]

        

        # Save JSON

        json_path = f"{base_filename}.json"

        with open(json_path, 'w') as f:

            f.write(self.state.to_json())

        print(f"Saved state to {json_path}")

        

        # Save PNG

        png_path = f"{base_filename}.png"

        self.rendering_engine.render_to_png(

            self.state.cards,

            self.state.positions,

            self.state.connections,

            png_path

        )

        print(f"Saved diagram to {png_path}")

        

        # Save SVG

        svg_path = f"{base_filename}.svg"

        self.rendering_engine.render_to_svg(

            self.state.cards,

            self.state.positions,

            self.state.connections,

            svg_path

        )

        print(f"Saved diagram to {svg_path}")

        

    def _execute_restore(self, params):

        """

        Restore state from a JSON file.

        

        Args:

            params: Dictionary with filename parameter

        """

        filename = params.get("filename")

        

        if not os.path.exists(filename):

            print(f"File '{filename}' not found")

            return

            

        with open(filename, 'r') as f:

            json_str = f.read()

            

        self.state = DiagramState.from_json(json_str)

        print(f"Restored state from {filename}")

        

    def _recompute_layout(self):

        """Recompute the layout for the current cards."""

        positions, connections = self.layout_engine.compute_layout(self.state.cards)

        self.state.update_layout(positions, connections)

        

    def get_current_state(self):

        """

        Get the current diagram state.

        

        Returns:

            DiagramState object

        """

        return self.state



# =========================================================================

# MAIN APPLICATION

# =========================================================================


class CRCDiagramTool:

    """

    Main application for CRC card diagramming.

    

    Provides an interactive command-line interface for creating and

    managing CRC card diagrams using natural language commands.

    """

    

    def __init__(self, llm_provider_type="mock", **llm_kwargs):

        """

        Initialize the CRC diagram tool.

        

        Args:

            llm_provider_type: Type of LLM provider to use

            **llm_kwargs: Provider-specific configuration

        """

        self.llm_manager = EnhancedLLMManager()

        self.llm_manager.setup(llm_provider_type, **llm_kwargs)

        

        self.command_parser = CommandParser(self.llm_manager)

        self.layout_engine = LayoutEngine()

        self.rendering_engine = RenderingEngine()

        self.state_manager = StateManager(self.layout_engine, 

                                         self.rendering_engine)

        

    def run(self):

        """Main application loop."""

        print("=" * 70)

        print("CRC DIAGRAM TOOL")

        print("=" * 70)

        print("Enter natural language descriptions of your components.")

        print("Type 'snapshot' to save the current diagram.")

        print("Type 'finish' when done.")

        print()

        

        while True:

            # Get user input

            user_input = input("You: ").strip()

            

            if not user_input:

                continue

                

            # Parse command

            command = self.command_parser.parse(user_input)

            

            if command is None:

                print("Sorry, I didn't understand that. Please try again.")

                continue

                

            # Execute command

            result = self.state_manager.execute_command(command)

            

            if result == "FINISH":

                print("Diagram complete. Goodbye!")

                break

            elif result == "SUCCESS":

                print("Command executed successfully.")

                self._show_summary()

                

        # Cleanup

        self.llm_manager.shutdown()

        

    def _show_summary(self):

        """Display a summary of the current diagram."""

        state = self.state_manager.get_current_state()

        print(f"\nCurrent diagram has {len(state.cards)} components:")

        for card in state.cards:

            print(f"  - {card.name} ({len(card.responsibilities)} responsibilities, "

                 f"{len(card.collaborations)} collaborations)")

        print()



# =========================================================================

# ENTRY POINT

# =========================================================================


def main():

    """

    Main entry point for the application.

    

    Parses command-line arguments and starts the tool with the

    appropriate configuration.

    """

    import argparse

    

    parser = argparse.ArgumentParser(

        description='CRC Diagram Tool - Create CRC card diagrams using natural language'

    )

    

    parser.add_argument(

        '--provider',

        choices=['mock', 'local', 'openai', 'anthropic', 'gemini'],

        default='mock',

        help='LLM provider to use (default: mock)'

    )

    

    parser.add_argument(

        '--model',

        type=str,

        help='Model name/identifier for the LLM provider'

    )

    

    parser.add_argument(

        '--api-key',

        type=str,

        help='API key for remote providers (or set via environment variable)'

    )

    

    parser.add_argument(

        '--temperature',

        type=float,

        default=0.7,

        help='Sampling temperature (default: 0.7)'

    )

    

    parser.add_argument(

        '--max-tokens',

        type=int,

        default=2048,

        help='Maximum tokens to generate (default: 2048)'

    )

    

    parser.add_argument(

        '--quantize',

        choices=['none', '8bit', '4bit'],

        default='none',

        help='Quantization mode for local models (default: none)'

    )

    

    args = parser.parse_args()

    

    # Build kwargs for LLM provider

    llm_kwargs = {

        'temperature': args.temperature,

        'max_tokens': args.max_tokens

    }

    

    if args.model:

        llm_kwargs['model_name'] = args.model

    

    if args.api_key:

        llm_kwargs['api_key'] = args.api_key

    

    if args.provider == 'local':

        if args.quantize == '8bit':

            llm_kwargs['load_in_8bit'] = True

        elif args.quantize == '4bit':

            llm_kwargs['load_in_4bit'] = True

    

    # Create and run the tool

    try:

        tool = CRCDiagramTool(llm_provider_type=args.provider, **llm_kwargs)

        tool.run()

    except KeyboardInterrupt:

        print("\n\nInterrupted by user. Exiting...")

    except Exception as e:

        print(f"\nError: {e}")

        import traceback

        traceback.print_exc()

        sys.exit(1)



if __name__ == "__main__":

    main()


USAGE INSTRUCTIONS

To run this complete example, save it to a file named crc_diagram_tool.py and execute it with Python 3.7 or later.


Basic usage with mock provider (no dependencies required):


python crc_diagram_tool.py


Usage with local LLM on NVIDIA GPU:


python crc_diagram_tool.py --provider local --model mistralai/Mistral-7B-Instruct-v0.2


Usage with local LLM with 4-bit quantization:


python crc_diagram_tool.py --provider local --model mistralai/Mistral-7B-Instruct-v0.2 --quantize 4bit


Usage with OpenAI GPT-4:


export OPENAI_API_KEY=your-api-key-here

python crc_diagram_tool.py --provider openai --model gpt-4


Usage with Anthropic Claude:


export ANTHROPIC_API_KEY=your-api-key-here

python crc_diagram_tool.py --provider anthropic --model claude-3-sonnet-20240229


Usage with Google Gemini:


export GOOGLE_API_KEY=your-api-key-here

python crc_diagram_tool.py --provider gemini --model gemini-pro


The tool will automatically detect your GPU hardware and configure itself for optimal performance. All generated diagrams are saved as PNG, SVG, and JSON files in the current directory.