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/tools/base.py ADDED
@@ -0,0 +1,57 @@
1
+ """
2
+ Base classes and interfaces for Parishad tools.
3
+ Tools enable the agent to interact with the external world (Perception and Action).
4
+ """
5
+
6
+ from abc import ABC, abstractmethod
7
+ from typing import Any, Dict, Optional, Type
8
+ from pydantic import BaseModel, Field
9
+
10
+
11
+ class ToolResult(BaseModel):
12
+ """Standardized output from a tool execution."""
13
+ success: bool
14
+ data: Any # The actual result (text, json, file path, etc.)
15
+ error: Optional[str] = None
16
+ metadata: Dict[str, Any] = Field(default_factory=dict)
17
+
18
+
19
+ class BaseTool(ABC):
20
+ """Abstract base class for all tools."""
21
+
22
+ name: str = "base_tool"
23
+ description: str = "Base tool description"
24
+
25
+ def __init__(self, **kwargs):
26
+ """Initialize the tool."""
27
+ pass
28
+
29
+ @property
30
+ def schema(self) -> Dict[str, Any]:
31
+ """Return the JSON schema for this tool's input."""
32
+ # By default, can infer from the `run` method type hints if using Pydantic V2
33
+ # For now, subclasses should define this explicitly or we use a decorator helper
34
+ return {
35
+ "name": self.name,
36
+ "description": self.description,
37
+ "parameters": {
38
+ "type": "object",
39
+ "properties": {},
40
+ }
41
+ }
42
+
43
+ @abstractmethod
44
+ def run(self, **kwargs) -> ToolResult:
45
+ """Execute the tool."""
46
+ pass
47
+
48
+ def __call__(self, **kwargs) -> ToolResult:
49
+ """Syntactic sugar for running the tool."""
50
+ try:
51
+ return self.run(**kwargs)
52
+ except Exception as e:
53
+ return ToolResult(
54
+ success=False,
55
+ data=None,
56
+ error=str(e)
57
+ )
parishad/tools/fs.py ADDED
@@ -0,0 +1,110 @@
1
+ """
2
+ File System Action Tool.
3
+ Allows the agent to read, write, and navigate the file system.
4
+ """
5
+
6
+ import os
7
+ from pathlib import Path
8
+ from typing import Any, Dict, Optional, Literal
9
+
10
+ from .base import BaseTool, ToolResult
11
+
12
+
13
+ class FileSystemTool(BaseTool):
14
+ """
15
+ Tool for interacting with the file system.
16
+ Capabilities: read, write, list.
17
+ """
18
+
19
+ name = "file_system"
20
+ description = "Read, write, and list files in the file system. Use this to modify code or read documentation."
21
+
22
+ def __init__(self, working_directory: Optional[str] = None):
23
+ """
24
+ Initialize FileSystemTool.
25
+
26
+ Args:
27
+ working_directory: Root directory to limit operations to (optional).
28
+ If NOT set, operations are unrestricted (use with caution).
29
+ """
30
+ super().__init__()
31
+ self.working_directory = Path(working_directory).resolve() if working_directory else Path.cwd()
32
+
33
+ @property
34
+ def schema(self) -> Dict[str, Any]:
35
+ return {
36
+ "name": self.name,
37
+ "description": self.description,
38
+ "parameters": {
39
+ "type": "object",
40
+ "properties": {
41
+ "operation": {
42
+ "type": "string",
43
+ "enum": ["read", "write", "list"],
44
+ "description": "Operation to perform"
45
+ },
46
+ "path": {
47
+ "type": "string",
48
+ "description": "Path to the file or directory"
49
+ },
50
+ "content": {
51
+ "type": "string",
52
+ "description": "Content to write (for 'write' operation only)"
53
+ }
54
+ },
55
+ "required": ["operation", "path"]
56
+ }
57
+ }
58
+
59
+ def run(self, operation: Literal["read", "write", "list"], path: str, content: Optional[str] = None) -> ToolResult:
60
+ """Execute file system operation."""
61
+ try:
62
+ target_path = Path(path).resolve()
63
+
64
+ # Basic security check: ensure path is relative to working directory if strict mode desired
65
+ # For now, we allow absolute paths to be powerful agents, but we log usage.
66
+
67
+ if operation == "read":
68
+ if not target_path.exists():
69
+ return ToolResult(success=False, data=None, error=f"File not found: {path}")
70
+ if not target_path.is_file():
71
+ return ToolResult(success=False, data=None, error=f"Not a file: {path}")
72
+
73
+ try:
74
+ text = target_path.read_text(encoding="utf-8")
75
+ return ToolResult(success=True, data=text)
76
+ except UnicodeDecodeError:
77
+ return ToolResult(success=False, data=None, error="Binary file reading not supported yet.")
78
+
79
+ elif operation == "write":
80
+ if content is None:
81
+ return ToolResult(success=False, data=None, error="Content required for write operation.")
82
+
83
+ # Ensure directory exists
84
+ target_path.parent.mkdir(parents=True, exist_ok=True)
85
+ target_path.write_text(content, encoding="utf-8")
86
+ return ToolResult(success=True, data=f"Successfully wrote {len(content)} bytes to {path}")
87
+
88
+ elif operation == "list":
89
+ if not target_path.exists():
90
+ return ToolResult(success=False, data=None, error=f"Directory not found: {path}")
91
+
92
+ items = []
93
+ if target_path.is_dir():
94
+ for item in target_path.iterdir():
95
+ kind = "DIR" if item.is_dir() else "FILE"
96
+ items.append(f"[{kind}] {item.name}")
97
+ else:
98
+ return ToolResult(success=False, data=None, error=f"Not a directory: {path}")
99
+
100
+ return ToolResult(success=True, data="\n".join(sorted(items)))
101
+
102
+ else:
103
+ return ToolResult(success=False, data=None, error=f"Unknown operation: {operation}")
104
+
105
+ except Exception as e:
106
+ return ToolResult(
107
+ success=False,
108
+ data=None,
109
+ error=f"FileSystem error ({operation}): {str(e)}"
110
+ )
@@ -0,0 +1,96 @@
1
+ """
2
+ Unified Perception Tool (The Eyes & Ears).
3
+ Uses MarkItDown to convert Documents, Images, and Audio to text.
4
+ Lightweight, no heavy model downloads.
5
+ """
6
+
7
+ from pathlib import Path
8
+ from typing import Any, Dict, Optional
9
+
10
+ from .base import BaseTool, ToolResult
11
+
12
+
13
+ class PerceptionTool(BaseTool):
14
+ """
15
+ Unified tool for perceiving the world (Docs, Images, Audio).
16
+ Powered by Microsoft MarkItDown.
17
+ """
18
+
19
+ name = "perception"
20
+ description = "Convert files (PDF, Docx, Images, Audio) into text. Use for reading docs or seeing images."
21
+
22
+ def __init__(self):
23
+ super().__init__()
24
+ self._markitdown = None
25
+
26
+ def _get_markitdown(self):
27
+ """Lazy import."""
28
+ if self._markitdown is None:
29
+ try:
30
+ from markitdown import MarkItDown
31
+ # Initialize without LLM client by default to keep it local/free.
32
+ # Image description will be basic (OCR/Metadata) unless LLM is provided later.
33
+ self._markitdown = MarkItDown()
34
+ except ImportError:
35
+ raise ImportError(
36
+ "markitdown dependency is missing. "
37
+ "Install with: pip install parishad[perception]"
38
+ )
39
+ return self._markitdown
40
+
41
+ @property
42
+ def schema(self) -> Dict[str, Any]:
43
+ return {
44
+ "name": self.name,
45
+ "description": self.description,
46
+ "parameters": {
47
+ "type": "object",
48
+ "properties": {
49
+ "file_path": {
50
+ "type": "string",
51
+ "description": "Absolute path to the file (pdf, docx, jpg, mp3, etc.)"
52
+ }
53
+ },
54
+ "required": ["file_path"]
55
+ }
56
+ }
57
+
58
+ def run(self, file_path: str) -> ToolResult:
59
+ """Convert file to text."""
60
+ try:
61
+ path = Path(file_path)
62
+ if not path.exists():
63
+ return ToolResult(
64
+ success=False,
65
+ data=None,
66
+ error=f"File not found: {file_path}"
67
+ )
68
+
69
+ md = self._get_markitdown()
70
+
71
+ # Convert
72
+ result = md.convert(str(path))
73
+
74
+ if result and hasattr(result, "text_content"):
75
+ return ToolResult(
76
+ success=True,
77
+ data=result.text_content,
78
+ metadata={
79
+ "source": str(path),
80
+ "format": path.suffix,
81
+ "title": result.title if hasattr(result, "title") else None
82
+ }
83
+ )
84
+ else:
85
+ return ToolResult(
86
+ success=False,
87
+ data=None,
88
+ error="Conversion returned empty result."
89
+ )
90
+
91
+ except Exception as e:
92
+ return ToolResult(
93
+ success=False,
94
+ data=None,
95
+ error=f"Perception failed: {str(e)}"
96
+ )
@@ -0,0 +1,74 @@
1
+ """
2
+ Retrieval Tool (Memory Access).
3
+ Allows agents to query the Vector Store (RDMA).
4
+ """
5
+
6
+ from typing import Any, Dict
7
+
8
+ from .base import BaseTool, ToolResult
9
+ from ..memory import VectorStore
10
+
11
+ class RetrievalTool(BaseTool):
12
+ """
13
+ Tool for searching the Agent's Long-Term Memory (Vector Store).
14
+ """
15
+
16
+ name = "memory_retrieval"
17
+ description = "Search the council's long-term memory (codebase, docs) for relevant context."
18
+
19
+ def __init__(self, collection_name: str = "parishad_memory"):
20
+ super().__init__()
21
+ # Initialize store
22
+ # Note: We share the persist_dir convention
23
+ self.store = VectorStore(collection_name=collection_name)
24
+
25
+ @property
26
+ def schema(self) -> Dict[str, Any]:
27
+ return {
28
+ "name": self.name,
29
+ "description": self.description,
30
+ "parameters": {
31
+ "type": "object",
32
+ "properties": {
33
+ "query": {
34
+ "type": "string",
35
+ "description": "The search query (e.g. 'How does the ShellTool work?')"
36
+ },
37
+ "limit": {
38
+ "type": "integer",
39
+ "description": "Number of results to return (default: 5)"
40
+ }
41
+ },
42
+ "required": ["query"]
43
+ }
44
+ }
45
+
46
+ def run(self, query: str, limit: int = 5) -> ToolResult:
47
+ """Execute retrieval."""
48
+ try:
49
+ results = self.store.query(query, n_results=limit)
50
+
51
+ if not results:
52
+ return ToolResult(success=True, data="No relevant memories found.", metadata={"count": 0})
53
+
54
+ # Format results into a readable string
55
+ formatted = []
56
+ for i, res in enumerate(results):
57
+ content = res['content']
58
+ meta = res['metadata']
59
+ dist = res['distance']
60
+ source = meta.get('source', 'unknown')
61
+ formatted.append(f"[{i+1}] (Source: {source}, Dist: {dist:.3f})\n{content}\n")
62
+
63
+ return ToolResult(
64
+ success=True,
65
+ data="\n".join(formatted),
66
+ metadata={"count": len(results)}
67
+ )
68
+
69
+ except Exception as e:
70
+ return ToolResult(
71
+ success=False,
72
+ data=None,
73
+ error=f"Retrieval failed: {str(e)}"
74
+ )
@@ -0,0 +1,103 @@
1
+ """
2
+ Shell Execution Tool.
3
+ Allows the agent to execute system commands.
4
+ """
5
+
6
+ import subprocess
7
+ import shlex
8
+ import os
9
+ from typing import Any, Dict, Optional
10
+
11
+ from .base import BaseTool, ToolResult
12
+
13
+
14
+ class ShellTool(BaseTool):
15
+ """
16
+ Tool for executing shell commands.
17
+ Capabilities: run_command.
18
+ """
19
+
20
+ name = "shell"
21
+ description = "Execute shell commands in the terminal."
22
+
23
+ def __init__(self, timeout: int = 60, safe_mode: bool = False):
24
+ """
25
+ Initialize ShellTool.
26
+
27
+ Args:
28
+ timeout: Maximum execution time in seconds.
29
+ safe_mode: If True, disallows potentially destructive commands (mock implementation).
30
+ """
31
+ super().__init__()
32
+ self.timeout = timeout
33
+ self.safe_mode = safe_mode
34
+
35
+ @property
36
+ def schema(self) -> Dict[str, Any]:
37
+ return {
38
+ "name": self.name,
39
+ "description": self.description,
40
+ "parameters": {
41
+ "type": "object",
42
+ "properties": {
43
+ "command": {
44
+ "type": "string",
45
+ "description": "Shell command to execute"
46
+ },
47
+ "cwd": {
48
+ "type": "string",
49
+ "description": "Directory to execute command in (optional)"
50
+ }
51
+ },
52
+ "required": ["command"]
53
+ }
54
+ }
55
+
56
+ def run(self, command: str, cwd: Optional[str] = None) -> ToolResult:
57
+ """Run a shell command."""
58
+ try:
59
+ if self.safe_mode:
60
+ # Basic blacklist (very naive, purely illustrative)
61
+ forbidden = ["rm -rf /", ":(){ :|:& };:"]
62
+ if any(f in command for f in forbidden):
63
+ return ToolResult(success=False, data=None, error="Command blocked by safe mode.")
64
+
65
+ # Resolve cwd
66
+ working_dir = os.path.abspath(cwd) if cwd else os.getcwd()
67
+
68
+ # Execute
69
+ # Using shell=True is dangerous but necessary for complex commands (pipes, etc.) often used by agents
70
+ result = subprocess.run(
71
+ command,
72
+ shell=True,
73
+ cwd=working_dir,
74
+ capture_output=True,
75
+ text=True,
76
+ timeout=self.timeout
77
+ )
78
+
79
+ output = result.stdout
80
+ if result.stderr:
81
+ output += f"\nSTDERR:\n{result.stderr}"
82
+
83
+ if result.returncode == 0:
84
+ return ToolResult(success=True, data=output.strip())
85
+ else:
86
+ return ToolResult(
87
+ success=False,
88
+ data=output.strip(),
89
+ error=f"Command failed with exit code {result.returncode}"
90
+ )
91
+
92
+ except subprocess.TimeoutExpired:
93
+ return ToolResult(
94
+ success=False,
95
+ data=None,
96
+ error=f"Command timed out after {self.timeout}s"
97
+ )
98
+ except Exception as e:
99
+ return ToolResult(
100
+ success=False,
101
+ data=None,
102
+ error=f"Shell execution failed: {str(e)}"
103
+ )
@@ -0,0 +1,7 @@
1
+ """Utility functions for Parishad."""
2
+
3
+ from .logging import setup_logging, get_logger, truncate_for_log
4
+ from .tracing import TraceManager
5
+
6
+
7
+ __all__ = ["setup_logging", "get_logger", "truncate_for_log", "TraceManager"]
@@ -0,0 +1,122 @@
1
+ """
2
+ Hardware detection utility for Parishad.
3
+ Detects system capabilities (RAM, GPU, VRAM) to populate config.json.
4
+ """
5
+ import platform
6
+ import psutil
7
+ import subprocess
8
+ import logging
9
+ from dataclasses import dataclass, asdict
10
+ from typing import Optional
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ @dataclass
15
+ class GPUInfo:
16
+ name: str
17
+ vram_gb: float
18
+ type: str # "apple_silicon", "cuda", "cpu"
19
+
20
+ @dataclass
21
+ class SystemInfo:
22
+ os: str
23
+ arch: str
24
+ ram_gb: float
25
+ gpu: GPUInfo
26
+
27
+ def to_dict(self):
28
+ return asdict(self)
29
+
30
+ def get_system_info() -> SystemInfo:
31
+ """Detect all system information."""
32
+
33
+ # 1. OS & Arch
34
+ os_name = platform.system()
35
+ arch = platform.machine()
36
+
37
+ # 2. RAM
38
+ try:
39
+ ram_bytes = psutil.virtual_memory().total
40
+ ram_gb = round(ram_bytes / (1024**3), 1)
41
+ except:
42
+ ram_gb = 8.0 # Fallback
43
+
44
+ # 3. GPU Detection
45
+ gpu = _detect_gpu(os_name, arch, ram_gb)
46
+
47
+ return SystemInfo(
48
+ os=os_name,
49
+ arch=arch,
50
+ ram_gb=ram_gb,
51
+ gpu=gpu
52
+ )
53
+
54
+ def _detect_gpu(os_name: str, arch: str, system_ram_gb: float) -> GPUInfo:
55
+ """Detect GPU specifics."""
56
+
57
+ # A. Apple Silicon
58
+ if os_name == "Darwin" and "arm" in arch.lower():
59
+ # Try to get specific chip name via sysctl
60
+ try:
61
+ cmd = ["sysctl", "-n", "machdep.cpu.brand_string"]
62
+ result = subprocess.run(cmd, capture_output=True, text=True)
63
+ chip_name = result.stdout.strip()
64
+ except:
65
+ chip_name = "Apple Silicon"
66
+
67
+ return GPUInfo(
68
+ name=chip_name,
69
+ vram_gb=system_ram_gb * 0.7, # Unified memory heuristic (approx usable)
70
+ type="apple_silicon"
71
+ )
72
+
73
+ # B. NVIDIA (Priority: Torch -> nvidia-smi)
74
+
75
+ # 1. Try Torch execution (Most reliable for Python environment)
76
+ try:
77
+ import torch
78
+ if torch.cuda.is_available():
79
+ vram_gb = torch.cuda.get_device_properties(0).total_memory / (1024**3)
80
+ return GPUInfo(
81
+ name=torch.cuda.get_device_name(0),
82
+ vram_gb=round(vram_gb, 1),
83
+ type="cuda"
84
+ )
85
+ except ImportError:
86
+ pass
87
+
88
+ # 2. Try nvidia-smi (System level)
89
+ try:
90
+ # Check standard paths on Windows if not in PATH
91
+ smi_cmd = "nvidia-smi"
92
+ if os_name == "Windows":
93
+ import shutil
94
+ if not shutil.which("nvidia-smi"):
95
+ # Common install path
96
+ candidate = r"C:\Windows\System32\nvidia-smi.exe"
97
+ if os.path.exists(candidate):
98
+ smi_cmd = candidate
99
+
100
+ # Query details
101
+ cmd = [smi_cmd, "--query-gpu=name,memory.total", "--format=csv,noheader,nounits"]
102
+ result = subprocess.run(cmd, capture_output=True, text=True)
103
+ if result.returncode == 0:
104
+ lines = result.stdout.strip().split('\n')
105
+ if lines:
106
+ parts = lines[0].split(',')
107
+ name = parts[0].strip()
108
+ vram_mb = float(parts[1].strip())
109
+ return GPUInfo(
110
+ name=name,
111
+ vram_gb=round(vram_mb / 1024, 1),
112
+ type="cuda"
113
+ )
114
+ except Exception as e:
115
+ logger.debug(f"nvidia-smi check failed: {e}")
116
+
117
+ # C. Fallback CPU
118
+ return GPUInfo(
119
+ name="CPU",
120
+ vram_gb=0.0,
121
+ type="cpu"
122
+ )
@@ -0,0 +1,79 @@
1
+ """Logging configuration for Parishad."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import sys
7
+ from typing import Optional
8
+
9
+
10
+ # Default truncation limits for memory-efficient logging
11
+ DEFAULT_TRUNCATE_LENGTH = 512
12
+ MAX_TRUNCATE_LENGTH = 2048
13
+
14
+
15
+ def truncate_for_log(text: str, max_length: int = DEFAULT_TRUNCATE_LENGTH) -> str:
16
+ """
17
+ Truncate a string for logging to avoid large memory allocations.
18
+
19
+ Args:
20
+ text: The text to truncate
21
+ max_length: Maximum length before truncation (default: 512)
22
+
23
+ Returns:
24
+ Truncated string with ellipsis if over limit, original otherwise
25
+ """
26
+ if len(text) <= max_length:
27
+ return text
28
+ return text[:max_length] + f"... [truncated, {len(text)} total chars]"
29
+
30
+
31
+ def setup_logging(
32
+ level: str = "INFO",
33
+ log_file: Optional[str] = None,
34
+ format_string: Optional[str] = None
35
+ ) -> None:
36
+ """
37
+ Set up logging for Parishad.
38
+
39
+ Args:
40
+ level: Logging level (DEBUG, INFO, WARNING, ERROR)
41
+ log_file: Optional file path to write logs to
42
+ format_string: Optional custom format string
43
+ """
44
+ if format_string is None:
45
+ format_string = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
46
+
47
+ # Create formatter
48
+ formatter = logging.Formatter(format_string)
49
+
50
+ # Get root logger for parishad
51
+ logger = logging.getLogger("parishad")
52
+ logger.setLevel(getattr(logging, level.upper()))
53
+
54
+ # Clear existing handlers
55
+ logger.handlers.clear()
56
+
57
+ # Console handler
58
+ console_handler = logging.StreamHandler(sys.stdout)
59
+ console_handler.setFormatter(formatter)
60
+ logger.addHandler(console_handler)
61
+
62
+ # File handler if specified
63
+ if log_file:
64
+ file_handler = logging.FileHandler(log_file)
65
+ file_handler.setFormatter(formatter)
66
+ logger.addHandler(file_handler)
67
+
68
+
69
+ def get_logger(name: str) -> logging.Logger:
70
+ """
71
+ Get a logger for a Parishad module.
72
+
73
+ Args:
74
+ name: Module name (e.g., "orchestrator", "roles.refiner")
75
+
76
+ Returns:
77
+ Logger instance
78
+ """
79
+ return logging.getLogger(f"parishad.{name}")