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 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")
@@ -1,32 +1,56 @@
1
- """Classifier agent for physics question image classification.
1
+ """Classifier agent for question image classification.
2
2
 
3
- Uses openai-agents SDK to analyze physics question images and extract
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 SYSTEM_PROMPT, USER_TEMPLATE
19
+ from vbagent.prompts.classifier import get_classifier_prompt, get_user_template
14
20
 
15
21
 
16
- # Create the classifier agent with structured output
17
- classifier_agent = create_agent(
18
- name="Classifier",
19
- instructions=SYSTEM_PROMPT,
20
- output_type=ClassificationResult,
21
- agent_type="classifier",
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
- def classify(image_path: str) -> ClassificationResult:
26
- """Analyze a physics question image and return structured metadata.
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
- message = create_image_message(image_path, USER_TEMPLATE)
38
- result = run_agent_sync(classifier_agent, message)
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 physics question images.
1
+ """Scanner agent for extracting LaTeX from question images.
2
2
 
3
- Uses openai-agents SDK to analyze physics question images and extract
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, USER_TEMPLATE
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(question_type: str, use_context: bool = True) -> "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
- prompt = get_scanner_prompt(question_type)
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 physics question image.
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
- agent = create_scanner_agent(classification.question_type, use_context)
100
- message = create_image_message(image_path, USER_TEMPLATE)
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 physics question image with explicit type.
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
- agent = create_scanner_agent(question_type, use_context)
134
- message = create_image_message(image_path, USER_TEMPLATE)
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 available models and config location
99
- console.print("\n[dim]Available models:[/dim]")
100
- console.print(f"[dim]{', '.join(MODELS.keys())}[/dim]")
101
- console.print(f"\n[dim]Config file: {CONFIG_FILE}[/dim]")
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
- def set(agent_type: str, model: str, reasoning: str, temperature: float, max_tokens: int):
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: {CONFIG_FILE}[/dim]")
193
+ console.print(f"\n[dim]Saved to: {config_path}[/dim]")
165
194
 
166
195
 
167
196
  @config.command()
168
- def reset():
169
- """Reset all configurations to defaults."""
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
- console.print("[green]✓[/green] Configuration reset to defaults")
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
- o_models = [m for m in MODELS.keys() if m.startswith("o")]
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
- console.print("[cyan]GPT Models:[/cyan]")
186
- for m in gpt_models:
187
- console.print(f" {m}")
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
- console.print("\n[cyan]Reasoning Models (o-series):[/cyan]")
190
- for m in o_models:
191
- console.print(f" • {m}")
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]")