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.
- parishad/__init__.py +70 -0
- parishad/__main__.py +10 -0
- parishad/checker/__init__.py +25 -0
- parishad/checker/deterministic.py +644 -0
- parishad/checker/ensemble.py +496 -0
- parishad/checker/retrieval.py +546 -0
- parishad/cli/__init__.py +6 -0
- parishad/cli/code.py +3254 -0
- parishad/cli/main.py +1158 -0
- parishad/cli/prarambh.py +99 -0
- parishad/cli/sthapana.py +368 -0
- parishad/config/modes.py +139 -0
- parishad/config/pipeline.core.yaml +128 -0
- parishad/config/pipeline.extended.yaml +172 -0
- parishad/config/pipeline.fast.yaml +89 -0
- parishad/config/user_config.py +115 -0
- parishad/data/catalog.py +118 -0
- parishad/data/models.json +108 -0
- parishad/memory/__init__.py +79 -0
- parishad/models/__init__.py +181 -0
- parishad/models/backends/__init__.py +247 -0
- parishad/models/backends/base.py +211 -0
- parishad/models/backends/huggingface.py +318 -0
- parishad/models/backends/llama_cpp.py +239 -0
- parishad/models/backends/mlx_lm.py +141 -0
- parishad/models/backends/ollama.py +253 -0
- parishad/models/backends/openai_api.py +193 -0
- parishad/models/backends/transformers_hf.py +198 -0
- parishad/models/costs.py +385 -0
- parishad/models/downloader.py +1557 -0
- parishad/models/optimizations.py +871 -0
- parishad/models/profiles.py +610 -0
- parishad/models/reliability.py +876 -0
- parishad/models/runner.py +651 -0
- parishad/models/tokenization.py +287 -0
- parishad/orchestrator/__init__.py +24 -0
- parishad/orchestrator/config_loader.py +210 -0
- parishad/orchestrator/engine.py +1113 -0
- parishad/orchestrator/exceptions.py +14 -0
- parishad/roles/__init__.py +71 -0
- parishad/roles/base.py +712 -0
- parishad/roles/dandadhyaksha.py +163 -0
- parishad/roles/darbari.py +246 -0
- parishad/roles/majumdar.py +274 -0
- parishad/roles/pantapradhan.py +150 -0
- parishad/roles/prerak.py +357 -0
- parishad/roles/raja.py +345 -0
- parishad/roles/sacheev.py +203 -0
- parishad/roles/sainik.py +427 -0
- parishad/roles/sar_senapati.py +164 -0
- parishad/roles/vidushak.py +69 -0
- parishad/tools/__init__.py +7 -0
- parishad/tools/base.py +57 -0
- parishad/tools/fs.py +110 -0
- parishad/tools/perception.py +96 -0
- parishad/tools/retrieval.py +74 -0
- parishad/tools/shell.py +103 -0
- parishad/utils/__init__.py +7 -0
- parishad/utils/hardware.py +122 -0
- parishad/utils/logging.py +79 -0
- parishad/utils/scanner.py +164 -0
- parishad/utils/text.py +61 -0
- parishad/utils/tracing.py +133 -0
- parishad-0.1.0.dist-info/METADATA +256 -0
- parishad-0.1.0.dist-info/RECORD +68 -0
- parishad-0.1.0.dist-info/WHEEL +4 -0
- parishad-0.1.0.dist-info/entry_points.txt +2 -0
- 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
|
+
)
|
parishad/tools/shell.py
ADDED
|
@@ -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,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}")
|