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/agents/base.py +4 -1
- vbagent/agents/classifier.py +43 -14
- vbagent/agents/compile_fixer.py +57 -0
- vbagent/agents/scanner.py +38 -14
- vbagent/cli/config.py +253 -19
- vbagent/cli/init.py +248 -0
- vbagent/cli/main.py +15 -5
- vbagent/cli/process.py +34 -0
- vbagent/cli/scan.py +27 -1
- vbagent/cli/tikz.py +29 -1
- vbagent/cli/util.py +327 -0
- vbagent/cli/variant.py +27 -0
- vbagent/compile.py +444 -0
- vbagent/config.py +398 -24
- vbagent/prompts/classifier.py +39 -5
- vbagent/prompts/scanner/__init__.py +71 -6
- vbagent/prompts/subjects/__init__.py +328 -0
- {vbagent-0.2.0.dist-info → vbagent-0.2.1.dist-info}/METADATA +1 -1
- {vbagent-0.2.0.dist-info → vbagent-0.2.1.dist-info}/RECORD +21 -16
- {vbagent-0.2.0.dist-info → vbagent-0.2.1.dist-info}/WHEEL +0 -0
- {vbagent-0.2.0.dist-info → vbagent-0.2.1.dist-info}/entry_points.txt +0 -0
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
|
-
|
|
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
|
|
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
|
-
|
|
89
|
+
util - File utilities (rename, count, clean)
|
|
86
90
|
"""
|
|
87
|
-
|
|
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
|
-
|
|
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)
|