1+ """
2+ UCKN Pattern Classification Molecule
3+
4+ Manages a hierarchical classification system for knowledge patterns,
5+ allowing patterns to be categorized for efficient retrieval and organization.
6+ """
7+
8+ import uuid
9+ import logging
10+ from datetime import datetime
11+ from typing import Dict , List , Optional , Any
12+
13+ from ...storage .chromadb_connector import ChromaDBConnector
14+
15+ class PatternClassification :
16+ """
17+ Manages a hierarchical classification system for knowledge patterns.
18+
19+ Categories are stored in a dedicated ChromaDB collection, allowing for
20+ parent-child relationships and multi-category assignment for patterns.
21+ """
22+
23+ CLASSIFICATION_COLLECTION = "pattern_classifications"
24+ PATTERN_COLLECTION = "code_patterns" # Reference to patterns, not managed here
25+
26+ def __init__ (self , chroma_connector : ChromaDBConnector ):
27+ self .chroma_connector = chroma_connector
28+ self ._logger = logging .getLogger (__name__ )
29+ self ._ensure_classification_collection ()
30+
31+ def _ensure_classification_collection (self ):
32+ """
33+ Ensure the pattern_classifications collection exists in ChromaDB.
34+ """
35+ if not hasattr (self .chroma_connector , "collections" ):
36+ self ._logger .error ("ChromaDBConnector missing 'collections' attribute." )
37+ return
38+ if self .CLASSIFICATION_COLLECTION not in self .chroma_connector .collections :
39+ try :
40+ self .chroma_connector .collections [self .CLASSIFICATION_COLLECTION ] = (
41+ self .chroma_connector .client .get_or_create_collection (
42+ name = self .CLASSIFICATION_COLLECTION ,
43+ metadata = {"description" : "UCKN pattern classifications and categories" }
44+ )
45+ )
46+ self ._logger .info (f"ChromaDB collection '{ self .CLASSIFICATION_COLLECTION } ' initialized." )
47+ except Exception as e :
48+ self ._logger .error (f"Failed to create { self .CLASSIFICATION_COLLECTION } collection: { e } " )
49+
50+ def add_category (
51+ self , category_name : str , description : Optional [str ] = None , category_id : Optional [str ] = None
52+ ) -> Optional [str ]:
53+ """
54+ Add a new pattern category.
55+
56+ Args:
57+ category_name: The name of the category.
58+ description: An optional description for the category.
59+ category_id: Optional, a specific ID for the category. If None, a UUID is generated.
60+
61+ Returns:
62+ The ID of the newly created category, or None if creation failed.
63+ """
64+ if not self .chroma_connector .is_available ():
65+ self ._logger .warning ("ChromaDB not available, cannot add category." )
66+ return None
67+
68+ category_id = category_id or str (uuid .uuid4 ())
69+ now_iso = datetime .now ().isoformat ()
70+
71+ metadata = {
72+ "category_id" : category_id ,
73+ "name" : category_name ,
74+ "description" : description ,
75+ "created_at" : now_iso ,
76+ "updated_at" : now_iso ,
77+ "patterns" : [] # List of pattern_ids assigned to this category
78+ }
79+
80+ try :
81+ success = self .chroma_connector .add_document (
82+ collection_name = self .CLASSIFICATION_COLLECTION ,
83+ doc_id = category_id ,
84+ document = f"Category: { category_name } " , # Document content for potential future semantic search on categories
85+ embedding = [0.0 ], # Placeholder, categories are retrieved by ID/metadata, not semantic search on content
86+ metadata = metadata
87+ )
88+ if success :
89+ self ._logger .info (f"Added category '{ category_name } ' with ID: { category_id } ." )
90+ return category_id
91+ else :
92+ self ._logger .error (f"Failed to add category '{ category_name } '." )
93+ return None
94+ except Exception as e :
95+ self ._logger .error (f"Error adding category '{ category_name } ': { e } " )
96+ return None
97+
98+ def get_category (self , category_id : str ) -> Optional [Dict [str , Any ]]:
99+ """
100+ Retrieve a specific pattern category by its ID.
101+
102+ Args:
103+ category_id: The ID of the category to retrieve.
104+
105+ Returns:
106+ A dictionary containing the category details (metadata), or None if not found.
107+ """
108+ if not self .chroma_connector .is_available ():
109+ self ._logger .warning ("ChromaDB not available, cannot retrieve category." )
110+ return None
111+ try :
112+ category_record = self .chroma_connector .get_document (
113+ collection_name = self .CLASSIFICATION_COLLECTION ,
114+ doc_id = category_id
115+ )
116+ if category_record :
117+ return category_record ["metadata" ]
118+ else :
119+ self ._logger .info (f"Category with ID '{ category_id } ' not found." )
120+ return None
121+ except Exception as e :
122+ self ._logger .error (f"Error retrieving category '{ category_id } ': { e } " )
123+ return None
124+
125+ def update_category (
126+ self , category_id : str , new_name : Optional [str ] = None , new_description : Optional [str ] = None
127+ ) -> bool :
128+ """
129+ Update an existing pattern category.
130+
131+ Args:
132+ category_id: The ID of the category to update.
133+ new_name: Optional new name for the category.
134+ new_description: Optional new description for the category.
135+
136+ Returns:
137+ True if the category was updated successfully, False otherwise.
138+ """
139+ if not self .chroma_connector .is_available ():
140+ self ._logger .warning ("ChromaDB not available, cannot update category." )
141+ return False
142+
143+ category_record = self .chroma_connector .get_document (
144+ collection_name = self .CLASSIFICATION_COLLECTION ,
145+ doc_id = category_id
146+ )
147+ if not category_record :
148+ self ._logger .error (f"Category with ID '{ category_id } ' not found for update." )
149+ return False
150+
151+ metadata = category_record ["metadata" ]
152+
153+ # Update name and description if provided and different
154+ if new_name is not None and metadata .get ("name" ) != new_name :
155+ metadata ["name" ] = new_name
156+ if new_description is not None and metadata .get ("description" ) != new_description :
157+ metadata ["description" ] = new_description
158+
159+ # Always update timestamp when update_category is called
160+ metadata ["updated_at" ] = datetime .now ().isoformat ()
161+
162+ try :
163+ success = self .chroma_connector .update_document (
164+ collection_name = self .CLASSIFICATION_COLLECTION ,
165+ doc_id = category_id ,
166+ metadata = metadata
167+ )
168+ if success :
169+ self ._logger .info (f"Updated category '{ category_id } '." )
170+ else :
171+ self ._logger .error (f"Failed to update category '{ category_id } '." )
172+ return success
173+ except Exception as e :
174+ self ._logger .error (f"Error updating category '{ category_id } ': { e } " )
175+ return False
176+
177+ def delete_category (self , category_id : str ) -> bool :
178+ """
179+ Delete a pattern category by its ID.
180+
181+ Args:
182+ category_id: The ID of the category to delete.
183+
184+ Returns:
185+ True if the category was deleted successfully, False otherwise.
186+ """
187+ if not self .chroma_connector .is_available ():
188+ self ._logger .warning ("ChromaDB not available, cannot delete category." )
189+ return False
190+ try :
191+ success = self .chroma_connector .delete_document (
192+ collection_name = self .CLASSIFICATION_COLLECTION ,
193+ doc_id = category_id
194+ )
195+ if success :
196+ self ._logger .info (f"Deleted category '{ category_id } '." )
197+ else :
198+ self ._logger .warning (f"Category with ID '{ category_id } ' not found or failed to delete." )
199+ return success
200+ except Exception as e :
201+ self ._logger .error (f"Error deleting category '{ category_id } ': { e } " )
202+ return False
203+
204+ def assign_pattern_to_category (self , pattern_id : str , category_id : str ) -> bool :
205+ """
206+ Assign a pattern to a specific category. Idempotent.
207+
208+ Args:
209+ pattern_id: The ID of the pattern to assign.
210+ category_id: The ID of the category to assign the pattern to.
211+
212+ Returns:
213+ True if the assignment was successful or already existed, False otherwise.
214+ """
215+ if not self .chroma_connector .is_available ():
216+ self ._logger .warning ("ChromaDB not available, cannot assign pattern to category." )
217+ return False
218+
219+ category_record = self .chroma_connector .get_document (
220+ collection_name = self .CLASSIFICATION_COLLECTION ,
221+ doc_id = category_id
222+ )
223+ if not category_record :
224+ self ._logger .error (f"Category with ID '{ category_id } ' not found for pattern assignment." )
225+ return False
226+
227+ metadata = category_record ["metadata" ]
228+ patterns_in_category = set (metadata .get ("patterns" , []))
229+
230+ if pattern_id in patterns_in_category :
231+ self ._logger .info (f"Pattern '{ pattern_id } ' is already assigned to category '{ category_id } '. Idempotent operation." )
232+ return True # Already assigned, so consider it a success
233+
234+ patterns_in_category .add (pattern_id )
235+ metadata ["patterns" ] = list (patterns_in_category )
236+ metadata ["updated_at" ] = datetime .now ().isoformat ()
237+
238+ try :
239+ success = self .chroma_connector .update_document (
240+ collection_name = self .CLASSIFICATION_COLLECTION ,
241+ doc_id = category_id ,
242+ metadata = metadata
243+ )
244+ if success :
245+ self ._logger .info (f"Assigned pattern '{ pattern_id } ' to category '{ category_id } '." )
246+ else :
247+ self ._logger .error (f"Failed to assign pattern '{ pattern_id } ' to category '{ category_id } '." )
248+ return success
249+ except Exception as e :
250+ self ._logger .error (f"Error assigning pattern '{ pattern_id } ' to category '{ category_id } ': { e } " )
251+ return False
252+
253+ def remove_pattern_from_category (self , pattern_id : str , category_id : str ) -> bool :
254+ """
255+ Remove a pattern from a specific category. Idempotent.
256+
257+ Args:
258+ pattern_id: The ID of the pattern to remove.
259+ category_id: The ID of the category to remove the pattern from.
260+
261+ Returns:
262+ True if the removal was successful or pattern was not present, False otherwise.
263+ """
264+ if not self .chroma_connector .is_available ():
265+ self ._logger .warning ("ChromaDB not available, cannot remove pattern from category." )
266+ return False
267+
268+ category_record = self .chroma_connector .get_document (
269+ collection_name = self .CLASSIFICATION_COLLECTION ,
270+ doc_id = category_id
271+ )
272+ if not category_record :
273+ self ._logger .error (f"Category with ID '{ category_id } ' not found for pattern removal." )
274+ return False
275+
276+ metadata = category_record ["metadata" ]
277+ patterns_in_category = set (metadata .get ("patterns" , []))
278+
279+ if pattern_id not in patterns_in_category :
280+ self ._logger .info (f"Pattern '{ pattern_id } ' is not in category '{ category_id } '. Idempotent operation." )
281+ return True # Not present, so consider it a success
282+
283+ patterns_in_category .discard (pattern_id ) # Use discard to avoid KeyError if somehow not present
284+ metadata ["patterns" ] = list (patterns_in_category )
285+ metadata ["updated_at" ] = datetime .now ().isoformat ()
286+
287+ try :
288+ success = self .chroma_connector .update_document (
289+ collection_name = self .CLASSIFICATION_COLLECTION ,
290+ doc_id = category_id ,
291+ metadata = metadata
292+ )
293+ if success :
294+ self ._logger .info (f"Removed pattern '{ pattern_id } ' from category '{ category_id } '." )
295+ else :
296+ self ._logger .error (f"Failed to remove pattern '{ pattern_id } ' from category '{ category_id } '." )
297+ return success
298+ except Exception as e :
299+ self ._logger .error (f"Error removing pattern '{ pattern_id } ' from category '{ category_id } ': { e } " )
300+ return False
301+
302+ def get_patterns_in_category (self , category_id : str ) -> List [str ]:
303+ """
304+ Get all pattern IDs assigned to a specific category.
305+
306+ Args:
307+ category_id: The ID of the category.
308+
309+ Returns:
310+ A list of pattern IDs. Returns an empty list if the category is not found
311+ or has no patterns assigned.
312+ """
313+ if not self .chroma_connector .is_available ():
314+ self ._logger .warning ("ChromaDB not available, cannot get patterns in category." )
315+ return []
316+ try :
317+ category_record = self .chroma_connector .get_document (
318+ collection_name = self .CLASSIFICATION_COLLECTION ,
319+ doc_id = category_id
320+ )
321+ if category_record :
322+ return category_record ["metadata" ].get ("patterns" , [])
323+ else :
324+ self ._logger .info (f"Category with ID '{ category_id } ' not found." )
325+ return []
326+ except Exception as e :
327+ self ._logger .error (f"Error getting patterns for category '{ category_id } ': { e } " )
328+ return []
329+
330+ def get_categories_for_pattern (self , pattern_id : str ) -> List [Dict [str , Any ]]:
331+ """
332+ Get all categories that a specific pattern is assigned to.
333+
334+ Args:
335+ pattern_id: The ID of the pattern.
336+
337+ Returns:
338+ A list of dictionaries, where each dictionary represents a category
339+ (its metadata). Returns an empty list if the pattern is not assigned
340+ to any categories or if ChromaDB is unavailable.
341+ """
342+ if not self .chroma_connector .is_available ():
343+ self ._logger .warning ("ChromaDB not available, cannot get categories for pattern." )
344+ return []
345+
346+ matching_categories = []
347+ try :
348+ all_categories = self .chroma_connector .get_all_documents (self .CLASSIFICATION_COLLECTION )
349+ for category_record in all_categories :
350+ metadata = category_record .get ("metadata" , {})
351+ patterns_in_category = metadata .get ("patterns" , [])
352+ if pattern_id in patterns_in_category :
353+ matching_categories .append (metadata )
354+ return matching_categories
355+ except Exception as e :
356+ self ._logger .error (f"Error getting categories for pattern '{ pattern_id } ': { e } " )
357+ return []
0 commit comments