saksh 1.0.0__tar.gz
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.
- saksh-1.0.0/MANIFEST.in +1 -0
- saksh-1.0.0/PKG-INFO +118 -0
- saksh-1.0.0/README.md +99 -0
- saksh-1.0.0/pyproject.toml +31 -0
- saksh-1.0.0/saksh/__init__.py +4 -0
- saksh-1.0.0/saksh/adapters.py +136 -0
- saksh-1.0.0/saksh/core.py +92 -0
- saksh-1.0.0/saksh/dashboard/server.py +48 -0
- saksh-1.0.0/saksh/dashboard/static/image.png +0 -0
- saksh-1.0.0/saksh/dashboard/static/index.html +61 -0
- saksh-1.0.0/saksh/dashboard/static/script.js +68 -0
- saksh-1.0.0/saksh/dashboard/static/style.css +77 -0
- saksh-1.0.0/saksh.egg-info/PKG-INFO +118 -0
- saksh-1.0.0/saksh.egg-info/SOURCES.txt +16 -0
- saksh-1.0.0/saksh.egg-info/dependency_links.txt +1 -0
- saksh-1.0.0/saksh.egg-info/requires.txt +7 -0
- saksh-1.0.0/saksh.egg-info/top_level.txt +1 -0
- saksh-1.0.0/setup.cfg +4 -0
saksh-1.0.0/MANIFEST.in
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
recursive-include saksh/dashboard/static *
|
saksh-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: saksh
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Sahasraksh - The All-Seeing Observer for AI Agents
|
|
5
|
+
Author-email: Ram Bikkina <itsrambikkina@gmail.com>
|
|
6
|
+
Keywords: observability,ai-agents,crewai,langgraph
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: fastapi
|
|
13
|
+
Requires-Dist: uvicorn
|
|
14
|
+
Requires-Dist: requests
|
|
15
|
+
Requires-Dist: pydantic
|
|
16
|
+
Requires-Dist: crewai
|
|
17
|
+
Requires-Dist: build>=1.3.0
|
|
18
|
+
Requires-Dist: twine>=6.0.1
|
|
19
|
+
|
|
20
|
+
# 👁️ Saksh (Sahasraksh) v1.0.0
|
|
21
|
+
|
|
22
|
+
> **"The All-Seeing Observer for your AI Agents."**
|
|
23
|
+
|
|
24
|
+
**Saksh** (derived from *Sahasraksh* — "The One with a Thousand Eyes") is a lightweight, non-intrusive observability layer designed specifically for modern AI Agent ecosystems.
|
|
25
|
+
|
|
26
|
+
Think of it as a **Sidecar** for your agents. whether you're running a swarm of **CrewAI** workers, a complex **LangGraph** state machine, or raw LLM chains, Saksh sits quietly in the background, monitoring health, latency, and vitality without ever blocking your main event loop.
|
|
27
|
+
|
|
28
|
+

|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
## ⚡ Why Saksh?
|
|
32
|
+
|
|
33
|
+
Building agents is hard. Debugging why they "hung" or "timed out" is harder.
|
|
34
|
+
Saksh gives you a **Mission Control** dashboard out of the box, so you can stop guessing if your agents are actually working or just hallucinating silence.
|
|
35
|
+
|
|
36
|
+
* **Universal & Agnostic:** We don't care if you use CrewAI, LangGraph, or raw OpenAI calls. If it has an API key and a URL, we can watch it.
|
|
37
|
+
* **Zero-Touch Dashboard:** Just instantiate the class, and a full React/FastAPI dashboard spins up on port `2604`. No React code required.
|
|
38
|
+
* **Vitality Scoring:** We don't just check "Is it up?". We calculate a **Vitality Score (0-100)** based on latency spikes and auth failures.
|
|
39
|
+
* **Non-Blocking Sidecar:** Runs in a daemon thread. Your agents keep working even if Saksh takes a coffee break.
|
|
40
|
+
|
|
41
|
+
## 📦 Installation
|
|
42
|
+
|
|
43
|
+
Get it via pip:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install saksh
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## 🛠️ Quick Start
|
|
51
|
+
|
|
52
|
+
You can attach Saksh to your existing stack in about 3 lines of code.
|
|
53
|
+
|
|
54
|
+
### 1. The Setup
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from saksh import Saksh, CrewAINetra, LangGraphNetra
|
|
58
|
+
from crewai import Agent
|
|
59
|
+
|
|
60
|
+
# 1. Summon the Observer 👁️
|
|
61
|
+
# This auto-magically starts the dashboard at http://localhost:2604
|
|
62
|
+
observer = Saksh(start_dashboard=True)
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 2. Connect CrewAI
|
|
67
|
+
|
|
68
|
+
Saksh automatically detects the LLM configuration (URL, API Key) inside your CrewAI agents.
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
# Your standard CrewAI setup
|
|
72
|
+
researcher = Agent(
|
|
73
|
+
role="Researcher",
|
|
74
|
+
goal="Analyze market trends",
|
|
75
|
+
backstory="You are a data wizard."
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Open an Eye on it
|
|
79
|
+
observer.open_eye(CrewAINetra(researcher, "Market-Researcher-01"))
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 3. Connect LangGraph
|
|
84
|
+
|
|
85
|
+
Since LangGraph is stateful and abstract, we monitor the underlying LLM provider it relies on.
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
# graph_app = workflow.compile()
|
|
89
|
+
|
|
90
|
+
# Register the graph (defaults to checking OpenAI availability)
|
|
91
|
+
observer.open_eye(LangGraphNetra(graph_app, "Math-Graph-State-Machine"))
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
*That's it. Your terminal will now log:*
|
|
96
|
+
`👁️ Saksh is watching...`
|
|
97
|
+
|
|
98
|
+
## 🧩 Architecture
|
|
99
|
+
|
|
100
|
+
Saksh uses a **Netra (Eye) Adapter Pattern**.
|
|
101
|
+
|
|
102
|
+
* **Saksh (Core):** The singleton observer that manages the background thread and dashboard.
|
|
103
|
+
* **Netra (Adapter):** A standardized interface that knows how to "Gaze" at a specific type of agent and normalize its health data into a `HealthPulse`.
|
|
104
|
+
|
|
105
|
+
## 🤝 Contributing
|
|
106
|
+
|
|
107
|
+
This is **v1.0.0** — the "It Works on My Machine" release.
|
|
108
|
+
We are actively looking for contributors to build **Netras** (Adapters) for:
|
|
109
|
+
|
|
110
|
+
* Autogen
|
|
111
|
+
* Semantic Kernel
|
|
112
|
+
* Local Ollama/Llama.cpp instances
|
|
113
|
+
|
|
114
|
+
PRs are welcome! Let's build the standard for Agent Observability together.
|
|
115
|
+
|
|
116
|
+
## 📜 License
|
|
117
|
+
|
|
118
|
+
MIT © 2026 Ram Bikkina
|
saksh-1.0.0/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# 👁️ Saksh (Sahasraksh) v1.0.0
|
|
2
|
+
|
|
3
|
+
> **"The All-Seeing Observer for your AI Agents."**
|
|
4
|
+
|
|
5
|
+
**Saksh** (derived from *Sahasraksh* — "The One with a Thousand Eyes") is a lightweight, non-intrusive observability layer designed specifically for modern AI Agent ecosystems.
|
|
6
|
+
|
|
7
|
+
Think of it as a **Sidecar** for your agents. whether you're running a swarm of **CrewAI** workers, a complex **LangGraph** state machine, or raw LLM chains, Saksh sits quietly in the background, monitoring health, latency, and vitality without ever blocking your main event loop.
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
## ⚡ Why Saksh?
|
|
13
|
+
|
|
14
|
+
Building agents is hard. Debugging why they "hung" or "timed out" is harder.
|
|
15
|
+
Saksh gives you a **Mission Control** dashboard out of the box, so you can stop guessing if your agents are actually working or just hallucinating silence.
|
|
16
|
+
|
|
17
|
+
* **Universal & Agnostic:** We don't care if you use CrewAI, LangGraph, or raw OpenAI calls. If it has an API key and a URL, we can watch it.
|
|
18
|
+
* **Zero-Touch Dashboard:** Just instantiate the class, and a full React/FastAPI dashboard spins up on port `2604`. No React code required.
|
|
19
|
+
* **Vitality Scoring:** We don't just check "Is it up?". We calculate a **Vitality Score (0-100)** based on latency spikes and auth failures.
|
|
20
|
+
* **Non-Blocking Sidecar:** Runs in a daemon thread. Your agents keep working even if Saksh takes a coffee break.
|
|
21
|
+
|
|
22
|
+
## 📦 Installation
|
|
23
|
+
|
|
24
|
+
Get it via pip:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install saksh
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## 🛠️ Quick Start
|
|
32
|
+
|
|
33
|
+
You can attach Saksh to your existing stack in about 3 lines of code.
|
|
34
|
+
|
|
35
|
+
### 1. The Setup
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from saksh import Saksh, CrewAINetra, LangGraphNetra
|
|
39
|
+
from crewai import Agent
|
|
40
|
+
|
|
41
|
+
# 1. Summon the Observer 👁️
|
|
42
|
+
# This auto-magically starts the dashboard at http://localhost:2604
|
|
43
|
+
observer = Saksh(start_dashboard=True)
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 2. Connect CrewAI
|
|
48
|
+
|
|
49
|
+
Saksh automatically detects the LLM configuration (URL, API Key) inside your CrewAI agents.
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
# Your standard CrewAI setup
|
|
53
|
+
researcher = Agent(
|
|
54
|
+
role="Researcher",
|
|
55
|
+
goal="Analyze market trends",
|
|
56
|
+
backstory="You are a data wizard."
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Open an Eye on it
|
|
60
|
+
observer.open_eye(CrewAINetra(researcher, "Market-Researcher-01"))
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 3. Connect LangGraph
|
|
65
|
+
|
|
66
|
+
Since LangGraph is stateful and abstract, we monitor the underlying LLM provider it relies on.
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
# graph_app = workflow.compile()
|
|
70
|
+
|
|
71
|
+
# Register the graph (defaults to checking OpenAI availability)
|
|
72
|
+
observer.open_eye(LangGraphNetra(graph_app, "Math-Graph-State-Machine"))
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
*That's it. Your terminal will now log:*
|
|
77
|
+
`👁️ Saksh is watching...`
|
|
78
|
+
|
|
79
|
+
## 🧩 Architecture
|
|
80
|
+
|
|
81
|
+
Saksh uses a **Netra (Eye) Adapter Pattern**.
|
|
82
|
+
|
|
83
|
+
* **Saksh (Core):** The singleton observer that manages the background thread and dashboard.
|
|
84
|
+
* **Netra (Adapter):** A standardized interface that knows how to "Gaze" at a specific type of agent and normalize its health data into a `HealthPulse`.
|
|
85
|
+
|
|
86
|
+
## 🤝 Contributing
|
|
87
|
+
|
|
88
|
+
This is **v1.0.0** — the "It Works on My Machine" release.
|
|
89
|
+
We are actively looking for contributors to build **Netras** (Adapters) for:
|
|
90
|
+
|
|
91
|
+
* Autogen
|
|
92
|
+
* Semantic Kernel
|
|
93
|
+
* Local Ollama/Llama.cpp instances
|
|
94
|
+
|
|
95
|
+
PRs are welcome! Let's build the standard for Agent Observability together.
|
|
96
|
+
|
|
97
|
+
## 📜 License
|
|
98
|
+
|
|
99
|
+
MIT © 2026 Ram Bikkina
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "saksh"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Sahasraksh - The All-Seeing Observer for AI Agents"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [{name = "Ram Bikkina", email = "itsrambikkina@gmail.com"}]
|
|
11
|
+
dependencies = [
|
|
12
|
+
"fastapi",
|
|
13
|
+
"uvicorn",
|
|
14
|
+
"requests",
|
|
15
|
+
"pydantic",
|
|
16
|
+
"crewai",
|
|
17
|
+
"build>=1.3.0",
|
|
18
|
+
"twine>=6.0.1",
|
|
19
|
+
]
|
|
20
|
+
requires-python = ">=3.9"
|
|
21
|
+
|
|
22
|
+
# Optional: Add keywords and classifiers to help people find your library
|
|
23
|
+
keywords = ["observability", "ai-agents", "crewai", "langgraph"]
|
|
24
|
+
classifiers = [
|
|
25
|
+
"Programming Language :: Python :: 3",
|
|
26
|
+
"License :: OSI Approved :: MIT License",
|
|
27
|
+
"Operating System :: OS Independent",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[tool.setuptools.package-data]
|
|
31
|
+
saksh = ["dashboard/static/*"]
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import time
|
|
3
|
+
import os
|
|
4
|
+
from typing import Optional, Dict, Any
|
|
5
|
+
from .core import Netra, HealthPulse
|
|
6
|
+
|
|
7
|
+
# --- SHARED HELPER FUNCTION ---
|
|
8
|
+
def _perform_health_check(agent_id: str, url: str, headers: Dict[str, str], timeout: int = 5) -> HealthPulse:
|
|
9
|
+
"""
|
|
10
|
+
Internal helper to ping a URL and return a standardized HealthPulse.
|
|
11
|
+
Used by both CrewAI and LangGraph adapters to avoid code duplication.
|
|
12
|
+
"""
|
|
13
|
+
start = time.time()
|
|
14
|
+
status = "down"
|
|
15
|
+
score = 0
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
# Perform the actual network request
|
|
19
|
+
response = requests.get(url, headers=headers, timeout=timeout)
|
|
20
|
+
|
|
21
|
+
# --- STRICT STATUS CHECK ---
|
|
22
|
+
# 200 = Healthy (Key worked, Server up)
|
|
23
|
+
# 401 = DOWN (Key Failed - Critical Logic)
|
|
24
|
+
# 404 = Degraded (Server up, endpoint might be slightly wrong but alive)
|
|
25
|
+
|
|
26
|
+
if response.status_code == 200:
|
|
27
|
+
status = "healthy"
|
|
28
|
+
score = 100
|
|
29
|
+
elif response.status_code == 401:
|
|
30
|
+
status = "down"
|
|
31
|
+
score = 0
|
|
32
|
+
print(f"⚠️ SAKSH WARNING: Agent {agent_id} Failed Auth (401)")
|
|
33
|
+
else:
|
|
34
|
+
status = "degraded"
|
|
35
|
+
score = 50
|
|
36
|
+
|
|
37
|
+
except requests.exceptions.Timeout:
|
|
38
|
+
status = "degraded" # It's alive but slow
|
|
39
|
+
score = 40
|
|
40
|
+
print(f"⚠️ SAKSH WARNING: Agent {agent_id} Timed Out")
|
|
41
|
+
except Exception:
|
|
42
|
+
status = "down"
|
|
43
|
+
score = 0
|
|
44
|
+
|
|
45
|
+
# Calculate Latency
|
|
46
|
+
latency = (time.time() - start) * 1000
|
|
47
|
+
|
|
48
|
+
# Adjust Score based on Latency (Penalize Slowness)
|
|
49
|
+
if status == "healthy":
|
|
50
|
+
if latency > 1500: score -= 20
|
|
51
|
+
if latency > 3000: score -= 40
|
|
52
|
+
|
|
53
|
+
return HealthPulse(
|
|
54
|
+
agent_id=agent_id,
|
|
55
|
+
status=status,
|
|
56
|
+
latency_ms=round(latency, 2),
|
|
57
|
+
vitality_score=max(0, score) # Ensure score never goes below 0
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# --- 1. CREWAI ADAPTER ---
|
|
62
|
+
class CrewAINetra(Netra):
|
|
63
|
+
"""
|
|
64
|
+
Specialized for CrewAI Agents.
|
|
65
|
+
Automatically extracts: agent.llm.base_url and agent.llm.api_key
|
|
66
|
+
"""
|
|
67
|
+
def __init__(self, crewai_agent, agent_id: str):
|
|
68
|
+
super().__init__(agent_id)
|
|
69
|
+
self.agent = crewai_agent
|
|
70
|
+
|
|
71
|
+
# 1. Logic to extract URL
|
|
72
|
+
# CrewAI often hides the base_url inside the llm object
|
|
73
|
+
if hasattr(crewai_agent.llm, 'base_url') and crewai_agent.llm.base_url:
|
|
74
|
+
self.target_url = crewai_agent.llm.base_url
|
|
75
|
+
self.is_custom = True
|
|
76
|
+
else:
|
|
77
|
+
self.target_url = "https://api.openai.com/v1"
|
|
78
|
+
self.is_custom = False
|
|
79
|
+
|
|
80
|
+
# 2. Logic to extract API Key
|
|
81
|
+
self.api_key = getattr(crewai_agent.llm, 'api_key', None)
|
|
82
|
+
|
|
83
|
+
# Fallback: Check Environment Variable if key is missing in object
|
|
84
|
+
if not self.api_key and not self.is_custom:
|
|
85
|
+
self.api_key = os.getenv("OPENAI_API_KEY")
|
|
86
|
+
|
|
87
|
+
def gaze(self) -> HealthPulse:
|
|
88
|
+
# Construct the Check URL
|
|
89
|
+
if self.is_custom:
|
|
90
|
+
base = self.target_url.rstrip("/")
|
|
91
|
+
if base.endswith("/v1"):
|
|
92
|
+
check_url = f"{base}/models"
|
|
93
|
+
else:
|
|
94
|
+
check_url = f"{base}/v1/models"
|
|
95
|
+
else:
|
|
96
|
+
check_url = "https://api.openai.com/v1/models"
|
|
97
|
+
|
|
98
|
+
# Construct Headers
|
|
99
|
+
headers = {}
|
|
100
|
+
if self.api_key and self.api_key != "na":
|
|
101
|
+
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
102
|
+
|
|
103
|
+
# Delegate to shared helper
|
|
104
|
+
return _perform_health_check(self.agent_id, check_url, headers)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# --- 2. LANGGRAPH ADAPTER ---
|
|
108
|
+
class LangGraphNetra(Netra):
|
|
109
|
+
"""
|
|
110
|
+
Specialized for LangGraph Compiled Graphs.
|
|
111
|
+
Since Graphs are abstract, we monitor the underlying LLM provider they rely on.
|
|
112
|
+
"""
|
|
113
|
+
def __init__(self, graph_app, agent_id: str, target_url="https://api.openai.com/v1", api_key=None):
|
|
114
|
+
super().__init__(agent_id)
|
|
115
|
+
self.graph = graph_app
|
|
116
|
+
|
|
117
|
+
# LangGraph allows flexible backends, so we accept overrides.
|
|
118
|
+
# Defaults to OpenAI if not specified.
|
|
119
|
+
self.target_url = target_url
|
|
120
|
+
self.api_key = api_key or os.getenv("OPENAI_API_KEY")
|
|
121
|
+
|
|
122
|
+
def gaze(self) -> HealthPulse:
|
|
123
|
+
# Construct URL
|
|
124
|
+
base = self.target_url.rstrip("/")
|
|
125
|
+
if base.endswith("/v1"):
|
|
126
|
+
check_url = f"{base}/models"
|
|
127
|
+
else:
|
|
128
|
+
check_url = f"{base}/v1/models"
|
|
129
|
+
|
|
130
|
+
# Construct Headers
|
|
131
|
+
headers = {}
|
|
132
|
+
if self.api_key:
|
|
133
|
+
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
134
|
+
|
|
135
|
+
# Delegate to shared helper
|
|
136
|
+
return _perform_health_check(self.agent_id, check_url, headers)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
import uvicorn
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from typing import List, Dict, Any, Literal
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
import abc
|
|
9
|
+
from fastapi import FastAPI
|
|
10
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
11
|
+
from fastapi.staticfiles import StaticFiles
|
|
12
|
+
|
|
13
|
+
# --- SCHEMAS ---
|
|
14
|
+
class HealthPulse(BaseModel):
|
|
15
|
+
agent_id: str
|
|
16
|
+
status: Literal["healthy", "degraded", "down"]
|
|
17
|
+
latency_ms: float
|
|
18
|
+
timestamp: datetime = Field(default_factory=datetime.now)
|
|
19
|
+
vitality_score: int
|
|
20
|
+
|
|
21
|
+
class Netra(abc.ABC):
|
|
22
|
+
def __init__(self, agent_id: str):
|
|
23
|
+
self.agent_id = agent_id
|
|
24
|
+
|
|
25
|
+
@abc.abstractmethod
|
|
26
|
+
def gaze(self) -> HealthPulse:
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
class Saksh:
|
|
30
|
+
_instance = None
|
|
31
|
+
|
|
32
|
+
def __new__(cls, *args, **kwargs):
|
|
33
|
+
if not cls._instance:
|
|
34
|
+
cls._instance = super(Saksh, cls).__new__(cls)
|
|
35
|
+
return cls._instance
|
|
36
|
+
|
|
37
|
+
def __init__(self, start_dashboard=True, port=2604):
|
|
38
|
+
if hasattr(self, '_initialized'):
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
self._thousand_eyes: List[Netra] = []
|
|
42
|
+
self._initialized = True
|
|
43
|
+
|
|
44
|
+
if start_dashboard:
|
|
45
|
+
self._start_dashboard_thread(port)
|
|
46
|
+
|
|
47
|
+
def open_eye(self, adapter: Netra):
|
|
48
|
+
self._thousand_eyes.append(adapter)
|
|
49
|
+
|
|
50
|
+
def observe(self) -> Dict[str, Any]:
|
|
51
|
+
report = {"total": len(self._thousand_eyes), "status": "stable", "eyes": []}
|
|
52
|
+
for eye in self._thousand_eyes:
|
|
53
|
+
pulse = eye.gaze()
|
|
54
|
+
report["eyes"].append(pulse.dict())
|
|
55
|
+
if pulse.status == "down":
|
|
56
|
+
report["status"] = "critical"
|
|
57
|
+
return report
|
|
58
|
+
|
|
59
|
+
def _start_dashboard_thread(self, port):
|
|
60
|
+
def run_server():
|
|
61
|
+
app = FastAPI(title="Saksh Sidecar")
|
|
62
|
+
|
|
63
|
+
app.add_middleware(
|
|
64
|
+
CORSMiddleware,
|
|
65
|
+
allow_origins=["*"],
|
|
66
|
+
allow_methods=["*"],
|
|
67
|
+
allow_headers=["*"],
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
@app.get("/api/status")
|
|
71
|
+
def get_status():
|
|
72
|
+
return self.observe()
|
|
73
|
+
|
|
74
|
+
# --- PATH FIX FOR LIBRARY ---
|
|
75
|
+
# Finds the 'dashboard/static' folder relative to THIS file (core.py)
|
|
76
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
77
|
+
static_dir = os.path.join(current_dir, "dashboard", "static")
|
|
78
|
+
|
|
79
|
+
if os.path.exists(static_dir):
|
|
80
|
+
app.mount("/", StaticFiles(directory=static_dir, html=True), name="static")
|
|
81
|
+
else:
|
|
82
|
+
print(f"⚠️ SAKSH WARNING: Static files not found at {static_dir}")
|
|
83
|
+
|
|
84
|
+
print(f"\n👁️ SAKSH DASHBOARD ONLINE: http://localhost:{port}\n")
|
|
85
|
+
|
|
86
|
+
config = uvicorn.Config(app, host="0.0.0.0", port=port, log_level="error", loop="asyncio")
|
|
87
|
+
server = uvicorn.Server(config)
|
|
88
|
+
server.install_signal_handlers = lambda: None
|
|
89
|
+
server.run()
|
|
90
|
+
|
|
91
|
+
thread = threading.Thread(target=run_server, daemon=True)
|
|
92
|
+
thread.start()
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from fastapi import FastAPI
|
|
2
|
+
from fastapi.staticfiles import StaticFiles
|
|
3
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
4
|
+
import uvicorn
|
|
5
|
+
import sys
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
# Add root directory to sys.path so we can import 'core' and 'agents'
|
|
9
|
+
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
|
10
|
+
|
|
11
|
+
from core.saksh import Saksh
|
|
12
|
+
from core.adapters import CrewAINetra
|
|
13
|
+
from agents.qwen_agent import get_qwen_agent
|
|
14
|
+
from agents.gpt_agent import get_gpt_agent
|
|
15
|
+
|
|
16
|
+
app = FastAPI(title="Saksh Observability")
|
|
17
|
+
|
|
18
|
+
# Enable CORS (Allows local development flexibility)
|
|
19
|
+
app.add_middleware(
|
|
20
|
+
CORSMiddleware,
|
|
21
|
+
allow_origins=["*"],
|
|
22
|
+
allow_methods=["*"],
|
|
23
|
+
allow_headers=["*"],
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# --- INITIALIZE SAKSH ---
|
|
27
|
+
# We instantiate the observer here. It acts as a standalone monitor.
|
|
28
|
+
observer = Saksh()
|
|
29
|
+
qwen = get_qwen_agent()
|
|
30
|
+
gpt = get_gpt_agent()
|
|
31
|
+
|
|
32
|
+
# Register Eyes
|
|
33
|
+
observer.open_eye(CrewAINetra(qwen, "Qwen-Custom-01"))
|
|
34
|
+
observer.open_eye(CrewAINetra(gpt, "OpenAI-GPT4-01"))
|
|
35
|
+
|
|
36
|
+
# --- API ENDPOINTS ---
|
|
37
|
+
|
|
38
|
+
@app.get("/api/status")
|
|
39
|
+
def get_status():
|
|
40
|
+
"""Returns the JSON health matrix for the UI to consume"""
|
|
41
|
+
return observer.observe()
|
|
42
|
+
|
|
43
|
+
# --- SERVE STATIC FILES (HTML/JS/CSS) ---
|
|
44
|
+
app.mount("/", StaticFiles(directory="dashboard/static", html=True), name="static")
|
|
45
|
+
|
|
46
|
+
if __name__ == "__main__":
|
|
47
|
+
print("👁️ SAKSH Dashboard running at http://localhost:2604")
|
|
48
|
+
uvicorn.run(app, host="0.0.0.0", port=2604)
|
|
Binary file
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Saksh | Mission Control</title>
|
|
7
|
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
8
|
+
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
|
|
9
|
+
<link rel="stylesheet" href="style.css">
|
|
10
|
+
</head>
|
|
11
|
+
<body class="bg-black text-light">
|
|
12
|
+
|
|
13
|
+
<nav class="navbar navbar-dark border-bottom border-secondary p-3 mb-4" style="background-color: #0d1117;">
|
|
14
|
+
<div class="container">
|
|
15
|
+
<a class="navbar-brand fw-bold d-flex align-items-center" href="#">
|
|
16
|
+
<span class="pulse-dot me-3"></span>
|
|
17
|
+
👁️ SAKSH <span class="badge bg-secondary ms-2 small-badge">OBSERVER</span>
|
|
18
|
+
</a>
|
|
19
|
+
<span class="navbar-text text-success" id="system-status" style="font-size: 0.9rem;">
|
|
20
|
+
● System Online
|
|
21
|
+
</span>
|
|
22
|
+
</div>
|
|
23
|
+
</nav>
|
|
24
|
+
|
|
25
|
+
<div class="container">
|
|
26
|
+
|
|
27
|
+
<div class="row mb-5 text-center">
|
|
28
|
+
<div class="col-md-4">
|
|
29
|
+
<div class="stat-box">
|
|
30
|
+
<small class="text-uppercase text-muted fw-bold" style="letter-spacing: 1px;">Active Agents</small>
|
|
31
|
+
<h1 id="total-agents" class="display-4 fw-bold text-white mt-2">--</h1>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="col-md-4">
|
|
35
|
+
<div class="stat-box">
|
|
36
|
+
<small class="text-uppercase text-muted fw-bold" style="letter-spacing: 1px;">Ecosystem Status</small>
|
|
37
|
+
<h1 id="overall-health" class="display-4 fw-bold mt-2">--</h1>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
<div class="col-md-4">
|
|
41
|
+
<div class="stat-box">
|
|
42
|
+
<small class="text-uppercase text-muted fw-bold" style="letter-spacing: 1px;">Last Heartbeat</small>
|
|
43
|
+
<h1 id="last-update" class="display-6 mt-3 text-white">--:--:--</h1>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div class="d-flex justify-content-between align-items-center mb-3 border-bottom border-secondary pb-2">
|
|
49
|
+
<h5 class="text-muted m-0 text-uppercase">Live Agent Telemetry</h5>
|
|
50
|
+
<span class="badge bg-dark border border-secondary text-muted">AUTO-REFRESH: 2s</span>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div class="row g-4" id="agent-grid">
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
|
59
|
+
<script src="script.js"></script>
|
|
60
|
+
</body>
|
|
61
|
+
</html>
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
$(document).ready(function() {
|
|
2
|
+
|
|
3
|
+
function fetchPulse() {
|
|
4
|
+
$.ajax({
|
|
5
|
+
url: '/api/status',
|
|
6
|
+
method: 'GET',
|
|
7
|
+
success: function(data) {
|
|
8
|
+
updateDashboard(data);
|
|
9
|
+
$('#system-status').html('<span class="text-success">● System Online</span>');
|
|
10
|
+
},
|
|
11
|
+
error: function(err) {
|
|
12
|
+
$('#system-status').html('<span class="text-danger">⚠️ Connection Lost</span>');
|
|
13
|
+
$('.pulse-dot').css('background-color', 'red');
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function updateDashboard(data) {
|
|
19
|
+
// 1. Update Header Stats
|
|
20
|
+
$('#total-agents').text(data.total);
|
|
21
|
+
|
|
22
|
+
let healthColor = data.status === "stable" ? "text-healthy" : "text-down";
|
|
23
|
+
$('#overall-health').html(`<span class="${healthColor}">${data.status.toUpperCase()}</span>`);
|
|
24
|
+
|
|
25
|
+
let now = new Date();
|
|
26
|
+
$('#last-update').text(now.toLocaleTimeString());
|
|
27
|
+
|
|
28
|
+
// 2. Generate Grid HTML
|
|
29
|
+
let gridHtml = '';
|
|
30
|
+
data.eyes.forEach(agent => {
|
|
31
|
+
let statusColor = agent.status === 'healthy' ? 'text-healthy' :
|
|
32
|
+
agent.status === 'degraded' ? 'text-degraded' : 'text-down';
|
|
33
|
+
|
|
34
|
+
let borderColor = `border-${agent.status}`; // references css class
|
|
35
|
+
|
|
36
|
+
let icon = agent.status === 'healthy' ? '⚡' : '⚠️';
|
|
37
|
+
if(agent.status === 'down') icon = '🛑';
|
|
38
|
+
|
|
39
|
+
gridHtml += `
|
|
40
|
+
<div class="col-md-4">
|
|
41
|
+
<div class="agent-card ${borderColor} p-4 h-100">
|
|
42
|
+
<div class="d-flex justify-content-between align-items-start mb-3">
|
|
43
|
+
<h5 class="fw-bold mb-0 text-white">${icon} ${agent.agent_id}</h5>
|
|
44
|
+
<span class="badge bg-dark border border-secondary">${agent.latency_ms} ms</span>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<div class="text-center py-2">
|
|
48
|
+
<div class="vitality-number ${statusColor}">${agent.vitality_score}</div>
|
|
49
|
+
<small class="text-secondary text-uppercase tracking-wider">Vitality Score</small>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<div class="mt-3 pt-3 border-top border-secondary d-flex justify-content-between">
|
|
53
|
+
<small class="text-muted">STATUS</small>
|
|
54
|
+
<small class="fw-bold ${statusColor} text-uppercase">${agent.status}</small>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
`;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// 3. Update DOM (Only if changed to prevent flicker, simplified here to direct replace)
|
|
62
|
+
$('#agent-grid').html(gridHtml);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Start Polling
|
|
66
|
+
fetchPulse();
|
|
67
|
+
setInterval(fetchPulse, 2000);
|
|
68
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/* style.css */
|
|
2
|
+
body {
|
|
3
|
+
font-family: 'JetBrains Mono', monospace;
|
|
4
|
+
background-color: #050505 !important;
|
|
5
|
+
color: #e0e0e0;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/* --- Text Visibility Fixes --- */
|
|
9
|
+
/* Overriding Bootstrap's text-muted to be visible on black */
|
|
10
|
+
.text-muted {
|
|
11
|
+
color: #8b949e !important; /* Lighter Gray */
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
h5.text-muted {
|
|
15
|
+
color: #8b949e !important;
|
|
16
|
+
font-weight: 600;
|
|
17
|
+
letter-spacing: 1px;
|
|
18
|
+
}
|
|
19
|
+
/* ----------------------------- */
|
|
20
|
+
|
|
21
|
+
/* Navbar Pulse Dot */
|
|
22
|
+
.pulse-dot {
|
|
23
|
+
width: 10px;
|
|
24
|
+
height: 10px;
|
|
25
|
+
background-color: #00ff00;
|
|
26
|
+
border-radius: 50%;
|
|
27
|
+
box-shadow: 0 0 0 0 rgba(0, 255, 0, 0.7);
|
|
28
|
+
animation: pulse-green 2s infinite;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@keyframes pulse-green {
|
|
32
|
+
0% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(0, 255, 0, 0.7); }
|
|
33
|
+
70% { transform: scale(1); box-shadow: 0 0 0 10px rgba(0, 255, 0, 0); }
|
|
34
|
+
100% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(0, 255, 0, 0); }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* Stat Boxes */
|
|
38
|
+
.stat-box {
|
|
39
|
+
background: #0d1117;
|
|
40
|
+
border: 1px solid #30363d;
|
|
41
|
+
border-radius: 6px;
|
|
42
|
+
padding: 20px;
|
|
43
|
+
height: 100%; /* Ensure equal height */
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* Agent Cards */
|
|
47
|
+
.agent-card {
|
|
48
|
+
background: #0d1117;
|
|
49
|
+
border: 1px solid #30363d;
|
|
50
|
+
border-radius: 8px;
|
|
51
|
+
transition: all 0.3s ease;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.agent-card:hover {
|
|
55
|
+
transform: translateY(-2px);
|
|
56
|
+
box-shadow: 0 4px 15px rgba(0,0,0,0.5);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* Dynamic Status Borders */
|
|
60
|
+
.border-healthy { border-top: 4px solid #2ea043 !important; }
|
|
61
|
+
.border-degraded { border-top: 4px solid #d29922 !important; }
|
|
62
|
+
.border-down { border-top: 4px solid #da3633 !important; }
|
|
63
|
+
|
|
64
|
+
/* Text Colors */
|
|
65
|
+
.text-healthy { color: #2ea043; }
|
|
66
|
+
.text-degraded { color: #d29922; }
|
|
67
|
+
.text-down { color: #da3633; }
|
|
68
|
+
|
|
69
|
+
.vitality-number {
|
|
70
|
+
font-size: 3.5rem; /* Slightly larger */
|
|
71
|
+
font-weight: 700;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.small-badge {
|
|
75
|
+
font-size: 0.6em;
|
|
76
|
+
vertical-align: middle;
|
|
77
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: saksh
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Sahasraksh - The All-Seeing Observer for AI Agents
|
|
5
|
+
Author-email: Ram Bikkina <itsrambikkina@gmail.com>
|
|
6
|
+
Keywords: observability,ai-agents,crewai,langgraph
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: fastapi
|
|
13
|
+
Requires-Dist: uvicorn
|
|
14
|
+
Requires-Dist: requests
|
|
15
|
+
Requires-Dist: pydantic
|
|
16
|
+
Requires-Dist: crewai
|
|
17
|
+
Requires-Dist: build>=1.3.0
|
|
18
|
+
Requires-Dist: twine>=6.0.1
|
|
19
|
+
|
|
20
|
+
# 👁️ Saksh (Sahasraksh) v1.0.0
|
|
21
|
+
|
|
22
|
+
> **"The All-Seeing Observer for your AI Agents."**
|
|
23
|
+
|
|
24
|
+
**Saksh** (derived from *Sahasraksh* — "The One with a Thousand Eyes") is a lightweight, non-intrusive observability layer designed specifically for modern AI Agent ecosystems.
|
|
25
|
+
|
|
26
|
+
Think of it as a **Sidecar** for your agents. whether you're running a swarm of **CrewAI** workers, a complex **LangGraph** state machine, or raw LLM chains, Saksh sits quietly in the background, monitoring health, latency, and vitality without ever blocking your main event loop.
|
|
27
|
+
|
|
28
|
+

|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
## ⚡ Why Saksh?
|
|
32
|
+
|
|
33
|
+
Building agents is hard. Debugging why they "hung" or "timed out" is harder.
|
|
34
|
+
Saksh gives you a **Mission Control** dashboard out of the box, so you can stop guessing if your agents are actually working or just hallucinating silence.
|
|
35
|
+
|
|
36
|
+
* **Universal & Agnostic:** We don't care if you use CrewAI, LangGraph, or raw OpenAI calls. If it has an API key and a URL, we can watch it.
|
|
37
|
+
* **Zero-Touch Dashboard:** Just instantiate the class, and a full React/FastAPI dashboard spins up on port `2604`. No React code required.
|
|
38
|
+
* **Vitality Scoring:** We don't just check "Is it up?". We calculate a **Vitality Score (0-100)** based on latency spikes and auth failures.
|
|
39
|
+
* **Non-Blocking Sidecar:** Runs in a daemon thread. Your agents keep working even if Saksh takes a coffee break.
|
|
40
|
+
|
|
41
|
+
## 📦 Installation
|
|
42
|
+
|
|
43
|
+
Get it via pip:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install saksh
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## 🛠️ Quick Start
|
|
51
|
+
|
|
52
|
+
You can attach Saksh to your existing stack in about 3 lines of code.
|
|
53
|
+
|
|
54
|
+
### 1. The Setup
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from saksh import Saksh, CrewAINetra, LangGraphNetra
|
|
58
|
+
from crewai import Agent
|
|
59
|
+
|
|
60
|
+
# 1. Summon the Observer 👁️
|
|
61
|
+
# This auto-magically starts the dashboard at http://localhost:2604
|
|
62
|
+
observer = Saksh(start_dashboard=True)
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 2. Connect CrewAI
|
|
67
|
+
|
|
68
|
+
Saksh automatically detects the LLM configuration (URL, API Key) inside your CrewAI agents.
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
# Your standard CrewAI setup
|
|
72
|
+
researcher = Agent(
|
|
73
|
+
role="Researcher",
|
|
74
|
+
goal="Analyze market trends",
|
|
75
|
+
backstory="You are a data wizard."
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Open an Eye on it
|
|
79
|
+
observer.open_eye(CrewAINetra(researcher, "Market-Researcher-01"))
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 3. Connect LangGraph
|
|
84
|
+
|
|
85
|
+
Since LangGraph is stateful and abstract, we monitor the underlying LLM provider it relies on.
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
# graph_app = workflow.compile()
|
|
89
|
+
|
|
90
|
+
# Register the graph (defaults to checking OpenAI availability)
|
|
91
|
+
observer.open_eye(LangGraphNetra(graph_app, "Math-Graph-State-Machine"))
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
*That's it. Your terminal will now log:*
|
|
96
|
+
`👁️ Saksh is watching...`
|
|
97
|
+
|
|
98
|
+
## 🧩 Architecture
|
|
99
|
+
|
|
100
|
+
Saksh uses a **Netra (Eye) Adapter Pattern**.
|
|
101
|
+
|
|
102
|
+
* **Saksh (Core):** The singleton observer that manages the background thread and dashboard.
|
|
103
|
+
* **Netra (Adapter):** A standardized interface that knows how to "Gaze" at a specific type of agent and normalize its health data into a `HealthPulse`.
|
|
104
|
+
|
|
105
|
+
## 🤝 Contributing
|
|
106
|
+
|
|
107
|
+
This is **v1.0.0** — the "It Works on My Machine" release.
|
|
108
|
+
We are actively looking for contributors to build **Netras** (Adapters) for:
|
|
109
|
+
|
|
110
|
+
* Autogen
|
|
111
|
+
* Semantic Kernel
|
|
112
|
+
* Local Ollama/Llama.cpp instances
|
|
113
|
+
|
|
114
|
+
PRs are welcome! Let's build the standard for Agent Observability together.
|
|
115
|
+
|
|
116
|
+
## 📜 License
|
|
117
|
+
|
|
118
|
+
MIT © 2026 Ram Bikkina
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
MANIFEST.in
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
saksh/__init__.py
|
|
5
|
+
saksh/adapters.py
|
|
6
|
+
saksh/core.py
|
|
7
|
+
saksh.egg-info/PKG-INFO
|
|
8
|
+
saksh.egg-info/SOURCES.txt
|
|
9
|
+
saksh.egg-info/dependency_links.txt
|
|
10
|
+
saksh.egg-info/requires.txt
|
|
11
|
+
saksh.egg-info/top_level.txt
|
|
12
|
+
saksh/dashboard/server.py
|
|
13
|
+
saksh/dashboard/static/image.png
|
|
14
|
+
saksh/dashboard/static/index.html
|
|
15
|
+
saksh/dashboard/static/script.js
|
|
16
|
+
saksh/dashboard/static/style.css
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
saksh
|
saksh-1.0.0/setup.cfg
ADDED