Skip to main content
Glama
context_enricher.py19.8 kB
"""Context Enricher - Enhances conversation context with FreeCAD state and history""" from datetime import datetime from typing import Any, Dict, List, Optional import FreeCAD import FreeCADGui class ContextEnricher: """ Enriches conversation context with current FreeCAD state, document information, and interaction history. """ def __init__(self): """Initialize the Context Enricher""" self.context_history = [] self.max_history_items = 10 self.object_cache = {} # Context extraction configuration self.config = { "include_document_info": True, "include_selection": True, "include_objects": True, "include_constraints": True, "include_materials": True, "include_view_info": True, "include_recent_commands": True, "max_objects_detail": 50, "summarize_large_documents": True, } def enrich(self, base_context: Optional[Dict] = None) -> Dict[str, Any]: """ Enrich the base context with FreeCAD state information Args: base_context: Optional base context to enrich Returns: Enriched context dictionary """ context = base_context.copy() if base_context else {} # Add timestamp context["timestamp"] = datetime.now().isoformat() context["enrichment_version"] = "1.0" # Extract various context components if self.config["include_document_info"]: context["document"] = self._extract_document_info() if self.config["include_selection"]: context["selection"] = self._extract_selection_info() if self.config["include_objects"]: context["objects"] = self._extract_objects_info() if self.config["include_constraints"]: context["constraints"] = self._extract_constraints_info() if self.config["include_materials"]: context["materials"] = self._extract_materials_info() if self.config["include_view_info"]: context["view"] = self._extract_view_info() if self.config["include_recent_commands"]: context["recent_commands"] = self._extract_recent_commands() # Add context summary context["summary"] = self._generate_context_summary(context) # Add to history self._add_to_history(context) return context def _extract_document_info(self) -> Dict[str, Any]: """Extract information about the active document""" doc_info = { "has_active_document": False, "name": None, "label": None, "filename": None, "modified": False, "object_count": 0, "undo_count": 0, "redo_count": 0, } try: doc = FreeCAD.ActiveDocument if doc: doc_info["has_active_document"] = True doc_info["name"] = doc.Name doc_info["label"] = doc.Label doc_info["filename"] = ( doc.FileName if hasattr(doc, "FileName") else None ) doc_info["modified"] = ( doc.Modified if hasattr(doc, "Modified") else False ) doc_info["object_count"] = len(doc.Objects) doc_info["undo_count"] = ( doc.UndoCount if hasattr(doc, "UndoCount") else 0 ) doc_info["redo_count"] = ( doc.RedoCount if hasattr(doc, "RedoCount") else 0 ) # Document properties doc_info["properties"] = self._extract_properties(doc) except Exception as e: doc_info["error"] = str(e) return doc_info def _extract_selection_info(self) -> Dict[str, Any]: """Extract information about current selection""" selection_info = { "count": 0, "objects": [], "sub_objects": [], "has_selection": False, } try: if FreeCADGui: selection = FreeCADGui.Selection.getSelection() selection_info["count"] = len(selection) selection_info["has_selection"] = len(selection) > 0 for obj in selection[:10]: # Limit to first 10 obj_info = { "name": obj.Name, "label": obj.Label, "type": obj.TypeId, "properties": self._extract_properties(obj, detailed=False), } # Check for shape if hasattr(obj, "Shape"): obj_info["shape_type"] = ( obj.Shape.ShapeType if obj.Shape else None ) if obj.Shape: obj_info["shape_info"] = { "volume": ( obj.Shape.Volume if hasattr(obj.Shape, "Volume") else None ), "area": ( obj.Shape.Area if hasattr(obj.Shape, "Area") else None ), "length": ( obj.Shape.Length if hasattr(obj.Shape, "Length") else None ), "is_valid": ( obj.Shape.isValid() if hasattr(obj.Shape, "isValid") else None ), } selection_info["objects"].append(obj_info) # Get sub-object selection sel_ex = FreeCADGui.Selection.getSelectionEx() for sel in sel_ex: if sel.SubElementNames: selection_info["sub_objects"].append( { "object": sel.ObjectName, "sub_elements": sel.SubElementNames, } ) except Exception as e: selection_info["error"] = str(e) return selection_info def _extract_objects_info(self) -> Dict[str, Any]: """Extract information about document objects""" objects_info = { "total_count": 0, "by_type": {}, "object_tree": [], "details": [], } try: doc = FreeCAD.ActiveDocument if not doc: return objects_info objects_info["total_count"] = len(doc.Objects) # Count by type for obj in doc.Objects: type_id = obj.TypeId if type_id not in objects_info["by_type"]: objects_info["by_type"][type_id] = 0 objects_info["by_type"][type_id] += 1 # Build object tree (limited) root_objects = [ obj for obj in doc.Objects if not hasattr(obj, "InList") or not obj.InList ] for obj in root_objects[:10]: # Limit root objects objects_info["object_tree"].append( self._build_object_tree(obj, max_depth=3) ) # Detailed info for recent/important objects if ( self.config["summarize_large_documents"] and len(doc.Objects) > self.config["max_objects_detail"] ): # Just get the most recent objects recent_objects = sorted( doc.Objects, key=lambda x: x.ID if hasattr(x, "ID") else 0, reverse=True, )[:10] else: recent_objects = doc.Objects[: self.config["max_objects_detail"]] for obj in recent_objects: obj_detail = self._extract_object_detail(obj) if obj_detail: objects_info["details"].append(obj_detail) except Exception as e: objects_info["error"] = str(e) return objects_info def _extract_object_detail(self, obj) -> Optional[Dict[str, Any]]: """Extract detailed information about an object""" try: detail = { "name": obj.Name, "label": obj.Label, "type": obj.TypeId, "id": obj.ID if hasattr(obj, "ID") else None, "visibility": obj.Visibility if hasattr(obj, "Visibility") else None, } # Shape information if hasattr(obj, "Shape") and obj.Shape: shape = obj.Shape detail["shape"] = { "type": shape.ShapeType, "faces": len(shape.Faces) if hasattr(shape, "Faces") else 0, "edges": len(shape.Edges) if hasattr(shape, "Edges") else 0, "vertices": ( len(shape.Vertexes) if hasattr(shape, "Vertexes") else 0 ), "is_solid": ( shape.isClosed() if hasattr(shape, "isClosed") else None ), } # Placement if hasattr(obj, "Placement"): placement = obj.Placement detail["placement"] = { "position": list(placement.Base), "rotation": { "axis": list(placement.Rotation.Axis), "angle": placement.Rotation.Angle, }, } # Key properties detail["properties"] = self._extract_properties(obj, detailed=False) return detail except (AttributeError, TypeError, RuntimeError) as e: # AttributeError: FreeCAD object missing expected attributes # TypeError: Invalid type conversion or property access # RuntimeError: FreeCAD object in invalid state import logging logging.getLogger(__name__).debug(f"Failed to extract object detail: {e}") return None def _build_object_tree(self, obj, current_depth=0, max_depth=3) -> Dict[str, Any]: """Build object hierarchy tree""" tree = { "name": obj.Name, "label": obj.Label, "type": obj.TypeId, "children": [], } if current_depth < max_depth and hasattr(obj, "OutList"): for child in obj.OutList[:5]: # Limit children tree["children"].append( self._build_object_tree(child, current_depth + 1, max_depth) ) return tree def _extract_properties(self, obj, detailed: bool = True) -> Dict[str, Any]: """Extract object properties""" properties = {} try: # Get property list if hasattr(obj, "PropertiesList"): prop_list = obj.PropertiesList # Filter important properties important_props = [ "Length", "Width", "Height", "Radius", "Radius1", "Radius2", "Angle", "Angle1", "Angle2", "Angle3", "Base", "Axis", "Center", "Position", "Label", "Expression", "Constrained", ] for prop_name in prop_list: if not detailed and prop_name not in important_props: continue try: value = getattr(obj, prop_name) # Convert complex types if hasattr(value, "__dict__"): # Skip complex objects in non-detailed mode if not detailed: continue value = str(value) elif hasattr(value, "__iter__") and not isinstance(value, str): value = list(value) properties[prop_name] = value except (AttributeError, TypeError, ValueError): # AttributeError: Property doesn't exist # TypeError: Property value type conversion issue # ValueError: Property value invalid continue except (AttributeError, TypeError): # AttributeError: Object missing expected attributes # TypeError: Invalid type operations pass return properties def _extract_constraints_info(self) -> Dict[str, Any]: """Extract constraint information from sketches""" constraints_info = {"total_count": 0, "by_type": {}, "sketches": []} try: doc = FreeCAD.ActiveDocument if not doc: return constraints_info # Find all sketches sketches = [ obj for obj in doc.Objects if obj.TypeId == "Sketcher::SketchObject" ] for sketch in sketches[:10]: # Limit sketches sketch_info = { "name": sketch.Name, "label": sketch.Label, "constraint_count": ( sketch.ConstraintCount if hasattr(sketch, "ConstraintCount") else 0 ), "constraints": [], } # Get constraints if hasattr(sketch, "Constraints"): for i, constraint in enumerate( sketch.Constraints[:20] ): # Limit constraints cons_info = { "index": i, "type": ( constraint.Type if hasattr(constraint, "Type") else None ), "value": ( constraint.Value if hasattr(constraint, "Value") else None ), } sketch_info["constraints"].append(cons_info) # Count by type cons_type = cons_info["type"] if cons_type: if cons_type not in constraints_info["by_type"]: constraints_info["by_type"][cons_type] = 0 constraints_info["by_type"][cons_type] += 1 constraints_info["total_count"] += 1 constraints_info["sketches"].append(sketch_info) except Exception as e: constraints_info["error"] = str(e) return constraints_info def _extract_materials_info(self) -> Dict[str, Any]: """Extract material information""" materials_info = {"assigned_materials": [], "available_materials": []} try: doc = FreeCAD.ActiveDocument if not doc: return materials_info # Find objects with materials for obj in doc.Objects: if hasattr(obj, "Material") and obj.Material: mat_info = {"object": obj.Name, "material": str(obj.Material)} materials_info["assigned_materials"].append(mat_info) except Exception as e: materials_info["error"] = str(e) return materials_info def _extract_view_info(self) -> Dict[str, Any]: """Extract 3D view information""" view_info = {"active_view": None, "camera": {}} try: if FreeCADGui: view = ( FreeCADGui.ActiveDocument.ActiveView if FreeCADGui.ActiveDocument else None ) if view: view_info["active_view"] = True # Camera info if hasattr(view, "getCameraNode"): camera = view.getCameraNode() if camera: view_info["camera"] = { "type": camera.getTypeId().getName(), "position": ( list(camera.position.getValue()) if hasattr(camera, "position") else None ), "orientation": ( str(camera.orientation.getValue()) if hasattr(camera, "orientation") else None ), } except Exception as e: view_info["error"] = str(e) return view_info def _extract_recent_commands(self) -> List[str]: """Extract recently executed commands""" # This is a placeholder - actual implementation would need to track commands return [] def _generate_context_summary(self, context: Dict) -> str: """Generate a human-readable summary of the context""" summary_parts = [] # Document summary if "document" in context and context["document"]["has_active_document"]: doc = context["document"] summary_parts.append( f"Active document: {doc['label']} with {doc['object_count']} objects" ) # Selection summary if "selection" in context and context["selection"]["has_selection"]: sel = context["selection"] summary_parts.append(f"Selected: {sel['count']} object(s)") # Objects summary if "objects" in context: obj = context["objects"] if obj["by_type"]: types = list(obj["by_type"].keys())[:3] summary_parts.append(f"Main object types: {', '.join(types)}") return ". ".join(summary_parts) if summary_parts else "Empty FreeCAD session" def _add_to_history(self, context: Dict): """Add context to history""" self.context_history.append( {"timestamp": context["timestamp"], "summary": context.get("summary", "")} ) # Limit history size if len(self.context_history) > self.max_history_items: self.context_history.pop(0) def get_history_summary(self) -> List[Dict]: """Get summary of context history""" return self.context_history.copy() def clear_history(self): """Clear context history""" self.context_history.clear() self.object_cache.clear() def update_config(self, config: Dict): """Update enricher configuration""" self.config.update(config)

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jango-blockchained/mcp-freecad'

If you have feedback or need assistance with the MCP directory API, please join our Discord server