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/agents/base.py
CHANGED
|
@@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, Any, Optional
|
|
|
8
8
|
if TYPE_CHECKING:
|
|
9
9
|
from agents import Agent, ModelSettings
|
|
10
10
|
|
|
11
|
-
from vbagent.config import get_model, get_model_settings
|
|
11
|
+
from vbagent.config import get_model, get_model_settings, apply_provider_config
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
def _get_agent_class():
|
|
@@ -115,6 +115,9 @@ def create_agent(
|
|
|
115
115
|
"""
|
|
116
116
|
Agent = _get_agent_class()
|
|
117
117
|
|
|
118
|
+
# Apply provider config (base_url, api_key) before creating agent
|
|
119
|
+
apply_provider_config()
|
|
120
|
+
|
|
118
121
|
# Get model and settings from config if not explicitly provided
|
|
119
122
|
if model is None:
|
|
120
123
|
model = get_model(agent_type or "default")
|
vbagent/agents/classifier.py
CHANGED
|
@@ -1,32 +1,56 @@
|
|
|
1
|
-
"""Classifier agent for
|
|
1
|
+
"""Classifier agent for question image classification.
|
|
2
2
|
|
|
3
|
-
Uses openai-agents SDK to analyze
|
|
3
|
+
Uses openai-agents SDK to analyze question images and extract
|
|
4
4
|
structured metadata including question type, difficulty, topic, etc.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
from typing import TYPE_CHECKING, Optional
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from agents import Agent
|
|
11
|
+
|
|
7
12
|
from vbagent.agents.base import (
|
|
8
13
|
create_agent,
|
|
9
14
|
create_image_message,
|
|
10
15
|
run_agent_sync,
|
|
11
16
|
)
|
|
17
|
+
from vbagent.config import get_config
|
|
12
18
|
from vbagent.models.classification import ClassificationResult
|
|
13
|
-
from vbagent.prompts.classifier import
|
|
19
|
+
from vbagent.prompts.classifier import get_classifier_prompt, get_user_template
|
|
14
20
|
|
|
15
21
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
def create_classifier_agent(subject: Optional[str] = None) -> "Agent":
|
|
23
|
+
"""Create a classifier agent for a subject.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
subject: Subject override (uses config if not provided)
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Configured Agent instance for classification
|
|
30
|
+
"""
|
|
31
|
+
if subject is None:
|
|
32
|
+
subject = get_config().subject
|
|
33
|
+
|
|
34
|
+
prompt = get_classifier_prompt(subject)
|
|
35
|
+
|
|
36
|
+
return create_agent(
|
|
37
|
+
name=f"Classifier-{subject}",
|
|
38
|
+
instructions=prompt,
|
|
39
|
+
output_type=ClassificationResult,
|
|
40
|
+
agent_type="classifier",
|
|
41
|
+
)
|
|
23
42
|
|
|
24
43
|
|
|
25
|
-
|
|
26
|
-
|
|
44
|
+
# Legacy: Create default classifier agent for backward compatibility
|
|
45
|
+
classifier_agent = create_classifier_agent("physics")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def classify(image_path: str, subject: Optional[str] = None) -> ClassificationResult:
|
|
49
|
+
"""Analyze a question image and return structured metadata.
|
|
27
50
|
|
|
28
51
|
Args:
|
|
29
52
|
image_path: Path to the image file to classify
|
|
53
|
+
subject: Subject override (uses config if not provided)
|
|
30
54
|
|
|
31
55
|
Returns:
|
|
32
56
|
ClassificationResult with extracted metadata
|
|
@@ -34,6 +58,11 @@ def classify(image_path: str) -> ClassificationResult:
|
|
|
34
58
|
Raises:
|
|
35
59
|
FileNotFoundError: If the image file doesn't exist
|
|
36
60
|
"""
|
|
37
|
-
|
|
38
|
-
|
|
61
|
+
if subject is None:
|
|
62
|
+
subject = get_config().subject
|
|
63
|
+
|
|
64
|
+
agent = create_classifier_agent(subject)
|
|
65
|
+
user_template = get_user_template(subject)
|
|
66
|
+
message = create_image_message(image_path, user_template)
|
|
67
|
+
result = run_agent_sync(agent, message)
|
|
39
68
|
return result
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Agent for fixing LaTeX compilation errors.
|
|
2
|
+
|
|
3
|
+
Takes a LaTeX snippet and pdflatex error output, returns corrected LaTeX.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from vbagent.agents.base import create_agent, run_agent_sync
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
SYSTEM_PROMPT = r"""You are a LaTeX error fixer. You receive LaTeX code that failed to compile and the pdflatex error output.
|
|
10
|
+
|
|
11
|
+
Your job:
|
|
12
|
+
1. Read the error messages carefully
|
|
13
|
+
2. Fix ONLY the errors — do not change the content or structure
|
|
14
|
+
3. Common fixes: missing braces, undefined commands, wrong environment names, missing $ delimiters
|
|
15
|
+
4. Output ONLY the corrected LaTeX code — no explanations, no markdown, no code blocks
|
|
16
|
+
|
|
17
|
+
CRITICAL: Output the EXACT same content with ONLY the compilation errors fixed. Do not add \documentclass, preamble, or any wrapping."""
|
|
18
|
+
|
|
19
|
+
USER_TEMPLATE = """Fix the compilation errors in this LaTeX code.
|
|
20
|
+
|
|
21
|
+
**Errors from pdflatex:**
|
|
22
|
+
```
|
|
23
|
+
{errors}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**LaTeX code to fix:**
|
|
27
|
+
```latex
|
|
28
|
+
{latex}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Output ONLY the corrected LaTeX code:"""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def fix_latex(error_summary: str, latex: str) -> str:
|
|
35
|
+
"""Send LaTeX + errors to agent and get fixed version.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
error_summary: Parsed pdflatex error output
|
|
39
|
+
latex: The LaTeX code that failed to compile
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Corrected LaTeX code
|
|
43
|
+
"""
|
|
44
|
+
agent = create_agent(
|
|
45
|
+
name="LaTeX-Fixer",
|
|
46
|
+
instructions=SYSTEM_PROMPT,
|
|
47
|
+
agent_type="converter", # Use converter model (lighter/cheaper)
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
prompt = USER_TEMPLATE.format(errors=error_summary, latex=latex)
|
|
51
|
+
result = run_agent_sync(agent, prompt)
|
|
52
|
+
|
|
53
|
+
# Clean markdown artifacts
|
|
54
|
+
import re
|
|
55
|
+
result = re.sub(r'^```(?:latex|tex)?\s*\n?', '', result, flags=re.IGNORECASE)
|
|
56
|
+
result = re.sub(r'\n?```\s*$', '', result)
|
|
57
|
+
return result.strip()
|
vbagent/agents/scanner.py
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
"""Scanner agent for extracting LaTeX from
|
|
1
|
+
"""Scanner agent for extracting LaTeX from question images.
|
|
2
2
|
|
|
3
|
-
Uses openai-agents SDK to analyze
|
|
4
|
-
LaTeX code using type-specific prompts.
|
|
3
|
+
Uses openai-agents SDK to analyze question images and extract
|
|
4
|
+
LaTeX code using type-specific and subject-specific prompts.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import re
|
|
8
|
-
from typing import TYPE_CHECKING
|
|
8
|
+
from typing import TYPE_CHECKING, Optional
|
|
9
9
|
|
|
10
10
|
if TYPE_CHECKING:
|
|
11
11
|
from agents import Agent
|
|
@@ -15,9 +15,10 @@ from vbagent.agents.base import (
|
|
|
15
15
|
create_image_message,
|
|
16
16
|
run_agent_sync,
|
|
17
17
|
)
|
|
18
|
+
from vbagent.config import get_config
|
|
18
19
|
from vbagent.models.classification import ClassificationResult
|
|
19
20
|
from vbagent.models.scan import ScanResult
|
|
20
|
-
from vbagent.prompts.scanner import get_scanner_prompt,
|
|
21
|
+
from vbagent.prompts.scanner import get_scanner_prompt, get_user_template
|
|
21
22
|
from vbagent.references.context import get_context_prompt_section
|
|
22
23
|
|
|
23
24
|
|
|
@@ -51,17 +52,26 @@ def clean_latex_output(latex: str) -> str:
|
|
|
51
52
|
return latex.strip()
|
|
52
53
|
|
|
53
54
|
|
|
54
|
-
def create_scanner_agent(
|
|
55
|
+
def create_scanner_agent(
|
|
56
|
+
question_type: str,
|
|
57
|
+
use_context: bool = True,
|
|
58
|
+
subject: Optional[str] = None,
|
|
59
|
+
) -> "Agent":
|
|
55
60
|
"""Create a scanner agent with type-specific prompt.
|
|
56
61
|
|
|
57
62
|
Args:
|
|
58
63
|
question_type: The type of question (mcq_sc, mcq_mc, etc.)
|
|
59
64
|
use_context: Whether to include reference context in prompt
|
|
65
|
+
subject: Subject override (uses config if not provided)
|
|
60
66
|
|
|
61
67
|
Returns:
|
|
62
68
|
Configured Agent instance for scanning that question type
|
|
63
69
|
"""
|
|
64
|
-
|
|
70
|
+
# Get subject from config if not provided
|
|
71
|
+
if subject is None:
|
|
72
|
+
subject = get_config().subject
|
|
73
|
+
|
|
74
|
+
prompt = get_scanner_prompt(question_type, subject)
|
|
65
75
|
|
|
66
76
|
# Add reference context if enabled
|
|
67
77
|
context = get_context_prompt_section("latex", use_context)
|
|
@@ -69,7 +79,7 @@ def create_scanner_agent(question_type: str, use_context: bool = True) -> "Agent
|
|
|
69
79
|
prompt = prompt + "\n" + context
|
|
70
80
|
|
|
71
81
|
return create_agent(
|
|
72
|
-
name=f"Scanner-{question_type}",
|
|
82
|
+
name=f"Scanner-{question_type}-{subject}",
|
|
73
83
|
instructions=prompt,
|
|
74
84
|
agent_type="scanner",
|
|
75
85
|
)
|
|
@@ -79,8 +89,9 @@ def scan(
|
|
|
79
89
|
image_path: str,
|
|
80
90
|
classification: ClassificationResult,
|
|
81
91
|
use_context: bool = True,
|
|
92
|
+
subject: Optional[str] = None,
|
|
82
93
|
) -> ScanResult:
|
|
83
|
-
"""Extract LaTeX from a
|
|
94
|
+
"""Extract LaTeX from a question image.
|
|
84
95
|
|
|
85
96
|
Uses the classification result to select the appropriate prompt
|
|
86
97
|
for the question type.
|
|
@@ -89,6 +100,7 @@ def scan(
|
|
|
89
100
|
image_path: Path to the image file to scan
|
|
90
101
|
classification: Classification result with question type info
|
|
91
102
|
use_context: Whether to include reference context in prompt
|
|
103
|
+
subject: Subject override (uses config if not provided)
|
|
92
104
|
|
|
93
105
|
Returns:
|
|
94
106
|
ScanResult with extracted LaTeX and diagram info
|
|
@@ -96,8 +108,13 @@ def scan(
|
|
|
96
108
|
Raises:
|
|
97
109
|
FileNotFoundError: If the image file doesn't exist
|
|
98
110
|
"""
|
|
99
|
-
|
|
100
|
-
|
|
111
|
+
# Get subject from config if not provided
|
|
112
|
+
if subject is None:
|
|
113
|
+
subject = get_config().subject
|
|
114
|
+
|
|
115
|
+
agent = create_scanner_agent(classification.question_type, use_context, subject)
|
|
116
|
+
user_template = get_user_template(subject)
|
|
117
|
+
message = create_image_message(image_path, user_template)
|
|
101
118
|
raw_latex = run_agent_sync(agent, message)
|
|
102
119
|
|
|
103
120
|
# Clean up markdown artifacts from LLM output
|
|
@@ -114,8 +131,9 @@ def scan_with_type(
|
|
|
114
131
|
image_path: str,
|
|
115
132
|
question_type: str,
|
|
116
133
|
use_context: bool = True,
|
|
134
|
+
subject: Optional[str] = None,
|
|
117
135
|
) -> ScanResult:
|
|
118
|
-
"""Extract LaTeX from a
|
|
136
|
+
"""Extract LaTeX from a question image with explicit type.
|
|
119
137
|
|
|
120
138
|
Bypasses classification and uses the provided question type directly.
|
|
121
139
|
|
|
@@ -123,6 +141,7 @@ def scan_with_type(
|
|
|
123
141
|
image_path: Path to the image file to scan
|
|
124
142
|
question_type: The type of question (mcq_sc, mcq_mc, etc.)
|
|
125
143
|
use_context: Whether to include reference context in prompt
|
|
144
|
+
subject: Subject override (uses config if not provided)
|
|
126
145
|
|
|
127
146
|
Returns:
|
|
128
147
|
ScanResult with extracted LaTeX
|
|
@@ -130,8 +149,13 @@ def scan_with_type(
|
|
|
130
149
|
Raises:
|
|
131
150
|
FileNotFoundError: If the image file doesn't exist
|
|
132
151
|
"""
|
|
133
|
-
|
|
134
|
-
|
|
152
|
+
# Get subject from config if not provided
|
|
153
|
+
if subject is None:
|
|
154
|
+
subject = get_config().subject
|
|
155
|
+
|
|
156
|
+
agent = create_scanner_agent(question_type, use_context, subject)
|
|
157
|
+
user_template = get_user_template(subject)
|
|
158
|
+
message = create_image_message(image_path, user_template)
|
|
135
159
|
raw_latex = run_agent_sync(agent, message)
|
|
136
160
|
|
|
137
161
|
# Clean up markdown artifacts from LLM output
|
vbagent/cli/config.py
CHANGED
|
@@ -9,9 +9,18 @@ from vbagent.config import (
|
|
|
9
9
|
get_config,
|
|
10
10
|
save_config,
|
|
11
11
|
reset_config,
|
|
12
|
+
init_workspace,
|
|
13
|
+
has_workspace_config,
|
|
14
|
+
get_workspace_config_path,
|
|
15
|
+
get_provider_name,
|
|
16
|
+
apply_model_group,
|
|
12
17
|
AGENT_TYPES,
|
|
13
18
|
MODELS,
|
|
19
|
+
MODEL_GROUPS,
|
|
20
|
+
SUBJECTS,
|
|
21
|
+
PROVIDERS,
|
|
14
22
|
CONFIG_FILE,
|
|
23
|
+
WORKSPACE_CONFIG_FILE,
|
|
15
24
|
)
|
|
16
25
|
|
|
17
26
|
|
|
@@ -64,6 +73,13 @@ def show():
|
|
|
64
73
|
console = _get_console()
|
|
65
74
|
cfg = get_config()
|
|
66
75
|
|
|
76
|
+
# Show config source
|
|
77
|
+
workspace_path = get_workspace_config_path()
|
|
78
|
+
if workspace_path:
|
|
79
|
+
console.print(f"[dim]Using workspace config: {workspace_path}[/dim]\n")
|
|
80
|
+
else:
|
|
81
|
+
console.print(f"[dim]Using global config: {CONFIG_FILE}[/dim]\n")
|
|
82
|
+
|
|
67
83
|
# Create table
|
|
68
84
|
table = _get_table(title="Agent Model Configuration")
|
|
69
85
|
table.add_column("Agent", style="cyan")
|
|
@@ -95,10 +111,21 @@ def show():
|
|
|
95
111
|
|
|
96
112
|
console.print(table)
|
|
97
113
|
|
|
98
|
-
# Show
|
|
99
|
-
console.print("\n[
|
|
100
|
-
console.print(f"[
|
|
101
|
-
|
|
114
|
+
# Show subject and provider
|
|
115
|
+
console.print(f"\n[bold]Subject:[/bold] {cfg.subject}")
|
|
116
|
+
console.print(f"[bold]Provider:[/bold] {get_provider_name()}")
|
|
117
|
+
if cfg.base_url:
|
|
118
|
+
console.print(f"[bold]Base URL:[/bold] {cfg.base_url}")
|
|
119
|
+
if cfg.api_key:
|
|
120
|
+
# Mask the API key
|
|
121
|
+
masked = cfg.api_key[:8] + "..." + cfg.api_key[-4:] if len(cfg.api_key) > 12 else "***"
|
|
122
|
+
console.print(f"[bold]API Key:[/bold] {masked}")
|
|
123
|
+
|
|
124
|
+
# Show available models
|
|
125
|
+
console.print(f"\n[dim]Available models: {', '.join(MODELS.keys())}[/dim]")
|
|
126
|
+
console.print(f"[dim]Available subjects: {', '.join(SUBJECTS)}[/dim]")
|
|
127
|
+
console.print(f"[dim]Known providers: {', '.join(PROVIDERS.keys())}[/dim]")
|
|
128
|
+
console.print(f"[dim]Model groups: {', '.join(MODEL_GROUPS.keys())}[/dim]")
|
|
102
129
|
|
|
103
130
|
|
|
104
131
|
@config.command()
|
|
@@ -111,7 +138,8 @@ def show():
|
|
|
111
138
|
)
|
|
112
139
|
@click.option("--temperature", "-t", type=float, help="Temperature (0.0-2.0)")
|
|
113
140
|
@click.option("--max-tokens", type=int, help="Maximum tokens")
|
|
114
|
-
|
|
141
|
+
@click.option("--workspace", "-w", is_flag=True, help="Save to workspace config instead of global")
|
|
142
|
+
def set(agent_type: str, model: str, reasoning: str, temperature: float, max_tokens: int, workspace: bool):
|
|
115
143
|
"""Set model configuration for an agent type.
|
|
116
144
|
|
|
117
145
|
\b
|
|
@@ -123,6 +151,7 @@ def set(agent_type: str, model: str, reasoning: str, temperature: float, max_tok
|
|
|
123
151
|
vbagent config set scanner --model gpt-4o
|
|
124
152
|
vbagent config set variant --model o1-mini --reasoning medium
|
|
125
153
|
vbagent config set default --model gpt-4.1
|
|
154
|
+
vbagent config set scanner -m gpt-4o --workspace # Save to .vbagent.json
|
|
126
155
|
"""
|
|
127
156
|
console = _get_console()
|
|
128
157
|
cfg = get_config()
|
|
@@ -146,7 +175,7 @@ def set(agent_type: str, model: str, reasoning: str, temperature: float, max_tok
|
|
|
146
175
|
console.print(f"[green]✓[/green] Updated {agent_type} configuration")
|
|
147
176
|
|
|
148
177
|
# Save to file
|
|
149
|
-
save_config()
|
|
178
|
+
config_path = save_config(workspace=workspace)
|
|
150
179
|
|
|
151
180
|
# Show updated config
|
|
152
181
|
if agent_type == "default":
|
|
@@ -161,15 +190,19 @@ def set(agent_type: str, model: str, reasoning: str, temperature: float, max_tok
|
|
|
161
190
|
if agent_cfg.max_tokens:
|
|
162
191
|
console.print(f" Max Tokens: {agent_cfg.max_tokens}")
|
|
163
192
|
|
|
164
|
-
console.print(f"\n[dim]Saved to: {
|
|
193
|
+
console.print(f"\n[dim]Saved to: {config_path}[/dim]")
|
|
165
194
|
|
|
166
195
|
|
|
167
196
|
@config.command()
|
|
168
|
-
|
|
169
|
-
|
|
197
|
+
@click.option("--workspace", "-w", is_flag=True, help="Reset workspace config instead of global")
|
|
198
|
+
def reset(workspace: bool):
|
|
199
|
+
"""Reset configuration to defaults."""
|
|
170
200
|
console = _get_console()
|
|
171
|
-
reset_config()
|
|
172
|
-
|
|
201
|
+
reset_config(workspace=workspace)
|
|
202
|
+
if workspace:
|
|
203
|
+
console.print("[green]✓[/green] Workspace configuration removed")
|
|
204
|
+
else:
|
|
205
|
+
console.print("[green]✓[/green] Global configuration reset to defaults")
|
|
173
206
|
|
|
174
207
|
|
|
175
208
|
@config.command()
|
|
@@ -178,14 +211,215 @@ def models():
|
|
|
178
211
|
console = _get_console()
|
|
179
212
|
console.print("[bold]Available Models:[/bold]\n")
|
|
180
213
|
|
|
181
|
-
# Group models
|
|
214
|
+
# Group models by provider
|
|
182
215
|
gpt_models = [m for m in MODELS.keys() if m.startswith("gpt")]
|
|
183
|
-
|
|
216
|
+
grok_models = [m for m in MODELS.keys() if m.startswith("grok")]
|
|
217
|
+
gemini_models = [m for m in MODELS.keys() if m.startswith("gemini")]
|
|
218
|
+
|
|
219
|
+
if gpt_models:
|
|
220
|
+
console.print("[cyan]OpenAI:[/cyan]")
|
|
221
|
+
for m in gpt_models:
|
|
222
|
+
console.print(f" • {m}")
|
|
223
|
+
|
|
224
|
+
if grok_models:
|
|
225
|
+
console.print("\n[cyan]xAI Grok:[/cyan]")
|
|
226
|
+
for m in grok_models:
|
|
227
|
+
console.print(f" • {m}")
|
|
228
|
+
|
|
229
|
+
if gemini_models:
|
|
230
|
+
console.print("\n[cyan]Google Gemini:[/cyan]")
|
|
231
|
+
for m in gemini_models:
|
|
232
|
+
console.print(f" • {m}")
|
|
233
|
+
|
|
234
|
+
console.print(f"\n[dim]Model groups available: {', '.join(MODEL_GROUPS.keys())}[/dim]")
|
|
235
|
+
console.print("[dim]Use 'vbagent config model-group' to view/apply groups[/dim]")
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@config.command()
|
|
240
|
+
@click.option("--force", "-f", is_flag=True, help="Overwrite existing workspace config")
|
|
241
|
+
@click.option("--quick", "-q", is_flag=True, help="Quick mode - only ask for subject")
|
|
242
|
+
@click.option("--yes", "-y", is_flag=True, help="Non-interactive mode with defaults")
|
|
243
|
+
@click.pass_context
|
|
244
|
+
def init(ctx, force: bool, quick: bool, yes: bool):
|
|
245
|
+
"""Initialize workspace config interactively.
|
|
246
|
+
|
|
247
|
+
Creates .vbagent.json in current directory with customized settings.
|
|
248
|
+
This is an alias for `vbagent init`.
|
|
249
|
+
|
|
250
|
+
\b
|
|
251
|
+
Examples:
|
|
252
|
+
vbagent config init # Interactive setup
|
|
253
|
+
vbagent config init --quick # Only ask for subject
|
|
254
|
+
vbagent config init --yes # Use all defaults
|
|
255
|
+
vbagent config init --force # Overwrite existing
|
|
256
|
+
"""
|
|
257
|
+
# Import and invoke the main init command
|
|
258
|
+
from vbagent.cli.init import init as main_init
|
|
259
|
+
ctx.invoke(main_init, force=force, quick=quick, yes=yes)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
@config.command()
|
|
263
|
+
@click.argument("subject", type=click.Choice(SUBJECTS))
|
|
264
|
+
@click.option("--workspace", "-w", is_flag=True, help="Set in workspace config")
|
|
265
|
+
def subject(subject: str, workspace: bool):
|
|
266
|
+
"""Set the subject for prompts.
|
|
267
|
+
|
|
268
|
+
\b
|
|
269
|
+
Subjects:
|
|
270
|
+
physics - Physics problems (default)
|
|
271
|
+
chemistry - Chemistry problems
|
|
272
|
+
mathematics - Mathematics problems
|
|
273
|
+
biology - Biology problems
|
|
274
|
+
|
|
275
|
+
\b
|
|
276
|
+
Examples:
|
|
277
|
+
vbagent config subject chemistry
|
|
278
|
+
vbagent config subject physics --workspace
|
|
279
|
+
"""
|
|
280
|
+
console = _get_console()
|
|
281
|
+
cfg = get_config()
|
|
282
|
+
cfg.subject = subject
|
|
283
|
+
config_path = save_config(workspace=workspace)
|
|
284
|
+
console.print(f"[green]✓[/green] Subject set to: {subject}")
|
|
285
|
+
console.print(f"[dim]Saved to: {config_path}[/dim]")
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
@config.command()
|
|
290
|
+
@click.argument("provider", required=False)
|
|
291
|
+
@click.option("--base-url", "-b", help="Custom base URL")
|
|
292
|
+
@click.option("--api-key", "-k", help="API key for the provider")
|
|
293
|
+
@click.option("--no-models", is_flag=True, help="Don't auto-switch agent models")
|
|
294
|
+
@click.option("--workspace", "-w", is_flag=True, help="Save to workspace config")
|
|
295
|
+
def provider(provider: str, base_url: str, api_key: str, no_models: bool, workspace: bool):
|
|
296
|
+
"""Set the API provider (openai, xai, google, or custom URL).
|
|
297
|
+
|
|
298
|
+
Switching providers auto-applies the matching model group so every
|
|
299
|
+
agent gets the right model. Use --no-models to skip this.
|
|
300
|
+
|
|
301
|
+
\b
|
|
302
|
+
Known Providers:
|
|
303
|
+
openai - OpenAI (default, no base_url needed)
|
|
304
|
+
xai - xAI Grok (https://api.x.ai/v1)
|
|
305
|
+
google - Google Gemini (OpenAI-compatible endpoint)
|
|
306
|
+
|
|
307
|
+
\b
|
|
308
|
+
Examples:
|
|
309
|
+
vbagent config provider openai
|
|
310
|
+
vbagent config provider xai --api-key xai-xxx
|
|
311
|
+
vbagent config provider xai --workspace
|
|
312
|
+
vbagent config provider xai --no-models # keep current models
|
|
313
|
+
vbagent config provider --base-url https://custom.api/v1
|
|
314
|
+
"""
|
|
315
|
+
console = _get_console()
|
|
316
|
+
cfg = get_config()
|
|
317
|
+
|
|
318
|
+
resolved_provider = None
|
|
319
|
+
|
|
320
|
+
if provider and provider in PROVIDERS:
|
|
321
|
+
cfg.base_url = PROVIDERS[provider]["base_url"]
|
|
322
|
+
resolved_provider = provider
|
|
323
|
+
console.print(f"[green]✓[/green] Provider: {provider}")
|
|
324
|
+
if PROVIDERS[provider]["base_url"]:
|
|
325
|
+
console.print(f" Base URL: {PROVIDERS[provider]['base_url']}")
|
|
326
|
+
env_key = PROVIDERS[provider]["env_key"]
|
|
327
|
+
console.print(f" API key env var: {env_key}")
|
|
328
|
+
else:
|
|
329
|
+
console.print(" Base URL: [dim]default (OpenAI)[/dim]")
|
|
330
|
+
elif base_url:
|
|
331
|
+
cfg.base_url = base_url
|
|
332
|
+
# Try to detect provider from custom URL
|
|
333
|
+
from vbagent.config import _provider_from_base_url
|
|
334
|
+
resolved_provider = _provider_from_base_url(base_url)
|
|
335
|
+
console.print(f"[green]✓[/green] Base URL: {base_url}")
|
|
336
|
+
elif provider:
|
|
337
|
+
console.print(f"[yellow]Unknown provider '{provider}'[/yellow]")
|
|
338
|
+
console.print(f"[dim]Known: {', '.join(PROVIDERS.keys())}[/dim]")
|
|
339
|
+
console.print("[dim]Or use --base-url for custom endpoints[/dim]")
|
|
340
|
+
return
|
|
184
341
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
342
|
+
if api_key:
|
|
343
|
+
cfg.api_key = api_key
|
|
344
|
+
masked = api_key[:8] + "..." + api_key[-4:] if len(api_key) > 12 else "***"
|
|
345
|
+
console.print(f"[green]✓[/green] API Key: {masked}")
|
|
188
346
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
347
|
+
# Auto-apply model group when switching providers
|
|
348
|
+
if resolved_provider and not no_models and resolved_provider in MODEL_GROUPS:
|
|
349
|
+
apply_model_group(cfg, resolved_provider)
|
|
350
|
+
console.print(f"[green]✓[/green] Applied [bold]{resolved_provider}[/bold] model group")
|
|
351
|
+
# Show the models that were set
|
|
352
|
+
table = _get_table(title=f"Model Group: {resolved_provider}")
|
|
353
|
+
table.add_column("Agent", style="cyan")
|
|
354
|
+
table.add_column("Model", style="green")
|
|
355
|
+
table.add_row("[bold]default[/bold]", cfg.default_model, style="dim")
|
|
356
|
+
for agent_type in AGENT_TYPES:
|
|
357
|
+
table.add_row(agent_type, getattr(cfg, agent_type).model)
|
|
358
|
+
console.print(table)
|
|
359
|
+
|
|
360
|
+
if not provider and not base_url and not api_key:
|
|
361
|
+
# Show current provider info
|
|
362
|
+
console.print(f"[bold]Current provider:[/bold] {get_provider_name()}")
|
|
363
|
+
if cfg.base_url:
|
|
364
|
+
console.print(f" Base URL: {cfg.base_url}")
|
|
365
|
+
if cfg.api_key:
|
|
366
|
+
masked = cfg.api_key[:8] + "..." + cfg.api_key[-4:] if len(cfg.api_key) > 12 else "***"
|
|
367
|
+
console.print(f" API Key: {masked}")
|
|
368
|
+
console.print(f"\n[dim]Known providers: {', '.join(PROVIDERS.keys())}[/dim]")
|
|
369
|
+
console.print(f"[dim]Model groups: {', '.join(MODEL_GROUPS.keys())}[/dim]")
|
|
370
|
+
return
|
|
371
|
+
|
|
372
|
+
config_path = save_config(workspace=workspace)
|
|
373
|
+
console.print(f"[dim]Saved to: {config_path}[/dim]")
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
@config.command("model-group")
|
|
377
|
+
@click.argument("group_name", required=False, type=click.Choice(list(MODEL_GROUPS.keys())))
|
|
378
|
+
@click.option("--workspace", "-w", is_flag=True, help="Save to workspace config")
|
|
379
|
+
def model_group(group_name: str, workspace: bool):
|
|
380
|
+
"""View or apply a model group.
|
|
381
|
+
|
|
382
|
+
Model groups are pre-configured sets of models for each agent,
|
|
383
|
+
optimized per provider. Switching providers auto-applies the
|
|
384
|
+
matching group, but you can also apply one manually.
|
|
385
|
+
|
|
386
|
+
\b
|
|
387
|
+
Examples:
|
|
388
|
+
vbagent config model-group # List all groups
|
|
389
|
+
vbagent config model-group openai # Apply OpenAI group
|
|
390
|
+
vbagent config model-group xai # Apply xAI group
|
|
391
|
+
vbagent config model-group google -w # Apply Google group to workspace
|
|
392
|
+
"""
|
|
393
|
+
console = _get_console()
|
|
394
|
+
|
|
395
|
+
if not group_name:
|
|
396
|
+
# Show all model groups
|
|
397
|
+
for name, group in MODEL_GROUPS.items():
|
|
398
|
+
table = _get_table(title=f"Model Group: {name}")
|
|
399
|
+
table.add_column("Agent", style="cyan")
|
|
400
|
+
table.add_column("Model", style="green")
|
|
401
|
+
table.add_row("[bold]default[/bold]", group["default_model"], style="dim")
|
|
402
|
+
for agent_type in AGENT_TYPES:
|
|
403
|
+
if agent_type in group:
|
|
404
|
+
table.add_row(agent_type, group[agent_type])
|
|
405
|
+
console.print(table)
|
|
406
|
+
console.print()
|
|
407
|
+
return
|
|
408
|
+
|
|
409
|
+
cfg = get_config()
|
|
410
|
+
apply_model_group(cfg, group_name)
|
|
411
|
+
config_path = save_config(workspace=workspace)
|
|
412
|
+
|
|
413
|
+
console.print(f"[green]✓[/green] Applied [bold]{group_name}[/bold] model group")
|
|
414
|
+
if cfg.base_url:
|
|
415
|
+
console.print(f" Base URL: {cfg.base_url}")
|
|
416
|
+
else:
|
|
417
|
+
console.print(" Base URL: [dim]default (OpenAI)[/dim]")
|
|
418
|
+
table = _get_table(title=f"Model Group: {group_name}")
|
|
419
|
+
table.add_column("Agent", style="cyan")
|
|
420
|
+
table.add_column("Model", style="green")
|
|
421
|
+
table.add_row("[bold]default[/bold]", cfg.default_model, style="dim")
|
|
422
|
+
for agent_type in AGENT_TYPES:
|
|
423
|
+
table.add_row(agent_type, getattr(cfg, agent_type).model)
|
|
424
|
+
console.print(table)
|
|
425
|
+
console.print(f"[dim]Saved to: {config_path}[/dim]")
|