@@ -293,6 +293,279 @@ def get_all_facts(self) -> List[str]:
293293 return all_facts
294294
295295
296+ class CSVMappingDialog :
297+ """Dialog for editing LLM-generated CSV to ACE mapping"""
298+
299+ def __init__ (self , parent , headers , sample_row , ai_translator ):
300+ self .parent = parent
301+ self .headers = headers
302+ self .sample_row = sample_row
303+ self .ai_translator = ai_translator
304+ self .result = None
305+
306+ self .dialog = tk .Toplevel (parent )
307+ self .dialog .title ("CSV to ACE Mapping" )
308+ self .dialog .geometry ("900x500" )
309+ self .dialog .transient (parent )
310+ self .dialog .grab_set ()
311+
312+ self .setup_ui ()
313+ self .generate_initial_mapping ()
314+
315+ def setup_ui (self ):
316+ """Setup the mapping dialog UI"""
317+ main_frame = ttk .Frame (self .dialog )
318+ main_frame .pack (fill = 'both' , expand = True , padx = 20 , pady = 20 )
319+
320+ # Title
321+ title_label = ttk .Label (main_frame , text = "CSV to ACE Logic Mapping" ,
322+ font = ('Arial' , 14 , 'bold' ))
323+ title_label .pack (pady = (0 , 15 ))
324+
325+ # Sample data display
326+ sample_frame = ttk .LabelFrame (main_frame , text = "Sample CSV Row" , padding = 10 )
327+ sample_frame .pack (fill = 'x' , pady = (0 , 15 ))
328+
329+ sample_display = tk .Text (sample_frame , height = 2 , font = ('Consolas' , 9 ),
330+ state = 'disabled' , bg = '#f8f9fa' )
331+ sample_display .pack (fill = 'x' )
332+
333+ # Display sample data
334+ sample_display .config (state = 'normal' )
335+ sample_content = " | " .join ([f"{ h } : { v } " for h , v in zip (self .headers , self .sample_row )])
336+ sample_display .insert ('1.0' , sample_content )
337+ sample_display .config (state = 'disabled' )
338+
339+ # Main mapping section - two columns
340+ mapping_frame = ttk .LabelFrame (main_frame , text = "Row Mapping" , padding = 15 )
341+ mapping_frame .pack (fill = 'both' , expand = True , pady = (0 , 15 ))
342+
343+ # Create two-column layout
344+ columns_container = ttk .Frame (mapping_frame )
345+ columns_container .pack (fill = 'both' , expand = True )
346+
347+ # Left column - Column names
348+ left_column = ttk .Frame (columns_container )
349+ left_column .pack (side = 'left' , fill = 'both' , expand = True , padx = (0 , 10 ))
350+
351+ ttk .Label (left_column , text = "CSV Columns:" , font = ('Arial' , 11 , 'bold' )).pack (anchor = 'w' , pady = (0 , 5 ))
352+
353+ self .columns_display = tk .Text (left_column , font = ('Consolas' , 10 ),
354+ state = 'disabled' , bg = '#f8f9fa' , width = 35 )
355+ self .columns_display .pack (fill = 'both' , expand = True )
356+
357+ # Right column - ACE template
358+ right_column = ttk .Frame (columns_container )
359+ right_column .pack (side = 'right' , fill = 'both' , expand = True , padx = (10 , 0 ))
360+
361+ ttk .Label (right_column , text = "ACE Statements (use <column_name> tags):" ,
362+ font = ('Arial' , 11 , 'bold' )).pack (anchor = 'w' , pady = (0 , 5 ))
363+
364+ self .ace_template = scrolledtext .ScrolledText (right_column , font = ('Consolas' , 10 ),
365+ wrap = 'word' , width = 50 )
366+ self .ace_template .pack (fill = 'both' , expand = True )
367+
368+ # Fill columns display
369+ self .populate_columns_display ()
370+
371+ # Preview section
372+ preview_frame = ttk .LabelFrame (main_frame , text = "Preview (Applied to Sample Row)" , padding = 10 )
373+ preview_frame .pack (fill = 'x' , pady = (0 , 15 ))
374+
375+ self .preview_text = scrolledtext .ScrolledText (preview_frame , height = 4 ,
376+ font = ('Consolas' , 10 ), state = 'disabled' ,
377+ bg = '#f0f8ff' )
378+ self .preview_text .pack (fill = 'x' )
379+
380+ # Buttons
381+ button_frame = ttk .Frame (main_frame )
382+ button_frame .pack (fill = 'x' )
383+
384+ ttk .Button (button_frame , text = "Generate AI Mapping" ,
385+ command = self .generate_initial_mapping ).pack (side = 'left' , padx = (0 , 10 ))
386+ ttk .Button (button_frame , text = "Update Preview" ,
387+ command = self .update_preview ).pack (side = 'left' , padx = (0 , 10 ))
388+
389+ ttk .Button (button_frame , text = "Cancel" ,
390+ command = self .cancel ).pack (side = 'right' , padx = (10 , 0 ))
391+ ttk .Button (button_frame , text = "Apply Mapping" ,
392+ command = self .apply_mapping ).pack (side = 'right' , padx = (10 , 0 ))
393+
394+ # Bind template changes to preview update
395+ self .ace_template .bind ('<KeyRelease>' , lambda e : self .root .after (1000 , self .update_preview ))
396+
397+ def populate_columns_display (self ):
398+ """Fill the columns display with CSV column names and sample values"""
399+ self .columns_display .config (state = 'normal' )
400+ self .columns_display .delete ('1.0' , 'end' )
401+
402+ content = []
403+ for i , (header , value ) in enumerate (zip (self .headers , self .sample_row ), 1 ):
404+ content .append (f"{ i :2d} . { header } " )
405+ content .append (f" Sample: { value } " )
406+ content .append ("" )
407+
408+ self .columns_display .insert ('1.0' , '\n ' .join (content ))
409+ self .columns_display .config (state = 'disabled' )
410+
411+ def generate_initial_mapping (self ):
412+ """Generate initial mapping using AI"""
413+ if not self .ai_translator .available :
414+ self .generate_fallback_mapping ()
415+ return
416+
417+ headers_text = ", " .join (self .headers )
418+ sample_text = " | " .join ([f"{ h } ={ v } " for h , v in zip (self .headers , self .sample_row )])
419+
420+ prompt = f"""Convert this CSV structure to ACE (Attempto Controlled English) statements.
421+
422+ CSV Columns: { headers_text }
423+ Sample Row: { sample_text }
424+
425+ Generate ACE statements using <column_name> tags for substitution.
426+
427+ Example:
428+ For columns: name, age, city
429+ ACE output: <name> is a person. <name> has age <age>. <name> lives in <city>.
430+
431+ Generate concise ACE statements for these columns:"""
432+
433+ try :
434+ response = self .generate_mapping_with_ai (prompt )
435+ if response :
436+ self .parse_ai_response (response )
437+ else :
438+ self .generate_fallback_mapping ()
439+ except :
440+ self .generate_fallback_mapping ()
441+
442+ def generate_mapping_with_ai (self , prompt ):
443+ """Generate mapping using AI translator"""
444+ try :
445+ response = requests .post (
446+ f"{ self .ai_translator .ollama_url } /api/generate" ,
447+ json = {
448+ "model" : self .ai_translator .model ,
449+ "prompt" : prompt ,
450+ "stream" : False ,
451+ "options" : {"temperature" : 0.2 , "max_tokens" : 150 }
452+ },
453+ timeout = 15
454+ )
455+
456+ if response .status_code == 200 :
457+ return response .json ()['response' ].strip ()
458+ except :
459+ pass
460+ return None
461+
462+ def parse_ai_response (self , response ):
463+ """Parse AI response and extract ACE statements"""
464+ lines = response .split ('\n ' )
465+ ace_statements = []
466+
467+ for line in lines :
468+ line = line .strip ()
469+ # Look for lines that contain angle bracket tags and look like ACE statements
470+ if '<' in line and '>' in line and (line .endswith ('.' ) or line .endswith ('?' )):
471+ ace_statements .append (line )
472+
473+ # If no proper statements found, try to extract any line with angle brackets
474+ if not ace_statements :
475+ for line in lines :
476+ line = line .strip ()
477+ if '<' in line and '>' in line and line :
478+ # Ensure it ends with proper punctuation
479+ if not line .endswith (('.' , '?' , '!' )):
480+ line += '.'
481+ ace_statements .append (line )
482+
483+ # Set the template
484+ if ace_statements :
485+ self .ace_template .delete ('1.0' , 'end' )
486+ self .ace_template .insert ('1.0' , '\n ' .join (ace_statements ))
487+ else :
488+ self .generate_fallback_mapping ()
489+
490+ self .update_preview ()
491+
492+ def generate_fallback_mapping (self ):
493+ """Generate simple fallback mapping when AI fails"""
494+ statements = []
495+
496+ if len (self .headers ) >= 1 :
497+ first_col = self .headers [0 ]
498+
499+ # For remaining columns, create has/property statements
500+ for header in self .headers [1 :]:
501+ clean_prop = header .lower ().replace ('_' , ' ' ).replace ('-' , ' ' )
502+ statements .append (f"<{ first_col } > has { clean_prop } <{ header } >." )
503+
504+ self .ace_template .delete ('1.0' , 'end' )
505+ self .ace_template .insert ('1.0' , '\n ' .join (statements ))
506+ self .update_preview ()
507+
508+ def update_preview (self ):
509+ """Update preview with sample data applied to template"""
510+ template_text = self .ace_template .get ('1.0' , 'end-1c' ).strip ()
511+
512+ if not template_text :
513+ self .preview_text .config (state = 'normal' )
514+ self .preview_text .delete ('1.0' , 'end' )
515+ self .preview_text .insert ('1.0' , "No template defined" )
516+ self .preview_text .config (state = 'disabled' )
517+ return
518+
519+ # Create substitution dictionary
520+ substitutions = {}
521+ for header , value in zip (self .headers , self .sample_row ):
522+ substitutions [header ] = value
523+
524+ preview_lines = []
525+
526+ try :
527+ # Apply substitutions to each line
528+ for line in template_text .split ('\n ' ):
529+ line = line .strip ()
530+ if line :
531+ # Replace <column_name> tags with actual values
532+ result_line = line
533+ for header , value in substitutions .items ():
534+ tag = f"<{ header } >"
535+ if tag in result_line :
536+ result_line = result_line .replace (tag , value )
537+
538+ preview_lines .append (result_line )
539+
540+ # Update preview display
541+ self .preview_text .config (state = 'normal' )
542+ self .preview_text .delete ('1.0' , 'end' )
543+ self .preview_text .insert ('1.0' , '\n ' .join (preview_lines ))
544+ self .preview_text .config (state = 'disabled' )
545+
546+ except Exception as e :
547+ self .preview_text .config (state = 'normal' )
548+ self .preview_text .delete ('1.0' , 'end' )
549+ self .preview_text .insert ('1.0' , f"Preview error: { str (e )} " )
550+ self .preview_text .config (state = 'disabled' )
551+
552+ def apply_mapping (self ):
553+ """Apply the mapping and close dialog"""
554+ template_text = self .ace_template .get ('1.0' , 'end-1c' ).strip ()
555+
556+ if not template_text :
557+ messagebox .showwarning ("Warning" , "Please define ACE template statements" )
558+ return
559+
560+ self .result = template_text
561+ self .dialog .destroy ()
562+
563+ def cancel (self ):
564+ """Cancel the dialog"""
565+ self .result = None
566+ self .dialog .destroy ()
567+
568+
296569class CSVProcessor :
297570 """Process CSV files and convert to ACE facts"""
298571
@@ -308,10 +581,37 @@ def load_csv(file_path: str) -> Tuple[List[str], List[Dict[str, str]]]:
308581 except Exception as e :
309582 raise Exception (f"Error loading CSV: { str (e )} " )
310583
584+ @staticmethod
585+ def convert_to_ace_facts_with_template (headers : List [str ], data : List [Dict [str , str ]],
586+ ace_template : str ) -> List [str ]:
587+ """Convert CSV data to ACE facts using template with <column_name> tags"""
588+ facts = []
589+
590+ for i , row in enumerate (data ):
591+ # Process each line of the template
592+ for line in ace_template .split ('\n ' ):
593+ line = line .strip ()
594+ if line :
595+ # Replace <column_name> tags with actual values
596+ result_line = line
597+ for header in headers :
598+ tag = f"<{ header } >"
599+ if tag in result_line :
600+ value = row .get (header , '' ).strip ()
601+ result_line = result_line .replace (tag , value )
602+
603+ # Only add if all tags were replaced (no < > remaining)
604+ if '<' not in result_line or '>' not in result_line :
605+ if not result_line .endswith ('.' ) and not result_line .endswith ('?' ):
606+ result_line += '.'
607+ facts .append (result_line )
608+
609+ return facts
610+
311611 @staticmethod
312612 def convert_to_ace_facts (headers : List [str ], data : List [Dict [str , str ]],
313613 entity_prefix : str = "Entity" ) -> List [str ]:
314- """Convert CSV data to ACE facts"""
614+ """Convert CSV data to ACE facts (legacy method) """
315615 facts = []
316616
317617 for i , row in enumerate (data ):
@@ -1218,8 +1518,9 @@ def clear_text(self):
12181518 self .text_input .delete (1.0 , tk .END )
12191519 self .status_var .set ("Text cleared" )
12201520
1521+ # Updated load_csv method in EnhancedACECalculator class
12211522 def load_csv (self ):
1222- """Load CSV file and convert to ACE facts"""
1523+ """Load CSV file and convert to ACE facts with LLM-powered mapping """
12231524 file_path = filedialog .askopenfilename (
12241525 title = "Select CSV file" ,
12251526 filetypes = [("CSV files" , "*.csv" ), ("All files" , "*.*" )]
@@ -1228,19 +1529,38 @@ def load_csv(self):
12281529 if file_path :
12291530 try :
12301531 headers , data = CSVProcessor .load_csv (file_path )
1231- facts = CSVProcessor .convert_to_ace_facts (headers , data )
12321532
1233- # Add facts to text area
1234- current_text = self .text_input .get (1.0 , tk .END )
1235- if current_text .strip ():
1236- self .text_input .insert (tk .END , "\n \n # CSV Facts\n " )
1237- else :
1238- self .text_input .insert (tk .END , "# CSV Facts\n " )
1533+ if not data :
1534+ messagebox .showwarning ("Warning" , "CSV file is empty" )
1535+ return
1536+
1537+ # Show mapping dialog with sample row
1538+ sample_row = [data [0 ].get (header , '' ) for header in headers ]
1539+
1540+ # Create and show mapping dialog
1541+ mapping_dialog = CSVMappingDialog (self .root , headers , sample_row , self .ai_translator )
1542+ self .root .wait_window (mapping_dialog .dialog )
12391543
1240- for fact in facts :
1241- self .text_input .insert (tk .END , fact + "\n " )
1544+ # Check if user applied mapping
1545+ if mapping_dialog .result :
1546+ # Convert using template
1547+ facts = CSVProcessor .convert_to_ace_facts_with_template (
1548+ headers , data , mapping_dialog .result
1549+ )
12421550
1243- self .status_var .set (f"Loaded { len (facts )} facts from CSV" )
1551+ # Add facts to text area
1552+ current_text = self .text_input .get (1.0 , tk .END )
1553+ if current_text .strip ():
1554+ self .text_input .insert (tk .END , "\n \n # CSV Facts (Template Mapping)\n " )
1555+ else :
1556+ self .text_input .insert (tk .END , "# CSV Facts (Template Mapping)\n " )
1557+
1558+ for fact in facts :
1559+ self .text_input .insert (tk .END , fact + "\n " )
1560+
1561+ self .status_var .set (f"Loaded { len (facts )} facts from CSV with template mapping" )
1562+ else :
1563+ self .status_var .set ("CSV import cancelled" )
12441564
12451565 except Exception as e :
12461566 messagebox .showerror ("Error" , f"Error loading CSV: { str (e )} " )
0 commit comments