Skip to content

Commit 18aed2a

Browse files
authored
Merge pull request #8 from bluebbberry/develop
feat: implement csv import based on LLM mapping
2 parents 7094cf9 + 17f2802 commit 18aed2a

File tree

1 file changed

+332
-12
lines changed

1 file changed

+332
-12
lines changed

main.py

Lines changed: 332 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
296569
class 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

Comments
 (0)