vbagent 0.1.1__py3-none-any.whl → 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
vbagent/cli/check.py CHANGED
@@ -34,6 +34,7 @@ from vbagent.cli.common import (
34
34
  natural_sort_key,
35
35
  extract_problem_solution,
36
36
  find_image_for_problem,
37
+ has_diagram_placeholder,
37
38
  _get_console,
38
39
  _get_panel,
39
40
  _get_table,
@@ -501,7 +502,7 @@ def check():
501
502
  solution - Check solution correctness
502
503
  grammar - Check grammar and spelling
503
504
  clarity - Check clarity and conciseness
504
- tikz - Check TikZ diagram code
505
+ tikz - Check/generate TikZ diagrams
505
506
  apply - Apply a stored suggestion
506
507
  history - View suggestion history
507
508
  resume - Resume interrupted session
@@ -518,7 +519,8 @@ def check():
518
519
  vbagent check solution -d ./src_tex/
519
520
  vbagent check grammar -d ./src_tex/
520
521
  vbagent check clarity -d ./src_tex/
521
- vbagent check tikz -d ./src_tex/
522
+ vbagent check tikz -d ./scans/ # Check/generate TikZ
523
+ vbagent check tikz --patch --ref-type circuit
522
524
  """
523
525
  pass
524
526
 
@@ -2550,6 +2552,22 @@ def check_clarity_cmd(
2550
2552
  is_flag=True,
2551
2553
  help="Reset progress and re-check all files"
2552
2554
  )
2555
+ @click.option(
2556
+ "--patch",
2557
+ is_flag=True,
2558
+ help="Use apply_patch mode for structured diffs (experimental)"
2559
+ )
2560
+ @click.option(
2561
+ "--use-context/--no-context",
2562
+ default=True,
2563
+ help="Include TikZ reference examples in prompt (default: enabled)"
2564
+ )
2565
+ @click.option(
2566
+ "--ref-type",
2567
+ type=str,
2568
+ default=None,
2569
+ help="Filter reference examples by diagram type (e.g., circuit, free_body, graph)"
2570
+ )
2553
2571
  def check_tikz_cmd(
2554
2572
  output_dir: str,
2555
2573
  count: int,
@@ -2558,39 +2576,636 @@ def check_tikz_cmd(
2558
2576
  prompt: Optional[str],
2559
2577
  only_tikz: bool,
2560
2578
  reset: bool,
2579
+ patch: bool,
2580
+ use_context: bool,
2581
+ ref_type: Optional[str],
2561
2582
  ):
2562
- """Check TikZ diagram code for errors and best practices.
2583
+ """Check and generate TikZ diagram code.
2563
2584
 
2564
- Reviews TikZ/PGF code for syntax errors, missing libraries,
2565
- and physics diagram conventions. Prompts for approval to apply fixes.
2585
+ This command has two modes:
2566
2586
 
2567
- Images are matched by filename (e.g., problem_1.tex -> problem_1.png).
2568
- Use --prompt to add specific instructions for the checker.
2587
+ 1. CHECK MODE: Reviews existing TikZ/PGF code for syntax errors,
2588
+ missing libraries, and physics diagram conventions.
2589
+
2590
+ 2. GENERATE MODE: If a file has \\input{diagram} placeholder but no
2591
+ TikZ code, automatically generates TikZ from the corresponding
2592
+ image (auto-discovered in images/ directory).
2593
+
2594
+ Images are auto-discovered by filename (e.g., Problem_1.tex -> images/Problem_1.png)
2595
+ or can be specified with --images-dir.
2596
+
2597
+ Reference examples are matched by diagram type from classification metadata,
2598
+ or can be filtered manually with --ref-type.
2599
+
2600
+ Use --patch to enable apply_patch mode for more precise edits.
2569
2601
 
2570
2602
  \b
2571
2603
  Examples:
2572
- vbagent check tikz
2573
- vbagent check tikz -d ./src/src_tex/
2574
- vbagent check tikz -d ./src/src_tex/ -i ./src/src_images/
2575
- vbagent check tikz -c 10
2576
- vbagent check tikz -p Problem_1
2577
- vbagent check tikz --prompt "Use circuit library for electrical diagrams"
2578
- vbagent check tikz --only-tikz
2579
- vbagent check tikz --reset
2604
+ vbagent check tikz # Check/generate in agentic/
2605
+ vbagent check tikz -d ./scans/ # Check specific directory
2606
+ vbagent check tikz -d ./scans/Problem_1.tex # Check single file
2607
+ vbagent check tikz -c 10 # Process 10 files
2608
+ vbagent check tikz -p Problem_1 # Check specific problem
2609
+ vbagent check tikz -i ./images/ # Explicit images directory
2610
+ vbagent check tikz --only-tikz # Skip files without TikZ
2611
+ vbagent check tikz --reset # Re-check all files
2612
+ vbagent check tikz --patch # Use apply_patch mode
2613
+ vbagent check tikz --ref-type circuit # Use only circuit references
2614
+ vbagent check tikz --prompt "Use circuitikz" # Add instructions
2580
2615
  """
2581
- _run_checker_session(
2582
- output_dir=output_dir,
2583
- count=count,
2584
- problem_id=problem_id,
2585
- checker_name="tikz",
2586
- check_func_module="vbagent.agents.tikz_checker",
2587
- check_func_name="check_tikz",
2588
- require_solution=False,
2589
- require_tikz=only_tikz,
2590
- reset=reset,
2591
- images_dir=images_dir,
2592
- extra_prompt=prompt,
2616
+ if patch:
2617
+ # Use new patch-based checker
2618
+ _run_tikz_patch_session(
2619
+ output_dir=output_dir,
2620
+ count=count,
2621
+ problem_id=problem_id,
2622
+ images_dir=images_dir,
2623
+ extra_prompt=prompt,
2624
+ only_tikz=only_tikz,
2625
+ reset=reset,
2626
+ use_context=use_context,
2627
+ ref_diagram_type=ref_type,
2628
+ )
2629
+ else:
2630
+ # Use legacy checker
2631
+ _run_checker_session(
2632
+ output_dir=output_dir,
2633
+ count=count,
2634
+ problem_id=problem_id,
2635
+ checker_name="tikz",
2636
+ check_func_module="vbagent.agents.tikz_checker",
2637
+ check_func_name="check_tikz",
2638
+ require_solution=False,
2639
+ require_tikz=only_tikz,
2640
+ reset=reset,
2641
+ images_dir=images_dir,
2642
+ extra_prompt=prompt,
2643
+ )
2644
+
2645
+
2646
+ def _load_diagram_type_from_classification(tex_file: Path, output_path: Path) -> Optional[str]:
2647
+ """Load diagram_type from classification JSON for a problem.
2648
+
2649
+ Looks for classification JSON in common locations:
2650
+ - Same directory as tex file: Problem_X.json
2651
+ - classifications/ subdirectory: classifications/Problem_X.json
2652
+ - Parent's classifications/: ../classifications/Problem_X.json
2653
+
2654
+ Args:
2655
+ tex_file: Path to the .tex file
2656
+ output_path: Base output directory
2657
+
2658
+ Returns:
2659
+ diagram_type string if found, None otherwise
2660
+ """
2661
+ import json
2662
+
2663
+ problem_name = tex_file.stem
2664
+
2665
+ # Try common classification file locations
2666
+ possible_paths = [
2667
+ tex_file.with_suffix('.json'), # Same dir, same name
2668
+ tex_file.parent / "classifications" / f"{problem_name}.json",
2669
+ output_path / "classifications" / f"{problem_name}.json",
2670
+ tex_file.parent.parent / "classifications" / f"{problem_name}.json",
2671
+ ]
2672
+
2673
+ for class_path in possible_paths:
2674
+ if class_path.exists():
2675
+ try:
2676
+ data = json.loads(class_path.read_text())
2677
+ diagram_type = data.get("diagram_type")
2678
+ if diagram_type and diagram_type != "none":
2679
+ return diagram_type
2680
+ except (json.JSONDecodeError, KeyError):
2681
+ continue
2682
+
2683
+ return None
2684
+
2685
+
2686
+ def _generate_tikz_for_placeholder(
2687
+ content: str,
2688
+ image_path: Optional[Path],
2689
+ diagram_type: Optional[str] = None,
2690
+ extra_prompt: Optional[str] = None,
2691
+ console = None,
2692
+ ) -> Optional[str]:
2693
+ """Generate TikZ code and replace \\input{diagram} placeholder.
2694
+
2695
+ Uses the TikZ generator agent to create TikZ code from the problem
2696
+ description and optional image, then replaces the placeholder.
2697
+
2698
+ Args:
2699
+ content: Full LaTeX content with \\input{diagram} placeholder
2700
+ image_path: Optional path to reference image
2701
+ diagram_type: Optional diagram type for reference matching
2702
+ extra_prompt: Optional additional instructions
2703
+ console: Rich console for output (optional)
2704
+
2705
+ Returns:
2706
+ Content with placeholder replaced by generated TikZ, or None on failure
2707
+ """
2708
+ import re
2709
+ from vbagent.agents.tikz import generate_tikz, validate_tikz_output
2710
+
2711
+ # Extract problem description for the generator
2712
+ # Try to get the problem statement (before solution)
2713
+ problem_match = re.search(r'\\item\s*(.*?)(?=\\begin\{solution\}|$)', content, re.DOTALL)
2714
+ if problem_match:
2715
+ description = problem_match.group(1).strip()
2716
+ # Clean up LaTeX commands for description
2717
+ description = re.sub(r'\\begin\{center\}.*?\\end\{center\}', '', description, flags=re.DOTALL)
2718
+ description = description.strip()
2719
+ else:
2720
+ description = "Generate a physics diagram based on the problem context."
2721
+
2722
+ # Add extra prompt if provided
2723
+ if extra_prompt:
2724
+ description = f"{description}\n\nAdditional instructions: {extra_prompt}"
2725
+
2726
+ if console:
2727
+ console.print(f"[dim]Generating TikZ... (Ctrl+C to quit)[/dim]")
2728
+
2729
+ # Generate TikZ code
2730
+ tikz_code = generate_tikz(
2731
+ description=description,
2732
+ image_path=str(image_path) if image_path else None,
2733
+ use_context=True,
2593
2734
  )
2735
+
2736
+ if not tikz_code or not validate_tikz_output(tikz_code):
2737
+ return None
2738
+
2739
+ # Wrap in center environment if not already wrapped
2740
+ if not tikz_code.strip().startswith(r'\begin{center}'):
2741
+ tikz_code = f"\\begin{{center}}\n{tikz_code}\n\\end{{center}}"
2742
+
2743
+ # Escape backslashes in tikz_code for use as replacement string
2744
+ # re.sub interprets \1, \2, etc. as backreferences, so we need to escape
2745
+ tikz_code_escaped = tikz_code.replace('\\', '\\\\')
2746
+
2747
+ # Replace the placeholder patterns
2748
+ # Pattern 1: \begin{center}\input{diagram}\end{center}
2749
+ placeholder_pattern = r'\\begin\{center\}\s*%?\s*\\input\{diagram\}\s*\\end\{center\}'
2750
+ result = re.sub(placeholder_pattern, tikz_code_escaped, content, flags=re.DOTALL)
2751
+
2752
+ # Pattern 2: Simple \input{diagram} (with optional comment)
2753
+ if result == content:
2754
+ simple_pattern = r'%?\s*\\input\{diagram\}'
2755
+ result = re.sub(simple_pattern, tikz_code_escaped, result)
2756
+
2757
+ return result if result != content else None
2758
+
2759
+
2760
+ def _prompt_tikz_action(console) -> str:
2761
+ """Prompt user for action on TikZ generation/check result.
2762
+
2763
+ Args:
2764
+ console: Rich console for output
2765
+
2766
+ Returns:
2767
+ Action string: 'approve', 'edit', 'reject', 'skip', or 'quit'
2768
+ """
2769
+ console.print("\n[bold]Actions:[/bold]")
2770
+ console.print(" [green]a[/green]pprove - Apply this change")
2771
+ console.print(" [red]r[/red]eject - Store for later, don't apply")
2772
+ console.print(" [blue]e[/blue]dit - Edit in editor before applying")
2773
+ console.print(" [yellow]s[/yellow]kip - Skip without storing")
2774
+ console.print(" [dim]q[/dim]uit - Exit session")
2775
+
2776
+ Prompt = _get_prompt()
2777
+ try:
2778
+ choice = Prompt.ask(
2779
+ "\nAction",
2780
+ choices=["a", "r", "e", "s", "q", "approve", "reject", "edit", "skip", "quit"],
2781
+ default="a"
2782
+ ).lower()
2783
+ except KeyboardInterrupt:
2784
+ return "quit"
2785
+
2786
+ if choice in ["a", "approve"]:
2787
+ return "approve"
2788
+ elif choice in ["r", "reject"]:
2789
+ return "reject"
2790
+ elif choice in ["e", "edit"]:
2791
+ return "edit"
2792
+ elif choice in ["s", "skip"]:
2793
+ return "skip"
2794
+ else:
2795
+ return "quit"
2796
+
2797
+
2798
+ def _run_tikz_patch_session(
2799
+ output_dir: str,
2800
+ count: int,
2801
+ problem_id: Optional[str],
2802
+ images_dir: Optional[str] = None,
2803
+ extra_prompt: Optional[str] = None,
2804
+ only_tikz: bool = False,
2805
+ reset: bool = False,
2806
+ use_context: bool = True,
2807
+ ref_diagram_type: Optional[str] = None,
2808
+ ) -> None:
2809
+ """Run TikZ checker session using apply_patch mode.
2810
+
2811
+ Uses OpenAI's apply_patch tool for structured diffs instead of
2812
+ returning full corrected content.
2813
+
2814
+ Args:
2815
+ output_dir: Directory containing .tex files
2816
+ count: Number of files to process
2817
+ problem_id: Optional specific problem ID to check
2818
+ images_dir: Optional directory containing images for problems
2819
+ extra_prompt: Optional additional instructions for the checker
2820
+ only_tikz: Whether to only check files with TikZ code
2821
+ reset: Whether to reset progress and re-check all files
2822
+ use_context: Whether to include TikZ reference examples
2823
+ ref_diagram_type: Filter reference examples by diagram type (e.g., circuit)
2824
+ """
2825
+ import re
2826
+ from vbagent.models.version_store import VersionStore, SuggestionStatus
2827
+ from vbagent.models.review import Suggestion, ReviewIssueType as IssueType
2828
+ from vbagent.agents.tikz_checker import (
2829
+ check_tikz_with_patch,
2830
+ has_tikz_environment,
2831
+ PatchResult,
2832
+ )
2833
+
2834
+ console = _get_console()
2835
+
2836
+ console.print("[cyan]Using apply_patch mode (experimental)[/cyan]")
2837
+ if ref_diagram_type:
2838
+ console.print(f"[cyan]Using only '{ref_diagram_type}' references[/cyan]")
2839
+
2840
+ output_path = Path(output_dir)
2841
+ tex_files = _discover_tex_files(output_path)
2842
+
2843
+ if not tex_files:
2844
+ console.print(f"[red]Error:[/red] No .tex files found in {output_dir}")
2845
+ raise SystemExit(1)
2846
+
2847
+ def natural_sort_key(p):
2848
+ return [int(t) if t.isdigit() else t.lower() for t in re.split(r'(\d+)', str(p))]
2849
+
2850
+ tex_files = sorted(tex_files, key=natural_sort_key)
2851
+
2852
+ if problem_id:
2853
+ tex_files = [f for f in tex_files if problem_id in f.stem]
2854
+ if not tex_files:
2855
+ console.print(f"[red]Error:[/red] No files found matching '{problem_id}'")
2856
+ raise SystemExit(1)
2857
+
2858
+ # Filter for TikZ files if required
2859
+ if only_tikz:
2860
+ tikz_files = []
2861
+ for f in tex_files:
2862
+ content = f.read_text()
2863
+ if has_tikz_environment(content):
2864
+ tikz_files.append(f)
2865
+ tex_files = tikz_files
2866
+ if not tex_files:
2867
+ console.print(f"[yellow]No files with TikZ code found in {output_dir}[/yellow]")
2868
+ return
2869
+
2870
+ # Initialize version store for tracking
2871
+ store = VersionStore(base_dir=".")
2872
+ output_dir_normalized = str(output_path.resolve())
2873
+
2874
+ # Reset progress if requested
2875
+ if reset:
2876
+ reset_count = store.reset_checker_progress("tikz_patch", output_dir_normalized)
2877
+ if reset_count > 0:
2878
+ console.print(f"[yellow]Reset progress for {reset_count} file(s)[/yellow]")
2879
+
2880
+ # Filter out already-checked files
2881
+ checked_files = store.get_checked_files("tikz_patch", output_dir_normalized)
2882
+ unchecked_files = [f for f in tex_files if str(f.resolve()) not in checked_files]
2883
+
2884
+ if not unchecked_files:
2885
+ console.print(f"[green]✓ All {len(tex_files)} file(s) have been checked[/green]")
2886
+ stats = store.get_checker_stats("tikz_patch", output_dir_normalized)
2887
+ console.print(f"[dim]Total: {stats['total']}, Passed: {stats['passed']}, Had issues: {stats['failed']}[/dim]")
2888
+ console.print(f"[dim]Use --reset to re-check files[/dim]")
2889
+ store.close()
2890
+ return
2891
+
2892
+ if len(checked_files) > 0:
2893
+ console.print(f"[dim]Skipping {len(checked_files)} already-checked file(s)[/dim]")
2894
+
2895
+ to_process = unchecked_files[:count]
2896
+ console.print(f"[cyan]Checking {len(to_process)} file(s) with apply_patch mode[/cyan]")
2897
+ session_id = store.create_session()
2898
+
2899
+ stats = {
2900
+ "processed": 0,
2901
+ "passed": 0,
2902
+ "approved": 0,
2903
+ "rejected": 0,
2904
+ "skipped": 0,
2905
+ "patch_errors": 0,
2906
+ "session_id": session_id,
2907
+ }
2908
+
2909
+ shutdown_requested = False
2910
+
2911
+ def signal_handler(signum, frame):
2912
+ nonlocal shutdown_requested
2913
+ shutdown_requested = True
2914
+ console.print("\n[yellow]Shutdown requested. Saving progress...[/yellow]")
2915
+
2916
+ original_sigint = signal.signal(signal.SIGINT, signal_handler)
2917
+ original_sigterm = None
2918
+ if sys.platform != "win32":
2919
+ original_sigterm = signal.signal(signal.SIGTERM, signal_handler)
2920
+
2921
+ try:
2922
+ for idx, tex_file in enumerate(to_process):
2923
+ if shutdown_requested:
2924
+ break
2925
+
2926
+ rel_path = tex_file.relative_to(output_path) if output_path.is_dir() else tex_file.name
2927
+ problem_name = tex_file.stem
2928
+ console.print(f"\n[bold cyan]═══ [{idx+1}/{len(to_process)}] {rel_path} ═══[/bold cyan]")
2929
+
2930
+ content = tex_file.read_text()
2931
+
2932
+ # Find corresponding image
2933
+ # 1. Use explicit images_dir if provided
2934
+ # 2. Otherwise, auto-discover if file has \input{diagram} placeholder
2935
+ image_path = None
2936
+ if images_dir:
2937
+ image_path = find_image_for_problem(tex_file, images_dir)
2938
+ if image_path:
2939
+ console.print(f"[dim]Image: {image_path.name}[/dim]")
2940
+ else:
2941
+ # Auto-discover image if file has diagram placeholder
2942
+ if has_diagram_placeholder(content):
2943
+ image_path = find_image_for_problem(tex_file, auto_discover=True)
2944
+ if image_path:
2945
+ console.print(f"[dim]Auto-found image: {image_path.name}[/dim]")
2946
+
2947
+ # Check if this file needs TikZ GENERATION (has placeholder but no TikZ)
2948
+ needs_generation = has_diagram_placeholder(content) and not has_tikz_environment(content)
2949
+
2950
+ if needs_generation:
2951
+ # Generate TikZ instead of checking
2952
+ console.print(f"[cyan]Generating TikZ (found \\input{{diagram}} placeholder)[/cyan]")
2953
+
2954
+ if not image_path:
2955
+ console.print("[yellow]Warning: No image found for generation. Results may be limited.[/yellow]")
2956
+
2957
+ # Auto-detect diagram type from classification
2958
+ auto_diagram_type = ref_diagram_type
2959
+ if not auto_diagram_type and use_context:
2960
+ auto_diagram_type = _load_diagram_type_from_classification(tex_file, output_path)
2961
+ if auto_diagram_type:
2962
+ console.print(f"[dim]Auto-detected diagram type: {auto_diagram_type}[/dim]")
2963
+
2964
+ try:
2965
+ generated_content = _generate_tikz_for_placeholder(
2966
+ content=content,
2967
+ image_path=image_path,
2968
+ diagram_type=auto_diagram_type,
2969
+ extra_prompt=extra_prompt,
2970
+ console=console,
2971
+ )
2972
+ stats["processed"] += 1
2973
+
2974
+ if not generated_content:
2975
+ console.print("[yellow]Failed to generate TikZ[/yellow]")
2976
+ stats["skipped"] += 1
2977
+ continue
2978
+
2979
+ # Show the generated content
2980
+ diff_text = _generate_diff(content, generated_content, str(rel_path))
2981
+
2982
+ if diff_text:
2983
+ console.print(f"\n[bold]Generated TikZ:[/bold]")
2984
+ display_diff(diff_text, console)
2985
+
2986
+ # Create suggestion for tracking
2987
+ suggestion = Suggestion(
2988
+ file_path=str(tex_file),
2989
+ issue_type=IssueType.FORMATTING,
2990
+ description="TikZ generation: replaced \\input{diagram} placeholder",
2991
+ original_content=content,
2992
+ suggested_content=generated_content,
2993
+ diff=diff_text,
2994
+ reasoning="Generated TikZ code from image to replace placeholder.",
2995
+ confidence=0.8,
2996
+ )
2997
+
2998
+ # Prompt for action
2999
+ action = _prompt_tikz_action(console)
3000
+
3001
+ if action == "quit":
3002
+ shutdown_requested = True
3003
+ break
3004
+ elif action == "skip":
3005
+ console.print("[dim]Skipped[/dim]")
3006
+ stats["skipped"] += 1
3007
+ continue
3008
+ elif action == "reject":
3009
+ store.save_suggestion(suggestion, problem_name, SuggestionStatus.REJECTED, session_id)
3010
+ store.mark_file_checked(str(tex_file.resolve()), "tikz_patch", output_dir_normalized, passed=False)
3011
+ console.print("[yellow]Suggestion stored for later[/yellow]")
3012
+ stats["rejected"] += 1
3013
+ continue
3014
+
3015
+ final_content = generated_content
3016
+ if action == "edit":
3017
+ success, edited = open_suggested_in_editor(str(tex_file), generated_content, console)
3018
+ if success and edited:
3019
+ final_content = edited
3020
+ console.print("[cyan]Content edited[/cyan]")
3021
+
3022
+ # Write the generated content
3023
+ try:
3024
+ tex_file.write_text(final_content)
3025
+ console.print(f"[green]✓ TikZ generated and applied to {rel_path}[/green]")
3026
+ store.save_suggestion(suggestion, problem_name, SuggestionStatus.APPROVED, session_id)
3027
+ store.mark_file_checked(str(tex_file.resolve()), "tikz_patch", output_dir_normalized, passed=False)
3028
+ stats["approved"] += 1
3029
+ except (IOError, OSError) as e:
3030
+ console.print(f"[red]✗ Failed to write: {e}[/red]")
3031
+ stats["rejected"] += 1
3032
+
3033
+ continue
3034
+
3035
+ except KeyboardInterrupt:
3036
+ console.print("\n[yellow]Interrupted[/yellow]")
3037
+ shutdown_requested = True
3038
+ break
3039
+ except Exception as e:
3040
+ console.print(f"[red]Error generating TikZ:[/red] {e}")
3041
+ stats["skipped"] += 1
3042
+ continue
3043
+
3044
+ # Normal checking flow (file has existing TikZ or no placeholder)
3045
+ # Auto-detect diagram type from classification if not manually specified
3046
+ auto_diagram_type = ref_diagram_type
3047
+ if not auto_diagram_type and use_context:
3048
+ auto_diagram_type = _load_diagram_type_from_classification(tex_file, output_path)
3049
+ if auto_diagram_type:
3050
+ console.print(f"[dim]Auto-detected diagram type: {auto_diagram_type}[/dim]")
3051
+
3052
+ # Prepare content with extra prompt if provided
3053
+ check_content = content
3054
+ if extra_prompt:
3055
+ console.print(f"[dim]Extra instructions: {extra_prompt}[/dim]")
3056
+ check_content = f"% ADDITIONAL INSTRUCTIONS: {extra_prompt}\n\n{content}"
3057
+
3058
+ try:
3059
+ console.print(f"[dim]Checking with apply_patch... (Ctrl+C to quit)[/dim]")
3060
+ result: PatchResult = check_tikz_with_patch(
3061
+ file_path=str(tex_file),
3062
+ full_content=check_content,
3063
+ image_path=str(image_path) if image_path else None,
3064
+ use_context=use_context,
3065
+ ref_diagram_type=auto_diagram_type,
3066
+ )
3067
+ stats["processed"] += 1
3068
+ except KeyboardInterrupt:
3069
+ console.print("\n[yellow]Interrupted[/yellow]")
3070
+ shutdown_requested = True
3071
+ break
3072
+ except Exception as e:
3073
+ console.print(f"[red]Error checking:[/red] {e}")
3074
+ stats["skipped"] += 1
3075
+ continue
3076
+
3077
+ if result.passed:
3078
+ console.print(f"[green]✓ {result.summary}[/green]")
3079
+ stats["passed"] += 1
3080
+ store.mark_file_checked(str(tex_file.resolve()), "tikz_patch", output_dir_normalized, passed=True)
3081
+ continue
3082
+
3083
+ # Show patch results
3084
+ console.print(f"[yellow]{result.summary}[/yellow]")
3085
+
3086
+ if result.patch_errors:
3087
+ for err in result.patch_errors:
3088
+ console.print(f"[red] Patch error: {err}[/red]")
3089
+ stats["patch_errors"] += len(result.patch_errors)
3090
+
3091
+ if not result.corrected_content:
3092
+ console.print("[yellow]No corrected content available[/yellow]")
3093
+ stats["skipped"] += 1
3094
+ continue
3095
+
3096
+ # Generate diff for display
3097
+ diff_text = _generate_diff(content, result.corrected_content, str(rel_path))
3098
+
3099
+ # Create suggestion object for database storage
3100
+ suggestion = Suggestion(
3101
+ file_path=str(tex_file),
3102
+ issue_type=IssueType.FORMATTING,
3103
+ description=f"TikZ patch check: {result.summary}",
3104
+ original_content=content,
3105
+ suggested_content=result.corrected_content,
3106
+ diff=diff_text,
3107
+ reasoning=f"Applied {result.patches_applied} patch(es) via apply_patch tool.",
3108
+ confidence=0.8,
3109
+ )
3110
+
3111
+ if diff_text:
3112
+ console.print(f"\n[bold]Proposed Changes ({result.patches_applied} patch(es)):[/bold]")
3113
+ display_diff(diff_text, console)
3114
+
3115
+ # Prompt for action
3116
+ console.print("\n[bold]Actions:[/bold]")
3117
+ console.print(" [green]a[/green]pprove - Apply this change")
3118
+ console.print(" [red]r[/red]eject - Store for later, don't apply")
3119
+ console.print(" [blue]e[/blue]dit - Edit in editor before applying")
3120
+ console.print(" [yellow]s[/yellow]kip - Skip without storing")
3121
+ console.print(" [dim]q[/dim]uit - Exit session")
3122
+
3123
+ Prompt = _get_prompt()
3124
+ try:
3125
+ choice = Prompt.ask(
3126
+ "\nAction",
3127
+ choices=["a", "r", "e", "s", "q", "approve", "reject", "edit", "skip", "quit"],
3128
+ default="a"
3129
+ ).lower()
3130
+ except KeyboardInterrupt:
3131
+ console.print("\n[yellow]Interrupted[/yellow]")
3132
+ shutdown_requested = True
3133
+ break
3134
+
3135
+ if choice in ["q", "quit"]:
3136
+ shutdown_requested = True
3137
+ break
3138
+
3139
+ if choice in ["s", "skip"]:
3140
+ console.print("[dim]Skipped[/dim]")
3141
+ stats["skipped"] += 1
3142
+ continue
3143
+
3144
+ if choice in ["r", "reject"]:
3145
+ store.save_suggestion(suggestion, problem_name, SuggestionStatus.REJECTED, session_id)
3146
+ store.mark_file_checked(str(tex_file.resolve()), "tikz_patch", output_dir_normalized, passed=False)
3147
+ console.print("[yellow]Suggestion stored for later[/yellow]")
3148
+ stats["rejected"] += 1
3149
+ continue
3150
+
3151
+ final_content = result.corrected_content
3152
+
3153
+ if choice in ["e", "edit"]:
3154
+ success, edited = open_suggested_in_editor(str(tex_file), result.corrected_content, console)
3155
+ if success and edited:
3156
+ final_content = edited
3157
+ console.print("[cyan]Content edited[/cyan]")
3158
+ else:
3159
+ console.print("[yellow]Edit cancelled, using original correction[/yellow]")
3160
+
3161
+ # Write the corrected content
3162
+ try:
3163
+ tex_file.write_text(final_content)
3164
+ console.print(f"[green]✓ Corrections applied to {rel_path}[/green]")
3165
+ store.save_suggestion(suggestion, problem_name, SuggestionStatus.APPROVED, session_id)
3166
+ store.mark_file_checked(str(tex_file.resolve()), "tikz_patch", output_dir_normalized, passed=False)
3167
+ stats["approved"] += 1
3168
+ except (IOError, OSError) as e:
3169
+ console.print(f"[red]✗ Failed to write: {e}[/red]")
3170
+ store.save_suggestion(suggestion, problem_name, SuggestionStatus.REJECTED, session_id)
3171
+ store.mark_file_checked(str(tex_file.resolve()), "tikz_patch", output_dir_normalized, passed=False)
3172
+ stats["rejected"] += 1
3173
+
3174
+ # Update session with final stats
3175
+ store.update_session(
3176
+ session_id,
3177
+ problems_reviewed=stats["processed"],
3178
+ suggestions_made=stats["approved"] + stats["rejected"],
3179
+ approved_count=stats["approved"],
3180
+ rejected_count=stats["rejected"],
3181
+ skipped_count=stats["skipped"],
3182
+ completed=not shutdown_requested,
3183
+ )
3184
+
3185
+ finally:
3186
+ signal.signal(signal.SIGINT, original_sigint)
3187
+ if original_sigterm is not None:
3188
+ signal.signal(signal.SIGTERM, original_sigterm)
3189
+ store.close()
3190
+
3191
+ # Summary
3192
+ console.print("\n[bold]═══ Session Summary (apply_patch mode) ═══[/bold]")
3193
+ table = _get_table(show_header=False, box=None)
3194
+ table.add_column("Metric", style="dim")
3195
+ table.add_column("Value", justify="right")
3196
+
3197
+ table.add_row("Files checked", str(stats["processed"]))
3198
+ table.add_row("Passed", f"[green]{stats['passed']}[/green]")
3199
+ table.add_row("Approved", f"[green]{stats['approved']}[/green]")
3200
+ table.add_row("Rejected", f"[red]{stats['rejected']}[/red]")
3201
+ table.add_row("Skipped", f"[yellow]{stats['skipped']}[/yellow]")
3202
+ if stats["patch_errors"] > 0:
3203
+ table.add_row("Patch errors", f"[red]{stats['patch_errors']}[/red]")
3204
+
3205
+ console.print(table)
3206
+
3207
+ if shutdown_requested:
3208
+ console.print(f"\n[dim]Session {session_id[:8]} saved. View with: vbagent check history[/dim]")
2594
3209
 
2595
3210
 
2596
3211
  def _run_checker_session(
@@ -2633,9 +3248,9 @@ def _run_checker_session(
2633
3248
  module = importlib.import_module(check_func_module)
2634
3249
  check_func = getattr(module, check_func_name)
2635
3250
 
2636
- # Import has_tikz_environment if needed
3251
+ # Import has_tikz_environment for tikz checker (needed for generation detection)
2637
3252
  has_tikz_environment = None
2638
- if require_tikz:
3253
+ if checker_name == "tikz" or require_tikz:
2639
3254
  from vbagent.agents.tikz_checker import has_tikz_environment
2640
3255
 
2641
3256
  console = _get_console()
@@ -2739,13 +3354,114 @@ def _run_checker_session(
2739
3354
  problem_name = tex_file.stem
2740
3355
  console.print(f"\n[bold cyan]═══ [{idx+1}/{len(to_process)}] {rel_path} ═══[/bold cyan]")
2741
3356
 
2742
- # Find corresponding image if images_dir is provided
2743
- image_path = find_image_for_problem(tex_file, images_dir) if images_dir else None
2744
- if image_path:
2745
- console.print(f"[dim]Image: {image_path.name}[/dim]")
2746
-
2747
3357
  content = tex_file.read_text()
2748
3358
 
3359
+ # Find corresponding image
3360
+ # 1. Use explicit images_dir if provided
3361
+ # 2. For tikz checker, auto-discover if file has \input{diagram} placeholder
3362
+ image_path = None
3363
+ if images_dir:
3364
+ image_path = find_image_for_problem(tex_file, images_dir)
3365
+ if image_path:
3366
+ console.print(f"[dim]Image: {image_path.name}[/dim]")
3367
+ elif checker_name == "tikz":
3368
+ # Auto-discover image if file has diagram placeholder
3369
+ if has_diagram_placeholder(content):
3370
+ image_path = find_image_for_problem(tex_file, auto_discover=True)
3371
+ if image_path:
3372
+ console.print(f"[dim]Auto-found image: {image_path.name}[/dim]")
3373
+
3374
+ # For tikz checker: check if generation is needed (has placeholder but no TikZ)
3375
+ if checker_name == "tikz" and has_tikz_environment:
3376
+ needs_generation = has_diagram_placeholder(content) and not has_tikz_environment(content)
3377
+
3378
+ if needs_generation:
3379
+ # Generate TikZ instead of checking
3380
+ console.print(f"[cyan]Generating TikZ (found \\input{{diagram}} placeholder)[/cyan]")
3381
+
3382
+ if not image_path:
3383
+ console.print("[yellow]Warning: No image found for generation. Results may be limited.[/yellow]")
3384
+
3385
+ try:
3386
+ generated_content = _generate_tikz_for_placeholder(
3387
+ content=content,
3388
+ image_path=image_path,
3389
+ diagram_type=None,
3390
+ extra_prompt=extra_prompt,
3391
+ console=console,
3392
+ )
3393
+ stats["processed"] += 1
3394
+
3395
+ if not generated_content:
3396
+ console.print("[yellow]Failed to generate TikZ[/yellow]")
3397
+ stats["skipped"] += 1
3398
+ continue
3399
+
3400
+ # Show the generated content
3401
+ diff_text = _generate_diff(content, generated_content, str(rel_path))
3402
+
3403
+ if diff_text:
3404
+ console.print(f"\n[bold]Generated TikZ:[/bold]")
3405
+ display_diff(diff_text, console)
3406
+
3407
+ # Create suggestion for tracking
3408
+ suggestion = Suggestion(
3409
+ file_path=str(tex_file),
3410
+ issue_type=issue_type,
3411
+ description="TikZ generation: replaced \\input{diagram} placeholder",
3412
+ original_content=content,
3413
+ suggested_content=generated_content,
3414
+ diff=diff_text,
3415
+ reasoning="Generated TikZ code from image to replace placeholder.",
3416
+ confidence=0.8,
3417
+ )
3418
+
3419
+ # Prompt for action
3420
+ action = _prompt_tikz_action(console)
3421
+
3422
+ if action == "quit":
3423
+ shutdown_requested = True
3424
+ break
3425
+ elif action == "skip":
3426
+ console.print("[dim]Skipped[/dim]")
3427
+ stats["skipped"] += 1
3428
+ continue
3429
+ elif action == "reject":
3430
+ store.save_suggestion(suggestion, problem_name, SuggestionStatus.REJECTED, session_id)
3431
+ store.mark_file_checked(str(tex_file.resolve()), checker_name, output_dir_normalized, passed=False)
3432
+ console.print("[yellow]Suggestion stored for later[/yellow]")
3433
+ stats["rejected"] += 1
3434
+ continue
3435
+
3436
+ final_content = generated_content
3437
+ if action == "edit":
3438
+ success, edited = open_suggested_in_editor(str(tex_file), generated_content, console)
3439
+ if success and edited:
3440
+ final_content = edited
3441
+ console.print("[cyan]Content edited[/cyan]")
3442
+
3443
+ # Write the generated content
3444
+ try:
3445
+ tex_file.write_text(final_content)
3446
+ console.print(f"[green]✓ TikZ generated and applied to {rel_path}[/green]")
3447
+ store.save_suggestion(suggestion, problem_name, SuggestionStatus.APPROVED, session_id)
3448
+ store.mark_file_checked(str(tex_file.resolve()), checker_name, output_dir_normalized, passed=False)
3449
+ stats["approved"] += 1
3450
+ except (IOError, OSError) as e:
3451
+ console.print(f"[red]✗ Failed to write: {e}[/red]")
3452
+ stats["rejected"] += 1
3453
+
3454
+ continue
3455
+
3456
+ except KeyboardInterrupt:
3457
+ console.print("\n[yellow]Interrupted[/yellow]")
3458
+ shutdown_requested = True
3459
+ break
3460
+ except Exception as e:
3461
+ console.print(f"[red]Error generating TikZ:[/red] {e}")
3462
+ stats["skipped"] += 1
3463
+ continue
3464
+
2749
3465
  if require_solution and r'\begin{solution}' not in content:
2750
3466
  console.print("[yellow]No solution environment found, skipping[/yellow]")
2751
3467
  stats["skipped"] += 1