vbagent 0.2.0__py3-none-any.whl → 0.2.1__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/init.py ADDED
@@ -0,0 +1,248 @@
1
+ """CLI command for initializing workspace config.
2
+
3
+ Shortcut for `vbagent config init`.
4
+ """
5
+
6
+ import click
7
+ import os
8
+
9
+ from vbagent.config import (
10
+ init_workspace,
11
+ SUBJECTS,
12
+ MODELS,
13
+ AGENT_TYPES,
14
+ PROVIDERS,
15
+ VBAgentConfig,
16
+ AgentModelConfig,
17
+ WORKSPACE_CONFIG_FILE,
18
+ )
19
+ from pathlib import Path
20
+
21
+
22
+ def _get_console():
23
+ """Lazy import of rich Console."""
24
+ from rich.console import Console
25
+ return Console()
26
+
27
+
28
+ def _get_prompt():
29
+ """Lazy import of rich Prompt."""
30
+ from rich.prompt import Prompt, IntPrompt
31
+ return Prompt, IntPrompt
32
+
33
+
34
+ CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
35
+
36
+
37
+ def _select_from_list(console, title: str, options: list[str], default: str) -> str:
38
+ """Interactive selection from a list of options.
39
+
40
+ Args:
41
+ console: Rich console instance
42
+ title: Title to display
43
+ options: List of options
44
+ default: Default option
45
+
46
+ Returns:
47
+ Selected option
48
+ """
49
+ Prompt, IntPrompt = _get_prompt()
50
+
51
+ default_idx = options.index(default) + 1 if default in options else 1
52
+
53
+ console.print(f"\n[bold cyan]{title}[/bold cyan] [dim](default: {default})[/dim]")
54
+ for i, opt in enumerate(options, 1):
55
+ marker = "[green]→[/green]" if opt == default else " "
56
+ console.print(f" {marker} {i}. {opt.title()}")
57
+
58
+ choice = Prompt.ask(
59
+ "\n[dim]Enter number or press Enter for default[/dim]",
60
+ default=str(default_idx),
61
+ )
62
+
63
+ try:
64
+ idx = int(choice) - 1
65
+ if 0 <= idx < len(options):
66
+ return options[idx]
67
+ except ValueError:
68
+ pass
69
+
70
+ return default
71
+
72
+
73
+ def _select_model(console, agent_type: str, current: str) -> str:
74
+ """Interactive model selection."""
75
+ Prompt, _ = _get_prompt()
76
+
77
+ model_list = list(MODELS.keys())
78
+ default_idx = model_list.index(current) + 1 if current in model_list else 1
79
+
80
+ console.print(f"\n[bold cyan]Model for {agent_type}[/bold cyan] [dim](default: {current})[/dim]")
81
+ for i, model in enumerate(model_list, 1):
82
+ marker = "[green]→[/green]" if model == current else " "
83
+ console.print(f" {marker} {i}. {model}")
84
+
85
+ choice = Prompt.ask(
86
+ "\n[dim]Enter number or press Enter for default[/dim]",
87
+ default=str(default_idx),
88
+ )
89
+
90
+ try:
91
+ idx = int(choice) - 1
92
+ if 0 <= idx < len(model_list):
93
+ return model_list[idx]
94
+ except ValueError:
95
+ pass
96
+
97
+ return current
98
+
99
+
100
+ def _select_reasoning(console, agent_type: str, current: str) -> str:
101
+ """Interactive reasoning effort selection."""
102
+ options = ["low", "medium", "high", "xhigh"]
103
+ return _select_from_list(console, f"Reasoning effort for {agent_type}", options, current)
104
+
105
+
106
+ @click.command(context_settings=CONTEXT_SETTINGS)
107
+ @click.option("--force", "-f", is_flag=True, help="Overwrite existing workspace config")
108
+ @click.option("--quick", "-q", is_flag=True, help="Quick mode - only ask for subject")
109
+ @click.option("--yes", "-y", is_flag=True, help="Non-interactive mode with defaults")
110
+ def init(force: bool, quick: bool, yes: bool):
111
+ """Initialize workspace config interactively.
112
+
113
+ Creates .vbagent.json in current directory with customized settings.
114
+
115
+ \b
116
+ Examples:
117
+ vbagent init # Interactive setup
118
+ vbagent init --quick # Only ask for subject
119
+ vbagent init --yes # Use all defaults (non-interactive)
120
+ vbagent init --force # Overwrite existing config
121
+
122
+ \b
123
+ The workspace config allows you to:
124
+ - Use different models per workspace
125
+ - Set subject-specific prompts (physics, chemistry, etc.)
126
+ - Override reasoning effort and other settings
127
+
128
+ \b
129
+ Config hierarchy:
130
+ 1. Global config (~/.config/vbagent/models.json)
131
+ 2. Workspace config (.vbagent.json) - overrides global
132
+ """
133
+ console = _get_console()
134
+ Prompt, _ = _get_prompt()
135
+
136
+ # Check if config already exists
137
+ workspace_config = Path.cwd() / WORKSPACE_CONFIG_FILE
138
+ if workspace_config.exists() and not force:
139
+ console.print(f"[yellow]⚠[/yellow] Workspace config already exists: {workspace_config}")
140
+ if not yes:
141
+ overwrite = Prompt.ask(
142
+ "Overwrite?",
143
+ choices=["y", "n"],
144
+ default="n",
145
+ )
146
+ if overwrite.lower() != "y":
147
+ console.print("[dim]Cancelled[/dim]")
148
+ raise SystemExit(0)
149
+
150
+ console.print("\n[bold]🚀 VBAgent Workspace Setup[/bold]\n")
151
+ console.print("[dim]Configure your workspace settings. Press Enter to accept defaults.[/dim]")
152
+
153
+ # Load global config as base
154
+ config = VBAgentConfig.load_global()
155
+
156
+ # === Subject Selection ===
157
+ if yes:
158
+ subject = "physics"
159
+ else:
160
+ subject = _select_from_list(
161
+ console,
162
+ "Select Subject",
163
+ SUBJECTS,
164
+ config.subject,
165
+ )
166
+ config.subject = subject
167
+ console.print(f"[green]✓[/green] Subject: {subject}")
168
+
169
+ if not quick and not yes:
170
+ # === Provider Selection ===
171
+ provider_names = list(PROVIDERS.keys())
172
+ current_provider = "openai"
173
+ if config.base_url:
174
+ for name, info in PROVIDERS.items():
175
+ if info["base_url"] and config.base_url.rstrip("/") == info["base_url"].rstrip("/"):
176
+ current_provider = name
177
+ break
178
+
179
+ selected_provider = _select_from_list(
180
+ console,
181
+ "Select Provider",
182
+ provider_names,
183
+ current_provider,
184
+ )
185
+ config.base_url = PROVIDERS[selected_provider]["base_url"]
186
+ console.print(f"[green]✓[/green] Provider: {selected_provider}")
187
+
188
+ # Ask for API key if non-OpenAI provider
189
+ if selected_provider != "openai":
190
+ env_key = PROVIDERS[selected_provider]["env_key"]
191
+ has_env = os.environ.get(env_key)
192
+
193
+ if has_env:
194
+ console.print(f"[dim] Found {env_key} in environment[/dim]")
195
+ else:
196
+ Prompt, _ = _get_prompt()
197
+ api_key = Prompt.ask(
198
+ f"\n[cyan]API key for {selected_provider}[/cyan] [dim](Enter to skip, set {env_key} env var)[/dim]",
199
+ default="",
200
+ password=True,
201
+ )
202
+ if api_key:
203
+ config.api_key = api_key
204
+ console.print(f"[green]✓[/green] API key set")
205
+ else:
206
+ console.print(f"[yellow] ⚠ No API key set. Set {env_key} in your environment[/yellow]")
207
+
208
+ # === Default Model ===
209
+ console.print("\n[bold]─── Default Settings ───[/bold]")
210
+
211
+ config.default_model = _select_model(console, "default", config.default_model)
212
+ console.print(f"[green]✓[/green] Default model: {config.default_model}")
213
+
214
+ config.default_reasoning_effort = _select_reasoning(console, "default", config.default_reasoning_effort)
215
+ console.print(f"[green]✓[/green] Default reasoning: {config.default_reasoning_effort}")
216
+
217
+ # === Per-Agent Configuration ===
218
+ customize_agents = Prompt.ask(
219
+ "\n[cyan]Customize individual agent settings?[/cyan]",
220
+ choices=["y", "n"],
221
+ default="n",
222
+ )
223
+
224
+ if customize_agents.lower() == "y":
225
+ console.print("\n[bold]─── Agent Settings ───[/bold]")
226
+ console.print("[dim]Configure each agent or press Enter to use defaults[/dim]")
227
+
228
+ for agent_type in AGENT_TYPES:
229
+ agent_cfg = getattr(config, agent_type)
230
+
231
+ console.print(f"\n[bold cyan]{agent_type.upper()}[/bold cyan]")
232
+
233
+ # Model
234
+ agent_cfg.model = _select_model(console, agent_type, agent_cfg.model)
235
+
236
+ # Reasoning
237
+ agent_cfg.reasoning_effort = _select_reasoning(console, agent_type, agent_cfg.reasoning_effort)
238
+
239
+ console.print(f"[green]✓[/green] {agent_type}: {agent_cfg.model} ({agent_cfg.reasoning_effort})")
240
+
241
+ # === Save Config ===
242
+ config_path = config.save(workspace=True)
243
+
244
+ console.print(f"\n[bold green]✓ Workspace initialized![/bold green]")
245
+ console.print(f" Config: {config_path}")
246
+ console.print(f" Subject: {config.subject}")
247
+ console.print(f" Default model: {config.default_model}")
248
+ console.print(f"\n[dim]Edit .vbagent.json to further customize settings[/dim]")
vbagent/cli/main.py CHANGED
@@ -54,6 +54,8 @@ LAZY_SUBCOMMANDS = {
54
54
  "ref": "vbagent.cli.ref",
55
55
  "config": "vbagent.cli.config",
56
56
  "check": "vbagent.cli.check",
57
+ "init": "vbagent.cli.init",
58
+ "util": "vbagent.cli.util",
57
59
  }
58
60
 
59
61
 
@@ -71,20 +73,28 @@ def main():
71
73
 
72
74
  \b
73
75
  Commands:
74
- classify - Stage 1: Classify physics question image
76
+ init - Initialize workspace config (.vbagent.json)
77
+ process - Full pipeline orchestration
78
+ classify - Stage 1: Classify question image
75
79
  scan - Stage 2: Extract LaTeX from image
76
80
  tikz - Generate TikZ code for diagrams
77
- idea - Extract physics concepts and ideas
81
+ idea - Extract concepts and ideas
78
82
  alternate - Generate alternative solutions
79
83
  variant - Generate problem variants
80
84
  convert - Convert between question formats
81
- process - Full pipeline orchestration
82
85
  batch - Batch processing with resume capability
86
+ check - QA review with interactive approval
83
87
  ref - Manage reference context files
84
88
  config - Configure models and settings
85
- check - QA review with interactive approval
89
+ util - File utilities (rename, count, clean)
86
90
  """
87
- pass
91
+ # Disable tracing early (before agents SDK import) for non-OpenAI providers.
92
+ # The SDK initializes tracing at import time, so the env var must be set first.
93
+ from vbagent.config import get_config
94
+ cfg = get_config()
95
+ if cfg.base_url:
96
+ import os
97
+ os.environ["OPENAI_AGENTS_DISABLE_TRACING"] = "1"
88
98
 
89
99
 
90
100
  if __name__ == "__main__":
vbagent/cli/process.py CHANGED
@@ -496,6 +496,16 @@ CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
496
496
  default=1,
497
497
  help="Number of images to process in parallel (default: 1, max recommended: 5)"
498
498
  )
499
+ @click.option(
500
+ "-c", "--compile", "do_compile",
501
+ is_flag=True,
502
+ help="Compile generated LaTeX to validate; retry with agent on failure"
503
+ )
504
+ @click.option(
505
+ "--verbose-compile", "verbose_compile",
506
+ is_flag=True,
507
+ help="Show full LaTeX document + preamble before each compile and prompt to continue/skip/quit"
508
+ )
499
509
  def process(
500
510
  image: Optional[str],
501
511
  tex: Optional[str],
@@ -507,6 +517,8 @@ def process(
507
517
  output: str,
508
518
  context: bool,
509
519
  parallel: int,
520
+ do_compile: bool,
521
+ verbose_compile: bool,
510
522
  ):
511
523
  """Full pipeline: Classify → Scan → TikZ → Ideas → Variants.
512
524
 
@@ -625,6 +637,28 @@ def process(
625
637
  generate_ideas=ideas,
626
638
  use_context=context,
627
639
  )
640
+
641
+ # Compile validation if -c flag
642
+ if do_compile:
643
+ from vbagent.compile import compile_and_retry
644
+ from vbagent.agents.compile_fixer import fix_latex
645
+ from vbagent.config import get_config as _get_cfg
646
+ _subj = _get_cfg().subject
647
+
648
+ console.print("[dim] → Compiling scanned LaTeX...[/dim]")
649
+ result.latex, _ = compile_and_retry(
650
+ result.latex, retry_fn=fix_latex,
651
+ subject=_subj, console=console,
652
+ verbose=verbose_compile,
653
+ )
654
+ if result.tikz_code:
655
+ console.print("[dim] → Compiling TikZ...[/dim]")
656
+ result.tikz_code, _ = compile_and_retry(
657
+ result.tikz_code, retry_fn=fix_latex,
658
+ subject=_subj, console=console,
659
+ verbose=verbose_compile,
660
+ )
661
+
628
662
  results.append(result)
629
663
 
630
664
  # Save immediately after each successful processing
vbagent/cli/scan.py CHANGED
@@ -66,7 +66,17 @@ CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
66
66
  type=click.Path(),
67
67
  help="Output TeX file path for saving results"
68
68
  )
69
- def scan(image: str | None, tex: str | None, question_type: str | None, output: str | None):
69
+ @click.option(
70
+ "-c", "--compile", "do_compile",
71
+ is_flag=True,
72
+ help="Compile LaTeX to validate; retry with agent on failure"
73
+ )
74
+ @click.option(
75
+ "--verbose-compile", "verbose_compile",
76
+ is_flag=True,
77
+ help="Show full LaTeX document + preamble before each compile and prompt to continue/skip/quit"
78
+ )
79
+ def scan(image: str | None, tex: str | None, question_type: str | None, output: str | None, do_compile: bool, verbose_compile: bool):
70
80
  """Stage 2: Extract LaTeX from physics question image.
71
81
 
72
82
  Runs classification first (unless --type provided), then extracts LaTeX
@@ -118,6 +128,22 @@ def scan(image: str | None, tex: str | None, question_type: str | None, output:
118
128
  # Display result
119
129
  display_scan_result(result, console)
120
130
 
131
+ # Compile validation if -c flag
132
+ if do_compile:
133
+ from vbagent.compile import compile_and_retry
134
+ from vbagent.agents.compile_fixer import fix_latex
135
+ from vbagent.config import get_config
136
+
137
+ subject = get_config().subject
138
+ console.print("[dim] → Compiling LaTeX...[/dim]")
139
+ result.latex, compile_result = compile_and_retry(
140
+ result.latex,
141
+ retry_fn=fix_latex,
142
+ subject=subject,
143
+ console=console,
144
+ verbose=verbose_compile,
145
+ )
146
+
121
147
  # Save to file if output path specified
122
148
  if output:
123
149
  output_path = Path(output)
vbagent/cli/tikz.py CHANGED
@@ -51,11 +51,23 @@ CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
51
51
  type=click.Path(),
52
52
  help="Output TeX file path for saving the generated TikZ code"
53
53
  )
54
+ @click.option(
55
+ "-c", "--compile", "do_compile",
56
+ is_flag=True,
57
+ help="Compile TikZ to validate; retry with agent on failure"
58
+ )
59
+ @click.option(
60
+ "--verbose-compile", "verbose_compile",
61
+ is_flag=True,
62
+ help="Show full LaTeX document + preamble before each compile and prompt to continue/skip/quit"
63
+ )
54
64
  def tikz(
55
65
  image: str | None,
56
66
  description: str | None,
57
67
  ref_dirs: tuple[str, ...],
58
- output: str | None
68
+ output: str | None,
69
+ do_compile: bool,
70
+ verbose_compile: bool,
59
71
  ):
60
72
  """Generate TikZ code for physics diagrams.
61
73
 
@@ -110,6 +122,22 @@ def tikz(
110
122
  syntax = _get_syntax(tikz_code, "latex", theme="monokai", line_numbers=True)
111
123
  console.print(_get_panel(syntax, title="Generated TikZ Code", border_style="green"))
112
124
 
125
+ # Compile validation if -c flag
126
+ if do_compile:
127
+ from vbagent.compile import compile_and_retry
128
+ from vbagent.agents.compile_fixer import fix_latex
129
+ from vbagent.config import get_config
130
+
131
+ subject = get_config().subject
132
+ console.print("[dim] → Compiling TikZ...[/dim]")
133
+ tikz_code, compile_result = compile_and_retry(
134
+ tikz_code,
135
+ retry_fn=fix_latex,
136
+ subject=subject,
137
+ console=console,
138
+ verbose=verbose_compile,
139
+ )
140
+
113
141
  # Save to file if output path specified
114
142
  if output:
115
143
  output_path = Path(output)