parishad 0.1.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.
Files changed (68) hide show
  1. parishad/__init__.py +70 -0
  2. parishad/__main__.py +10 -0
  3. parishad/checker/__init__.py +25 -0
  4. parishad/checker/deterministic.py +644 -0
  5. parishad/checker/ensemble.py +496 -0
  6. parishad/checker/retrieval.py +546 -0
  7. parishad/cli/__init__.py +6 -0
  8. parishad/cli/code.py +3254 -0
  9. parishad/cli/main.py +1158 -0
  10. parishad/cli/prarambh.py +99 -0
  11. parishad/cli/sthapana.py +368 -0
  12. parishad/config/modes.py +139 -0
  13. parishad/config/pipeline.core.yaml +128 -0
  14. parishad/config/pipeline.extended.yaml +172 -0
  15. parishad/config/pipeline.fast.yaml +89 -0
  16. parishad/config/user_config.py +115 -0
  17. parishad/data/catalog.py +118 -0
  18. parishad/data/models.json +108 -0
  19. parishad/memory/__init__.py +79 -0
  20. parishad/models/__init__.py +181 -0
  21. parishad/models/backends/__init__.py +247 -0
  22. parishad/models/backends/base.py +211 -0
  23. parishad/models/backends/huggingface.py +318 -0
  24. parishad/models/backends/llama_cpp.py +239 -0
  25. parishad/models/backends/mlx_lm.py +141 -0
  26. parishad/models/backends/ollama.py +253 -0
  27. parishad/models/backends/openai_api.py +193 -0
  28. parishad/models/backends/transformers_hf.py +198 -0
  29. parishad/models/costs.py +385 -0
  30. parishad/models/downloader.py +1557 -0
  31. parishad/models/optimizations.py +871 -0
  32. parishad/models/profiles.py +610 -0
  33. parishad/models/reliability.py +876 -0
  34. parishad/models/runner.py +651 -0
  35. parishad/models/tokenization.py +287 -0
  36. parishad/orchestrator/__init__.py +24 -0
  37. parishad/orchestrator/config_loader.py +210 -0
  38. parishad/orchestrator/engine.py +1113 -0
  39. parishad/orchestrator/exceptions.py +14 -0
  40. parishad/roles/__init__.py +71 -0
  41. parishad/roles/base.py +712 -0
  42. parishad/roles/dandadhyaksha.py +163 -0
  43. parishad/roles/darbari.py +246 -0
  44. parishad/roles/majumdar.py +274 -0
  45. parishad/roles/pantapradhan.py +150 -0
  46. parishad/roles/prerak.py +357 -0
  47. parishad/roles/raja.py +345 -0
  48. parishad/roles/sacheev.py +203 -0
  49. parishad/roles/sainik.py +427 -0
  50. parishad/roles/sar_senapati.py +164 -0
  51. parishad/roles/vidushak.py +69 -0
  52. parishad/tools/__init__.py +7 -0
  53. parishad/tools/base.py +57 -0
  54. parishad/tools/fs.py +110 -0
  55. parishad/tools/perception.py +96 -0
  56. parishad/tools/retrieval.py +74 -0
  57. parishad/tools/shell.py +103 -0
  58. parishad/utils/__init__.py +7 -0
  59. parishad/utils/hardware.py +122 -0
  60. parishad/utils/logging.py +79 -0
  61. parishad/utils/scanner.py +164 -0
  62. parishad/utils/text.py +61 -0
  63. parishad/utils/tracing.py +133 -0
  64. parishad-0.1.0.dist-info/METADATA +256 -0
  65. parishad-0.1.0.dist-info/RECORD +68 -0
  66. parishad-0.1.0.dist-info/WHEEL +4 -0
  67. parishad-0.1.0.dist-info/entry_points.txt +2 -0
  68. parishad-0.1.0.dist-info/licenses/LICENSE +21 -0
parishad/cli/main.py ADDED
@@ -0,0 +1,1158 @@
1
+ """
2
+ Parishad CLI - Command line interface for the Parishad council.
3
+ """
4
+
5
+ import json
6
+ import os
7
+ import sys
8
+ from pathlib import Path
9
+ from typing import Optional
10
+
11
+ import click
12
+ from rich.console import Console
13
+ from rich.panel import Panel
14
+ from rich.syntax import Syntax
15
+ from rich.table import Table
16
+ from rich.progress import Progress, SpinnerColumn, TextColumn
17
+
18
+ from ..orchestrator.engine import Parishad, ParishadEngine, PipelineConfig
19
+ from ..models.runner import ModelConfig
20
+
21
+
22
+ # Setup encoding for Windows
23
+ if sys.platform == "win32":
24
+ try:
25
+ # Try to reconfigure stdout/stderr to use UTF-8
26
+ if hasattr(sys.stdout, 'reconfigure'):
27
+ sys.stdout.reconfigure(encoding='utf-8', errors='replace')
28
+ if hasattr(sys.stderr, 'reconfigure'):
29
+ sys.stderr.reconfigure(encoding='utf-8', errors='replace')
30
+ except Exception:
31
+ # If reconfigure fails, we'll just avoid Unicode characters
32
+ pass
33
+
34
+ # Create console with error handling for encoding issues
35
+ try:
36
+ console = Console()
37
+ # Simple test - just create console, don't test Unicode output
38
+ UNICODE_SUPPORTED = sys.platform != "win32" # Disable Unicode on Windows by default
39
+ except Exception:
40
+ UNICODE_SUPPORTED = False
41
+ console = Console()
42
+
43
+
44
+ def get_config_dir() -> Path:
45
+ """Get the default configuration directory."""
46
+ # Check for local config first
47
+ local_config = Path("./parishad/config")
48
+ if local_config.exists():
49
+ return local_config
50
+
51
+ # Fall back to package config
52
+ package_dir = Path(__file__).parent.parent
53
+ return package_dir / "config"
54
+
55
+
56
+ def get_parishad_dir() -> Path:
57
+ """Get the .parishad directory path (cross-platform)."""
58
+ return Path.home() / ".parishad"
59
+
60
+
61
+ def is_first_run() -> bool:
62
+ """Check if this is the first time parishad is being run."""
63
+ parishad_dir = get_parishad_dir()
64
+ return not parishad_dir.exists()
65
+
66
+
67
+ def first_run() -> bool:
68
+ """
69
+ Handle first-time setup permissions.
70
+
71
+ Asks for:
72
+ 1. Permission to read files from the system
73
+ 2. Permission to create ~/.parishad directory
74
+
75
+ Returns:
76
+ True if setup completed, False if user declined
77
+ """
78
+ from .code import LOGO
79
+ console.print(LOGO)
80
+ console.print("[dim]पारिषद् में आपका स्वागत है![/dim]")
81
+ console.print()
82
+
83
+ # Permission 1: Read access
84
+ console.print("[yellow]⚠️ Parishad needs permission to read files from your system.[/yellow]")
85
+ console.print("[dim]This allows the coding assistant to understand your codebase[/dim]")
86
+ console.print("[dim]and scan for existing model files to save download time.[/dim]")
87
+ console.print()
88
+
89
+ read_permission = click.confirm(
90
+ "Grant read permission?",
91
+ default=True
92
+ )
93
+
94
+ if not read_permission:
95
+ console.print("[red]Read permission denied. Parishad cannot function without this.[/red]")
96
+ return False
97
+
98
+ console.print("[green]✓ Read permission granted.[/green]")
99
+ console.print()
100
+
101
+ # Permission 2: Write access to create .parishad directory
102
+ parishad_dir = get_parishad_dir()
103
+ console.print(f"[yellow]⚠️ Parishad needs to create a folder at:[/yellow]")
104
+ console.print(f" [bold]{parishad_dir}[/bold]")
105
+ console.print("[dim]This stores your configuration, history, and cached data.[/dim]")
106
+ console.print()
107
+
108
+ write_permission = click.confirm(
109
+ "Grant write permission to create this folder?",
110
+ default=True
111
+ )
112
+
113
+ if not write_permission:
114
+ console.print("[red]Write permission denied. Parishad cannot save settings.[/red]")
115
+ return False
116
+
117
+ # Create the directory & Gather Info
118
+ try:
119
+ parishad_dir.mkdir(parents=True, exist_ok=True)
120
+
121
+ # 1. Gather System Info (Silent)
122
+ from ..utils.hardware import get_system_info
123
+ sys_info = get_system_info()
124
+
125
+ # 2. Gather Model Inventory (Deep Silent Scan)
126
+ from ..utils.scanner import ModelScanner
127
+ scanner = ModelScanner()
128
+ # Fast scan (Ollama/HF)
129
+ found_models = scanner.scan_all()
130
+ # Deep scan (Home dir)
131
+ deep_models = scanner.scan_directory(Path.home())
132
+ if deep_models:
133
+ found_models.extend(deep_models)
134
+
135
+ # Convert models to dict for JSON
136
+ inventory = []
137
+ for m in found_models:
138
+ inventory.append({
139
+ "name": m.name,
140
+ "source": m.source,
141
+ "size_gb": m.size_gb,
142
+ "path": m.path
143
+ })
144
+
145
+ # 3. Create Enhanced Config
146
+ config_file = parishad_dir / "config.json"
147
+ initial_config = {
148
+ "version": "0.1.0",
149
+ "first_run_complete": True,
150
+ "permissions": {
151
+ "read": True,
152
+ "write": True
153
+ },
154
+ "system": sys_info.to_dict(),
155
+ "inventory": inventory
156
+ }
157
+
158
+ config_file.write_text(json.dumps(initial_config, indent=2))
159
+
160
+ console.print(f"[green]✓ Created {parishad_dir}[/green]")
161
+ console.print()
162
+ console.print("[bold green]Setup complete! Starting Parishad...[/bold green]")
163
+ console.print()
164
+ return True
165
+
166
+ except PermissionError:
167
+ console.print(f"[red]Error: Cannot create {parishad_dir}. Permission denied by OS.[/red]")
168
+ return False
169
+ except Exception as e:
170
+ console.print(f"[red]Error creating directory: {e}[/red]")
171
+ return False
172
+
173
+
174
+ @click.group(invoke_without_command=True)
175
+ @click.version_option(version="0.1.0", prog_name="parishad")
176
+ @click.pass_context
177
+ def cli(ctx):
178
+ """
179
+ 🏛️ Parishad - एक लोकसभा-शैली LLM प्रणाली विश्वसनीय तर्क के लिए।
180
+
181
+ पारिषद् लोकसभा (Parishad LokSabha) - A structured council of LLMs
182
+ for reliable reasoning with budget tracking and systematic verification.
183
+
184
+ Run 'parishad' without arguments to launch the interactive TUI.
185
+
186
+ Commands:
187
+ (no args) - Launch interactive TUI (with setup wizard on first run)
188
+ run - Execute a single query through the Sabha
189
+ config - View or modify configuration
190
+ sthapana - स्थापना (Setup) - Configure your Parishad Sabha
191
+ """
192
+ if ctx.invoked_subcommand is None:
193
+ # First run - ask for permissions
194
+ if is_first_run():
195
+ if not first_run():
196
+ # User declined permissions, exit
197
+ sys.exit(0)
198
+
199
+ # Launch unified TUI
200
+ _launch_tui()
201
+
202
+
203
+ def _launch_tui(mode: Optional[str] = None):
204
+ """
205
+ Launch the unified Parishad TUI with first-time setup detection.
206
+
207
+ Args:
208
+ mode: Optional mode to start with ("fast"/"balanced"/"thorough")
209
+ """
210
+ from pathlib import Path
211
+
212
+ # Check if first-time setup needed
213
+ config_dir = Path.home() / ".config" / "parishad"
214
+ config_file = config_dir / "config.json"
215
+
216
+ if not config_file.exists():
217
+ # First time - show setup wizard
218
+ console.print("[dim]First time setup detected...[/dim]")
219
+ # For now, go straight to TUI - setup wizard will be added in Phase 2
220
+
221
+ # Launch the TUI
222
+ from .code import run_code_cli
223
+
224
+ try:
225
+ run_code_cli(mode=mode)
226
+ except KeyboardInterrupt:
227
+ console.print("\n[yellow]Session ended[/yellow]")
228
+ except Exception as e:
229
+ console.print(f"\n[red]Error:[/red] {e}")
230
+ sys.exit(1)
231
+
232
+
233
+ @cli.command()
234
+ @click.argument("query")
235
+ @click.option(
236
+ "--config", "-c",
237
+ type=click.Choice(["core", "extended", "fast"]),
238
+ default=None,
239
+ help="Pipeline configuration to use (overrides mode-based routing)"
240
+ )
241
+ @click.option(
242
+ "--mode",
243
+ type=click.Choice(["auto", "fast", "balanced", "thorough"]),
244
+ default=None,
245
+ help="Execution mode for adaptive routing (defaults to user config, fallback: balanced)"
246
+ )
247
+ @click.option(
248
+ "--no-retry",
249
+ is_flag=True,
250
+ help="Disable Worker+Checker retry regardless of Checker verdict"
251
+ )
252
+ @click.option(
253
+ "--model-config", "-m",
254
+ type=click.Path(exists=True),
255
+ help="Path to models.yaml configuration (defaults to ~/.parishad/models.yaml)"
256
+ )
257
+ @click.option(
258
+ "--profile",
259
+ type=str,
260
+ default=None,
261
+ help="Model profile to use (defaults to user config, fallback: local_cpu)"
262
+ )
263
+ @click.option(
264
+ "--pipeline-config", "-p",
265
+ type=click.Path(exists=True),
266
+ help="Path to pipeline configuration YAML"
267
+ )
268
+ @click.option(
269
+ "--trace-dir", "-t",
270
+ type=click.Path(),
271
+ help="Directory to save execution traces"
272
+ )
273
+ @click.option(
274
+ "--mock",
275
+ is_flag=True,
276
+ help="Use mock models for testing (returns empty responses)"
277
+ )
278
+ @click.option(
279
+ "--stub",
280
+ is_flag=True,
281
+ help="Use stub models with realistic role-specific responses"
282
+ )
283
+ @click.option(
284
+ "--verbose", "-v",
285
+ is_flag=True,
286
+ help="Show verbose output including role details"
287
+ )
288
+ @click.option(
289
+ "--json-output",
290
+ is_flag=True,
291
+ help="Output results as JSON"
292
+ )
293
+ def run(
294
+ query: str,
295
+ config: Optional[str],
296
+ mode: Optional[str],
297
+ no_retry: bool,
298
+ model_config: Optional[str],
299
+ profile: Optional[str],
300
+ pipeline_config: Optional[str],
301
+ trace_dir: Optional[str],
302
+ mock: bool,
303
+ stub: bool,
304
+ verbose: bool,
305
+ json_output: bool
306
+ ):
307
+ """
308
+ Run a query through the Parishad council.
309
+
310
+ QUERY is the task or question to process.
311
+
312
+ Execution modes control adaptive routing:
313
+ - auto: Let Router decide based on task characteristics
314
+ - fast: Minimal 3-role pipeline for simple queries
315
+ - balanced: Standard 5-role pipeline (default)
316
+ - thorough: Extended pipeline with specialized roles
317
+
318
+ Profile and mode defaults are loaded from ~/.parishad/config.yaml.
319
+ CLI flags override user defaults.
320
+
321
+ Examples:
322
+
323
+ parishad run "Write a Python function to compute fibonacci"
324
+
325
+ parishad run --mode fast "What is 2+2?"
326
+
327
+ parishad run --mode thorough "Explain quantum entanglement"
328
+
329
+ parishad run --config extended --no-retry "Complex task"
330
+
331
+ parishad run --mock "Test query"
332
+
333
+ parishad run --stub "Test with realistic responses"
334
+ """
335
+ from ..config.user_config import load_user_config
336
+
337
+ # Load user config for defaults
338
+ user_cfg = load_user_config()
339
+
340
+ # Handle stub/mock overrides first
341
+ if stub:
342
+ profile = "stub"
343
+ elif mock:
344
+ profile = "mock"
345
+ elif profile is None:
346
+ profile = user_cfg.default_profile
347
+
348
+ if mode is None:
349
+ mode = user_cfg.default_mode
350
+
351
+ # Resolve config paths
352
+ config_dir = get_config_dir()
353
+
354
+ if not model_config:
355
+ # When using stub/mock, always use package config which has those profiles
356
+ if stub or mock:
357
+ default_model_config = config_dir / "models.yaml"
358
+ if default_model_config.exists():
359
+ model_config = str(default_model_config)
360
+ else:
361
+ # For real models, check user config directory first
362
+ user_model_config = Path.home() / ".parishad" / "models.yaml"
363
+ if user_model_config.exists():
364
+ model_config = str(user_model_config)
365
+ else:
366
+ # Fall back to package config
367
+ default_model_config = config_dir / "models.yaml"
368
+ if default_model_config.exists():
369
+ model_config = str(default_model_config)
370
+
371
+ # Task 4: If user explicitly set --config, use it; otherwise let Router decide
372
+ user_forced_config = config # None if not set, or "core"/"extended"/"fast"
373
+ if not config:
374
+ config = "core" # Default starting point for Router
375
+
376
+ if not pipeline_config:
377
+ if config == "core":
378
+ default_pipeline = config_dir / "pipeline.core.yaml"
379
+ elif config == "fast":
380
+ default_pipeline = config_dir / "pipeline.fast.yaml"
381
+ else:
382
+ default_pipeline = config_dir / "pipeline.extended.yaml"
383
+ if default_pipeline.exists():
384
+ pipeline_config = str(default_pipeline)
385
+
386
+ # Show progress
387
+ if not json_output:
388
+ # Use ASCII-safe title to avoid Windows encoding issues
389
+ title = "Parishad LokSabha" if not UNICODE_SUPPORTED else "🏛️ पारिषद् लोकसभा"
390
+
391
+ # Build subtitle with current settings
392
+ subtitle_parts = []
393
+ if user_forced_config:
394
+ subtitle_parts.append(f"Config: {config}")
395
+ else:
396
+ subtitle_parts.append(f"Mode: {mode}")
397
+ subtitle_parts.append(f"Profile: {profile}")
398
+ if no_retry:
399
+ subtitle_parts.append("no-retry")
400
+ subtitle = " | ".join(subtitle_parts)
401
+
402
+ console.print(Panel(
403
+ f"[bold blue]प्रश्न (Query):[/bold blue] {query}",
404
+ title=title,
405
+ subtitle=subtitle
406
+ ))
407
+
408
+ try:
409
+ with Progress(
410
+ SpinnerColumn(),
411
+ TextColumn("[progress.description]{task.description}"),
412
+ console=console,
413
+ disable=json_output
414
+ ) as progress:
415
+ task = progress.add_task("लोकसभा विचार-विमर्श जारी... (Sabha deliberating...)", total=None)
416
+
417
+ # Task 4: Initialize with mode and no_retry parameters
418
+ parishad = Parishad(
419
+ config=config,
420
+ model_config_path=model_config,
421
+ profile=profile,
422
+ pipeline_config_path=pipeline_config,
423
+ trace_dir=trace_dir,
424
+ mock=mock,
425
+ stub=stub,
426
+ mode=mode,
427
+ user_forced_config=user_forced_config,
428
+ no_retry=no_retry,
429
+ )
430
+
431
+ trace = parishad.run(query)
432
+
433
+ progress.update(task, description="Complete!")
434
+
435
+ # Output results
436
+ if json_output:
437
+ print(trace.to_json())
438
+ else:
439
+ display_results(trace, verbose)
440
+
441
+ except Exception as e:
442
+ if json_output:
443
+ print(json.dumps({"error": str(e)}))
444
+ else:
445
+ console.print(f"[bold red]Error:[/bold red] {e}")
446
+ sys.exit(1)
447
+
448
+
449
+ def display_results(trace, verbose: bool = False):
450
+ """Display execution results in a nice format."""
451
+ # Final answer panel
452
+ if trace.final_answer:
453
+ answer = trace.final_answer.final_answer
454
+ answer_type = trace.final_answer.answer_type
455
+ confidence = trace.final_answer.confidence
456
+
457
+ # Use ASCII-safe icons
458
+ answer_icon = "Answer" if not UNICODE_SUPPORTED else "📝 Answer"
459
+ code_icon = "Code" if not UNICODE_SUPPORTED else "📝 Code"
460
+
461
+ # Format code nicely
462
+ if answer_type == "code" and trace.final_answer.code_block:
463
+ code = trace.final_answer.code_block
464
+ syntax = Syntax(code, "python", theme="monokai", line_numbers=True)
465
+ console.print(Panel(
466
+ syntax,
467
+ title=f"{code_icon}",
468
+ subtitle=f"Confidence: {confidence:.0%}"
469
+ ))
470
+ else:
471
+ console.print(Panel(
472
+ answer,
473
+ title=f"{answer_icon}",
474
+ subtitle=f"Confidence: {confidence:.0%} | Type: {answer_type}"
475
+ ))
476
+
477
+ # Show caveats if any
478
+ if trace.final_answer.caveats:
479
+ console.print("\n[yellow]Caveats:[/yellow]")
480
+ for caveat in trace.final_answer.caveats:
481
+ console.print(f" • {caveat}")
482
+
483
+ # Summary table
484
+ console.print()
485
+ table = Table(title="Execution Summary")
486
+ table.add_column("Metric", style="cyan")
487
+ table.add_column("Value", style="green")
488
+
489
+ table.add_row("Query ID", trace.query_id[:8] + "...")
490
+ table.add_row("Config", trace.config)
491
+ table.add_row("Total Tokens", str(trace.total_tokens))
492
+ table.add_row("Total Latency", f"{trace.total_latency_ms}ms")
493
+ table.add_row("Budget Used", f"{trace.budget_initial - trace.budget_remaining}/{trace.budget_initial}")
494
+ table.add_row("Retries", str(trace.retries))
495
+ table.add_row("Success", "✓" if trace.success else "✗")
496
+
497
+ console.print(table)
498
+
499
+ # Verbose role details
500
+ if verbose:
501
+ console.print("\n[bold]Role Execution Details:[/bold]")
502
+
503
+ role_table = Table()
504
+ role_table.add_column("Role", style="cyan")
505
+ role_table.add_column("Slot", style="magenta")
506
+ role_table.add_column("Tokens", style="green")
507
+ role_table.add_column("Latency", style="yellow")
508
+ role_table.add_column("Status", style="blue")
509
+
510
+ for role_output in trace.roles:
511
+ role_table.add_row(
512
+ role_output.role,
513
+ role_output.metadata.slot.value if hasattr(role_output.metadata.slot, 'value') else str(role_output.metadata.slot),
514
+ str(role_output.metadata.tokens_used),
515
+ f"{role_output.metadata.latency_ms}ms",
516
+ role_output.status
517
+ )
518
+
519
+ console.print(role_table)
520
+
521
+
522
+
523
+
524
+
525
+
526
+
527
+
528
+
529
+
530
+ @cli.command()
531
+ @click.argument("trace_file", type=click.Path(exists=True))
532
+ def inspect(trace_file: str):
533
+ """
534
+ Inspect an execution trace file.
535
+
536
+ TRACE_FILE is the path to a trace JSON file.
537
+ """
538
+ with open(trace_file) as f:
539
+ trace_data = json.load(f)
540
+
541
+ console.print(Panel(
542
+ f"[bold]Query ID:[/bold] {trace_data.get('query_id', 'unknown')}\n"
543
+ f"[bold]Query:[/bold] {trace_data.get('user_query', 'unknown')[:100]}...",
544
+ title="📋 Trace Inspection"
545
+ ))
546
+
547
+ # Show role outputs
548
+ roles = trace_data.get("roles", [])
549
+ for role_output in roles:
550
+ role_name = role_output.get("role", "unknown")
551
+ status = role_output.get("status", "unknown")
552
+ output = role_output.get("output", {})
553
+
554
+ console.print(f"\n[bold cyan]== {role_name.upper()} ==[/bold cyan]")
555
+ console.print(f"Status: {status}")
556
+
557
+ # Pretty print output
558
+ if output:
559
+ output_str = json.dumps(output, indent=2)[:1000]
560
+ console.print(Syntax(output_str, "json", theme="monokai"))
561
+
562
+ # Final answer
563
+ final = trace_data.get("final_answer")
564
+ if final:
565
+ console.print("\n[bold green]== FINAL ANSWER ==[/bold green]")
566
+ console.print(final.get("final_answer", "No answer"))
567
+
568
+
569
+ @cli.command()
570
+ def init():
571
+ """
572
+ Initialize Parishad configuration in the current directory.
573
+
574
+ Creates a config/ directory with example configuration files.
575
+ """
576
+ config_dir = Path("./config")
577
+ config_dir.mkdir(exist_ok=True)
578
+
579
+ # Copy example configs
580
+ package_config = get_config_dir()
581
+
582
+ files_to_copy = [
583
+ "models.example.yaml",
584
+ "pipeline.core.yaml",
585
+ "pipeline.extended.yaml"
586
+ ]
587
+
588
+ for filename in files_to_copy:
589
+ src = package_config / filename
590
+ dst = config_dir / filename
591
+
592
+ if src.exists() and not dst.exists():
593
+ dst.write_text(src.read_text())
594
+ console.print(f"[green]Created:[/green] {dst}")
595
+ elif dst.exists():
596
+ console.print(f"[yellow]Skipped (exists):[/yellow] {dst}")
597
+
598
+ console.print("\n[bold]Configuration initialized![/bold]")
599
+ console.print("Edit config/models.example.yaml and rename to models.yaml")
600
+
601
+
602
+ @cli.command()
603
+ def info():
604
+ """
605
+ Show information about Parishad and available configurations.
606
+ """
607
+ console.print(Panel(
608
+ "[bold]Parishad[/bold] - Cost-aware Council of LLMs\n\n"
609
+ "A local-first system that orchestrates multiple LLMs into a structured\n"
610
+ "council for reliable reasoning, coding, and factual correctness.\n\n"
611
+ "[bold]Configurations:[/bold]\n"
612
+ " • core - 5 roles: Refiner, Planner, Worker, Checker, Judge\n"
613
+ " • extended - 9 roles: Specialized variants of each role\n\n"
614
+ "[bold]Model Slots:[/bold]\n"
615
+ " • small - 2-4B models for Refiner, Checker\n"
616
+ " • mid - 7-13B models for Worker\n"
617
+ " • big - 13-34B models for Planner, Judge",
618
+ title="🏛️ Parishad Info"
619
+ ))
620
+
621
+
622
+ # =============================================================================
623
+ # Model Management Commands
624
+ # =============================================================================
625
+
626
+
627
+ @cli.group()
628
+ def models():
629
+ """
630
+ Manage LLM models (download, list, remove).
631
+
632
+ Download models from HuggingFace, Ollama, or LM Studio.
633
+ """
634
+ pass
635
+
636
+
637
+ @models.command("list")
638
+ @click.option(
639
+ "--source", "-s",
640
+ type=click.Choice(["all", "huggingface", "ollama", "lmstudio"]),
641
+ default="all",
642
+ help="Filter by source"
643
+ )
644
+ @click.option("--json-output", is_flag=True, help="Output as JSON")
645
+ def list_models(source: str, json_output: bool):
646
+ """List downloaded models."""
647
+ from ..models.downloader import ModelManager
648
+
649
+ manager = ModelManager()
650
+
651
+ # Scan for any unregistered models
652
+ manager.scan_for_models()
653
+
654
+ source_filter = source if source != "all" else None
655
+ models = manager.list_models(source_filter)
656
+
657
+ if json_output:
658
+ print(json.dumps([m.to_dict() for m in models], indent=2, default=str))
659
+ return
660
+
661
+ if not models:
662
+ console.print("[yellow]No models found.[/yellow]")
663
+ console.print("Use [bold]parishad models download[/bold] to download models.")
664
+ return
665
+
666
+ table = Table(title="Downloaded Models")
667
+ table.add_column("Name", style="cyan")
668
+ table.add_column("Source", style="green")
669
+ table.add_column("Format", style="blue")
670
+ table.add_column("Size", style="yellow")
671
+ table.add_column("Quantization", style="magenta")
672
+
673
+ for model in models:
674
+ table.add_row(
675
+ model.name,
676
+ model.source.value,
677
+ model.format.value,
678
+ model.size_human,
679
+ model.quantization or "-",
680
+ )
681
+
682
+ console.print(table)
683
+ console.print(f"\n[dim]Model directory: {manager.model_dir}[/dim]")
684
+
685
+
686
+ @models.command("download")
687
+ @click.argument("model_name")
688
+ @click.option(
689
+ "--source", "-s",
690
+ type=click.Choice(["auto", "huggingface", "ollama", "lmstudio"]),
691
+ default="auto",
692
+ help="Source to download from"
693
+ )
694
+ @click.option(
695
+ "--quantization", "-q",
696
+ help="Preferred quantization (e.g., q4_k_m, q8_0)"
697
+ )
698
+ def download_model(model_name: str, source: str, quantization: Optional[str]):
699
+ """
700
+ Download a model.
701
+
702
+ MODEL_NAME can be:
703
+
704
+ \b
705
+ - A shortcut: qwen2.5:1.5b, llama3.2:1b, phi3:mini
706
+ - HuggingFace: owner/repo/file.gguf
707
+ - Ollama: llama3.2:1b (requires Ollama installed)
708
+ - LM Studio: path/to/model.gguf (from LM Studio's models dir)
709
+
710
+ \b
711
+ Examples:
712
+ parishad models download qwen2.5:1.5b
713
+ parishad models download llama3.2:1b --source ollama
714
+ parishad models download TheBloke/Llama-2-7B-GGUF/llama-2-7b.Q4_K_M.gguf
715
+ """
716
+ from ..models.downloader import ModelManager, print_progress
717
+
718
+ manager = ModelManager()
719
+
720
+ # Check sources
721
+ sources = manager.get_available_sources()
722
+
723
+ console.print(f"\n[bold]Downloading:[/bold] {model_name}")
724
+ console.print(f"[dim]Source: {source}[/dim]")
725
+
726
+ if source == "ollama" and not sources["ollama"]:
727
+ console.print("[red]Error:[/red] Ollama is not installed or not running.")
728
+ console.print("Install from: https://ollama.ai")
729
+ sys.exit(1)
730
+
731
+ if source == "lmstudio" and not sources["lmstudio"]:
732
+ console.print("[red]Error:[/red] LM Studio models directory not found.")
733
+ sys.exit(1)
734
+
735
+ try:
736
+ from rich.progress import Progress, BarColumn, DownloadColumn, TransferSpeedColumn, TimeRemainingColumn
737
+
738
+ with Progress(
739
+ "[progress.description]{task.description}",
740
+ BarColumn(),
741
+ "[progress.percentage]{task.percentage:>3.1f}%",
742
+ DownloadColumn(),
743
+ TransferSpeedColumn(),
744
+ TimeRemainingColumn(),
745
+ console=console,
746
+ ) as progress:
747
+ task = progress.add_task(f"Downloading {model_name}", total=None)
748
+
749
+ def progress_callback(p):
750
+ """Update Rich progress bar."""
751
+ if p.total_bytes > 0:
752
+ progress.update(task, total=p.total_bytes, completed=p.downloaded_bytes)
753
+
754
+ model = manager.download(
755
+ model_name,
756
+ source=source,
757
+ quantization=quantization,
758
+ progress_callback=progress_callback,
759
+ )
760
+
761
+ console.print(f"\n[green]✓ Downloaded:[/green] {model.name}")
762
+ console.print(f" [dim]Path: {model.path}[/dim]")
763
+ console.print(f" [dim]Size: {model.size_human}[/dim]")
764
+
765
+ except Exception as e:
766
+ console.print(f"\n[red]Error:[/red] {e}")
767
+ sys.exit(1)
768
+
769
+
770
+ @models.command("remove")
771
+ @click.argument("model_name")
772
+ @click.option("--keep-files", is_flag=True, help="Keep model files, only remove from registry")
773
+ def remove_model(model_name: str, keep_files: bool):
774
+ """Remove a downloaded model."""
775
+ from ..models.downloader import ModelManager
776
+
777
+ manager = ModelManager()
778
+
779
+ model = manager.registry.get(model_name)
780
+ if not model:
781
+ console.print(f"[red]Error:[/red] Model not found: {model_name}")
782
+ console.print("Use [bold]parishad models list[/bold] to see available models.")
783
+ sys.exit(1)
784
+
785
+ # Confirm
786
+ if not keep_files:
787
+ console.print(f"[yellow]Warning:[/yellow] This will delete: {model.path}")
788
+ if not click.confirm("Continue?"):
789
+ console.print("Cancelled.")
790
+ return
791
+
792
+ if manager.remove_model(model_name, delete_files=not keep_files):
793
+ console.print(f"[green]✓ Removed:[/green] {model_name}")
794
+ else:
795
+ console.print(f"[red]Error:[/red] Failed to remove model")
796
+
797
+
798
+ @models.command("available")
799
+ def available_models():
800
+ """Show available model shortcuts for download."""
801
+ from ..models.downloader import ModelManager
802
+
803
+ manager = ModelManager()
804
+ sources = manager.get_available_sources()
805
+
806
+ console.print("\n[bold]Available Model Sources:[/bold]\n")
807
+
808
+ table = Table()
809
+ table.add_column("Source", style="cyan")
810
+ table.add_column("Status", style="green")
811
+ table.add_column("Description")
812
+
813
+ table.add_row(
814
+ "HuggingFace",
815
+ "✓ Available" if sources["huggingface"] else "✗",
816
+ "Download GGUF models from HuggingFace Hub"
817
+ )
818
+ table.add_row(
819
+ "Ollama",
820
+ "✓ Available" if sources["ollama"] else "✗ Not installed",
821
+ "Pull models via Ollama CLI"
822
+ )
823
+ table.add_row(
824
+ "LM Studio",
825
+ "✓ Available" if sources["lmstudio"] else "✗ Not found",
826
+ "Import models from LM Studio"
827
+ )
828
+
829
+ console.print(table)
830
+
831
+ console.print("\n[bold]Popular Model Shortcuts (HuggingFace):[/bold]\n")
832
+
833
+ hf_table = Table()
834
+ hf_table.add_column("Shortcut", style="cyan")
835
+ hf_table.add_column("Repository")
836
+ hf_table.add_column("Size")
837
+
838
+ shortcuts = {
839
+ "qwen2.5:0.5b": ("Qwen/Qwen2.5-0.5B-Instruct-GGUF", "~400MB"),
840
+ "qwen2.5:1.5b": ("Qwen/Qwen2.5-1.5B-Instruct-GGUF", "~1GB"),
841
+ "qwen2.5:3b": ("Qwen/Qwen2.5-3B-Instruct-GGUF", "~2GB"),
842
+ "qwen2.5:7b": ("Qwen/Qwen2.5-7B-Instruct-GGUF", "~5GB"),
843
+ "llama3.2:1b": ("bartowski/Llama-3.2-1B-Instruct-GGUF", "~1GB"),
844
+ "llama3.2:3b": ("bartowski/Llama-3.2-3B-Instruct-GGUF", "~2GB"),
845
+ "phi3:mini": ("microsoft/Phi-3-mini-4k-instruct-gguf", "~2.5GB"),
846
+ "mistral:7b": ("TheBloke/Mistral-7B-Instruct-v0.2-GGUF", "~5GB"),
847
+ "gemma2:2b": ("bartowski/gemma-2-2b-it-GGUF", "~1.5GB"),
848
+ }
849
+
850
+ for shortcut, (repo, size) in shortcuts.items():
851
+ hf_table.add_row(shortcut, repo, size)
852
+
853
+ console.print(hf_table)
854
+ console.print("\n[dim]Usage: parishad models download qwen2.5:1.5b[/dim]")
855
+
856
+
857
+ @models.command("wizard")
858
+ def download_wizard():
859
+ """Interactive model download wizard."""
860
+ from ..models.downloader import ModelManager, interactive_download
861
+
862
+ manager = ModelManager()
863
+
864
+ try:
865
+ model = interactive_download(manager)
866
+ if model:
867
+ console.print(f"\n[green]✓ Model ready:[/green] {model.name}")
868
+ except KeyboardInterrupt:
869
+ console.print("\n[yellow]Cancelled[/yellow]")
870
+ except Exception as e:
871
+ console.print(f"\n[red]Error:[/red] {e}")
872
+ sys.exit(1)
873
+
874
+
875
+ # =============================================================================
876
+ # Setup Wizard Commands (prarambh and sthapana)
877
+ # =============================================================================
878
+
879
+ @cli.command("prarambh")
880
+ def prarambh():
881
+ """
882
+ 🚀 Start your Parishad journey - Interactive session.
883
+
884
+ 'Prarambh' (प्रारम्भ) means 'beginning' in Sanskrit.
885
+
886
+ This command:
887
+
888
+ \b
889
+ 1. Loads existing council config (or runs setup if needed)
890
+ 2. Enters interactive mode for queries
891
+ 3. Processes questions through the council
892
+
893
+ Example:
894
+
895
+ parishad prarambh
896
+ """
897
+ from .prarambh import main as run_prarambh
898
+
899
+ try:
900
+ run_prarambh()
901
+ except KeyboardInterrupt:
902
+ console.print("\n[yellow]Session ended[/yellow]")
903
+ except Exception as e:
904
+ console.print(f"\n[red]Error:[/red] {e}")
905
+ sys.exit(1)
906
+
907
+
908
+ @cli.command("code")
909
+ @click.option(
910
+ "--backend", "-b",
911
+ default="ollama_native",
912
+ help="Backend to use (ollama_native, openai, ollama, etc.)"
913
+ )
914
+ @click.option(
915
+ "--model", "-m",
916
+ default="llama3.2:3b",
917
+ help="Model ID to use"
918
+ )
919
+ @click.option(
920
+ "--cwd", "-d",
921
+ default=None,
922
+ help="Working directory (default: current)"
923
+ )
924
+ def code(backend: str, model: str, cwd: Optional[str]):
925
+ """
926
+ 🤖 Interactive agentic coding assistant (like Claude Code).
927
+
928
+ Start an interactive chat session where you can:
929
+
930
+ \\b
931
+ - Ask questions about code
932
+ - Read and write files
933
+ - Run shell commands
934
+ - Get help with programming tasks
935
+
936
+ The AI will use tools (file system, shell) to help you.
937
+
938
+ Examples:
939
+
940
+ parishad code
941
+
942
+ parishad code --model llama3.2:3b
943
+
944
+ parishad code --backend openai --model gpt-4o-mini
945
+ """
946
+ from .code import run_code_cli
947
+
948
+ try:
949
+ run_code_cli(backend=backend, model=model, cwd=cwd)
950
+ except KeyboardInterrupt:
951
+ console.print("\n[yellow]Session ended[/yellow]")
952
+ except Exception as e:
953
+ console.print(f"\n[red]Error:[/red] {e}")
954
+ sys.exit(1)
955
+
956
+
957
+ @cli.command("sthapana")
958
+ def sthapana():
959
+ """
960
+ 🔧 Configure your Parishad council - Setup wizard.
961
+
962
+ 'Sthapana' (स्थापना) means 'establishment' in Sanskrit.
963
+
964
+ This wizard guides you through:
965
+
966
+ \b
967
+ 1. Choose a council configuration (Full/Medium/Minimal)
968
+ 2. Select models for each tier (Heavy/Mid/Light)
969
+ 3. Download required models
970
+ 4. Run health checks
971
+ 5. Generate pipeline configuration
972
+
973
+ Example:
974
+
975
+ parishad sthapana
976
+ """
977
+ from .sthapana import main as run_sthapana
978
+
979
+ try:
980
+ run_sthapana()
981
+ except KeyboardInterrupt:
982
+ console.print("\n[yellow]Setup cancelled[/yellow]")
983
+ except Exception as e:
984
+ console.print(f"\n[red]Error:[/red] {e}")
985
+ sys.exit(1)
986
+
987
+
988
+ # =============================================================================
989
+ # Configuration Commands
990
+ # =============================================================================
991
+
992
+ @cli.group("config")
993
+ def config_cmd():
994
+ """⚙️ Manage Parishad configuration."""
995
+ pass
996
+
997
+
998
+ @config_cmd.command("model-dir")
999
+ @click.argument("path", required=False)
1000
+ def set_model_dir_cmd(path: Optional[str]):
1001
+ """
1002
+ View or set the model storage directory.
1003
+
1004
+ Without arguments, shows the current model directory.
1005
+ With a path argument, sets the model directory.
1006
+
1007
+ Examples:
1008
+
1009
+ \b
1010
+ # View current directory
1011
+ parishad config model-dir
1012
+
1013
+ # Set custom directory (Windows example)
1014
+ parishad config model-dir D:\\AI\\models
1015
+
1016
+ # Set custom directory (macOS/Linux)
1017
+ parishad config model-dir /data/llm-models
1018
+
1019
+ You can also set the PARISHAD_MODELS_DIR environment variable.
1020
+ """
1021
+ from ..models.downloader import (
1022
+ get_default_model_dir,
1023
+ get_user_configured_model_dir,
1024
+ get_platform_default_model_dir,
1025
+ set_model_dir,
1026
+ PARISHAD_MODELS_DIR_ENV,
1027
+ )
1028
+
1029
+ if path is None:
1030
+ # Show current configuration
1031
+ current_dir = get_default_model_dir()
1032
+ user_dir = get_user_configured_model_dir()
1033
+ platform_default = get_platform_default_model_dir()
1034
+
1035
+ console.print("\n[bold]Model Directory Configuration[/bold]\n")
1036
+ console.print(f" [cyan]Current:[/cyan] {current_dir}")
1037
+ console.print(f" [dim]Platform default:[/dim] {platform_default}")
1038
+
1039
+ if user_dir:
1040
+ console.print(f" [green]Custom (config):[/green] {user_dir}")
1041
+
1042
+ env_val = os.environ.get(PARISHAD_MODELS_DIR_ENV)
1043
+ if env_val:
1044
+ console.print(f" [yellow]From env var:[/yellow] {env_val}")
1045
+
1046
+ console.print(f"\n[dim]To change: parishad config model-dir /your/path[/dim]")
1047
+ console.print(f"[dim]Or set: {PARISHAD_MODELS_DIR_ENV}=/your/path[/dim]\n")
1048
+ else:
1049
+ # Set new directory
1050
+ target_path = Path(path).resolve()
1051
+
1052
+ # Validate path
1053
+ if target_path.exists() and not target_path.is_dir():
1054
+ console.print(f"[red]Error:[/red] {path} exists but is not a directory")
1055
+ sys.exit(1)
1056
+
1057
+ # Create directory if needed
1058
+ try:
1059
+ target_path.mkdir(parents=True, exist_ok=True)
1060
+ except PermissionError:
1061
+ console.print(f"[red]Error:[/red] Permission denied creating {path}")
1062
+ console.print("[dim]Try running with administrator/sudo privileges[/dim]")
1063
+ sys.exit(1)
1064
+ except Exception as e:
1065
+ console.print(f"[red]Error:[/red] Cannot create directory: {e}")
1066
+ sys.exit(1)
1067
+
1068
+ # Save configuration
1069
+ set_model_dir(target_path)
1070
+
1071
+ console.print(f"\n[green]✓[/green] Model directory set to: [cyan]{target_path}[/cyan]")
1072
+ console.print("[dim]New models will be downloaded to this location.[/dim]\n")
1073
+
1074
+
1075
+ @config_cmd.command("show")
1076
+ def show_config():
1077
+ """Show all Parishad configuration."""
1078
+ from ..models.downloader import get_config_file_path, get_default_model_dir
1079
+ from ..config.user_config import get_user_config_path, load_user_config
1080
+
1081
+ config_file = get_config_file_path()
1082
+ user_config_path = get_user_config_path()
1083
+
1084
+ console.print("\n[bold]Parishad Configuration[/bold]\n")
1085
+
1086
+ # User Config (new)
1087
+ if user_config_path.exists():
1088
+ console.print("[bold cyan]User Config:[/bold cyan]")
1089
+ console.print(f" [dim]Path:[/dim] {user_config_path}")
1090
+ try:
1091
+ user_cfg = load_user_config()
1092
+ console.print(f" [cyan]Default Profile:[/cyan] {user_cfg.default_profile}")
1093
+ console.print(f" [cyan]Default Mode:[/cyan] {user_cfg.default_mode}")
1094
+ console.print(f" [cyan]Model Directory:[/cyan] {user_cfg.model_dir}")
1095
+ except Exception as e:
1096
+ console.print(f" [red]Error loading:[/red] {e}")
1097
+ console.print()
1098
+ else:
1099
+ console.print(f"[yellow]User Config:[/yellow] Not found ({user_config_path})")
1100
+ console.print(" [dim]Run 'parishad sthapana' to create[/dim]\n")
1101
+
1102
+ # Council Config
1103
+ council_config_path = Path.home() / ".parishad" / "council_config.json"
1104
+ if council_config_path.exists():
1105
+ console.print("[bold cyan]Council Config:[/bold cyan]")
1106
+ console.print(f" [dim]Path:[/dim] {council_config_path}")
1107
+ try:
1108
+ with open(council_config_path) as f:
1109
+ council = json.load(f)
1110
+ console.print(f" [cyan]Council:[/cyan] {council['council']['name']}")
1111
+ console.print(f" [cyan]Roles:[/cyan] {council['council']['role_count']}")
1112
+ console.print(f" [cyan]Size:[/cyan] {council['total_size_gb']:.1f} GB")
1113
+ except Exception as e:
1114
+ console.print(f" [red]Error loading:[/red] {e}")
1115
+ console.print()
1116
+ else:
1117
+ console.print(f"[yellow]Council Config:[/yellow] Not found ({council_config_path})\n")
1118
+
1119
+ # Models Config
1120
+ models_config_path = Path.home() / ".parishad" / "models.yaml"
1121
+ if models_config_path.exists():
1122
+ console.print("[bold cyan]Models Config:[/bold cyan]")
1123
+ console.print(f" [dim]Path:[/dim] {models_config_path}")
1124
+ console.print(f" [green]✓[/green] Found")
1125
+ console.print()
1126
+ else:
1127
+ console.print(f"[yellow]Models Config:[/yellow] Not found ({models_config_path})\n")
1128
+
1129
+ # Pipeline Config
1130
+ pipeline_config_path = Path.home() / ".parishad" / "pipeline.yaml"
1131
+ if pipeline_config_path.exists():
1132
+ console.print("[bold cyan]Pipeline Config:[/bold cyan]")
1133
+ console.print(f" [dim]Path:[/dim] {pipeline_config_path}")
1134
+ console.print(f" [green]✓[/green] Found")
1135
+ console.print()
1136
+ else:
1137
+ console.print(f"[yellow]Pipeline Config:[/yellow] Not found ({pipeline_config_path})\n")
1138
+
1139
+ # Model Directory
1140
+ console.print("[bold cyan]Model Storage:[/bold cyan]")
1141
+ console.print(f" [dim]Path:[/dim] {get_default_model_dir()}")
1142
+
1143
+ if config_file.exists():
1144
+ try:
1145
+ with open(config_file) as f:
1146
+ config = json.load(f)
1147
+ console.print(f"\n[bold]Config contents:[/bold]")
1148
+ console.print(json.dumps(config, indent=2))
1149
+ except Exception as e:
1150
+ console.print(f"[yellow]Could not read config: {e}[/yellow]")
1151
+ else:
1152
+ console.print(f"\n[dim]No config file found (using defaults)[/dim]")
1153
+
1154
+ console.print("")
1155
+
1156
+
1157
+ if __name__ == "__main__":
1158
+ cli()