aris-sdk 0.1.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.
- aris_sdk-0.1.0/LICENSE +5 -0
- aris_sdk-0.1.0/PKG-INFO +20 -0
- aris_sdk-0.1.0/README.md +110 -0
- aris_sdk-0.1.0/agent_node/__init__.py +0 -0
- aris_sdk-0.1.0/agent_node/llm_agent.py +70 -0
- aris_sdk-0.1.0/agent_node/main.py +52 -0
- aris_sdk-0.1.0/agent_node/math_agent.py +44 -0
- aris_sdk-0.1.0/aris/__init__.py +4 -0
- aris_sdk-0.1.0/aris/client.py +137 -0
- aris_sdk-0.1.0/aris/types.py +0 -0
- aris_sdk-0.1.0/aris_sdk.egg-info/PKG-INFO +20 -0
- aris_sdk-0.1.0/aris_sdk.egg-info/SOURCES.txt +21 -0
- aris_sdk-0.1.0/aris_sdk.egg-info/dependency_links.txt +1 -0
- aris_sdk-0.1.0/aris_sdk.egg-info/entry_points.txt +3 -0
- aris_sdk-0.1.0/aris_sdk.egg-info/requires.txt +8 -0
- aris_sdk-0.1.0/aris_sdk.egg-info/top_level.txt +3 -0
- aris_sdk-0.1.0/registry/__init__.py +0 -0
- aris_sdk-0.1.0/registry/main.py +155 -0
- aris_sdk-0.1.0/registry/mint_key.py +53 -0
- aris_sdk-0.1.0/registry/models.py +0 -0
- aris_sdk-0.1.0/registry/search.py +60 -0
- aris_sdk-0.1.0/setup.cfg +4 -0
- aris_sdk-0.1.0/setup.py +30 -0
aris_sdk-0.1.0/LICENSE
ADDED
aris_sdk-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aris-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Decentralized AI Network SDK and Node Infrastructure
|
|
5
|
+
Author: Sid
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: requests
|
|
9
|
+
Requires-Dist: pydantic
|
|
10
|
+
Requires-Dist: python-jose[cryptography]
|
|
11
|
+
Requires-Dist: uvicorn
|
|
12
|
+
Requires-Dist: fastapi
|
|
13
|
+
Requires-Dist: httpx
|
|
14
|
+
Requires-Dist: pyjwt
|
|
15
|
+
Requires-Dist: python-dotenv
|
|
16
|
+
Dynamic: author
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
Dynamic: requires-dist
|
|
19
|
+
Dynamic: requires-python
|
|
20
|
+
Dynamic: summary
|
aris_sdk-0.1.0/README.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Aris SDK š
|
|
2
|
+
### The Decentralized AI Network Layer
|
|
3
|
+
|
|
4
|
+
Aris is a lightweight, decentralized infrastructure for local LLM inference. It allows users to contribute compute power as "Worker Nodes" and developers to consume that AI compute via a unified SDK with built-in micro-payments and registry management.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## š Features
|
|
9
|
+
- **Decentralized Registry**: On-chain style ledger for managing API keys and balances.
|
|
10
|
+
- **Worker Nodes**: Easily turn any machine with a local LLM into a revenue-generating node.
|
|
11
|
+
- **Standardized SDK**: OpenAI-compatible interface for easy integration.
|
|
12
|
+
- **Transparent Billing**: Built-in balance deduction per inference request.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## š¦ Installation
|
|
17
|
+
|
|
18
|
+
Install the core SDK via pip:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install aris-sdk
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## š Quick Start (Client Side)
|
|
28
|
+
|
|
29
|
+
To use the network as a developer, simply initialize the `Aris` client with your API key.
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from aris.client import Aris
|
|
33
|
+
|
|
34
|
+
# Initialize with your minted key
|
|
35
|
+
client = Aris(api_key="sk-aris-your-unique-key")
|
|
36
|
+
|
|
37
|
+
# Generate a response from the decentralized swarm
|
|
38
|
+
response = client.generate("Why is the ocean salty?")
|
|
39
|
+
|
|
40
|
+
print(f"š¤ ARIS: {response}")
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## š Infrastructure Setup
|
|
47
|
+
|
|
48
|
+
### 1. Start the Registry
|
|
49
|
+
|
|
50
|
+
The Registry acts as the "Bank" and "DNS" of the network.
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# Run the registry server (Default port: 8000)
|
|
54
|
+
python3 -m registry.main
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 2. Join as a Worker Node
|
|
59
|
+
|
|
60
|
+
Nodes register themselves with the Registry and wait for inference jobs.
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# Start a node on port 9006
|
|
64
|
+
aris-node --port 9006
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### 3. Admin: Minting New Keys
|
|
69
|
+
|
|
70
|
+
Manage your network users using the admin CLI.
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# Create a new user with a 500 credit balance
|
|
74
|
+
aris-admin --email user@example.com --balance 500.0
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## š Project Structure
|
|
81
|
+
|
|
82
|
+
* `/aris`: The core Client SDK logic.
|
|
83
|
+
* `/agent_node`: Worker node infrastructure and LLM integration (TinyLlama).
|
|
84
|
+
* `/registry`: Centralized ledger for account and node management.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## š License
|
|
89
|
+
|
|
90
|
+
Distributed under the MIT License. See `LICENSE` for more information.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## š¤ Contributing
|
|
95
|
+
|
|
96
|
+
Contributions are welcome! Please open an issue or submit a pull request.
|
|
97
|
+
|
|
98
|
+
**Built with ā¤ļø for the Decentralized AI Community.**
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
### **Final Pro-Tip for the Launch**
|
|
105
|
+
Before you run `python3 -m twine upload dist/*`, make sure your `LICENSE` file exists. You can create a quick one like this:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
echo "Copyright (c) 2026 Sid - MIT License" > LICENSE
|
|
109
|
+
|
|
110
|
+
```
|
|
File without changes
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import uvicorn
|
|
2
|
+
import jwt
|
|
3
|
+
import os
|
|
4
|
+
import httpx
|
|
5
|
+
import asyncio
|
|
6
|
+
import argparse
|
|
7
|
+
from fastapi import FastAPI, HTTPException, Header
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
app = FastAPI(title="Aris Node: LLM Specialist")
|
|
11
|
+
|
|
12
|
+
# --- CONFIG ---
|
|
13
|
+
REGISTRY_URL = os.getenv("ARIS_REGISTRY", "http://localhost:8000/register")
|
|
14
|
+
MY_DID = "did:aris:llm-node-01"
|
|
15
|
+
MY_ENDPOINT = "http://localhost:9006"
|
|
16
|
+
ARIS_PUBLIC_KEY = "s3cret_k3y_for_signing_tokens"
|
|
17
|
+
OLLAMA_URL = "http://localhost:11434/api/generate"
|
|
18
|
+
|
|
19
|
+
# --- AUTO-REGISTER ON STARTUP ---
|
|
20
|
+
@app.on_event("startup")
|
|
21
|
+
async def start_heartbeat():
|
|
22
|
+
async def heartbeat():
|
|
23
|
+
while True:
|
|
24
|
+
async with httpx.AsyncClient() as client:
|
|
25
|
+
try:
|
|
26
|
+
await client.post(REGISTRY_URL, json={
|
|
27
|
+
"did": MY_DID,
|
|
28
|
+
"endpoint": MY_ENDPOINT,
|
|
29
|
+
"capabilities": ["ai.generate"]
|
|
30
|
+
})
|
|
31
|
+
print("š Heartbeat: Registry updated.")
|
|
32
|
+
except Exception:
|
|
33
|
+
print("ā ļø Heartbeat: Registry unreachable...")
|
|
34
|
+
await asyncio.sleep(30) # Check in every 30 seconds
|
|
35
|
+
|
|
36
|
+
asyncio.create_task(heartbeat())
|
|
37
|
+
|
|
38
|
+
# --- MODELS & ENDPOINTS ---
|
|
39
|
+
class PromptRequest(BaseModel):
|
|
40
|
+
model: str = "tinyllama"
|
|
41
|
+
prompt: str
|
|
42
|
+
|
|
43
|
+
@app.post("/generate")
|
|
44
|
+
async def generate_text(job: PromptRequest, x_aris_token: str = Header(...)):
|
|
45
|
+
try:
|
|
46
|
+
# Verify the session token issued by the Registry
|
|
47
|
+
payload = jwt.decode(x_aris_token, ARIS_PUBLIC_KEY, algorithms=["HS256"], audience=MY_DID)
|
|
48
|
+
except jwt.InvalidTokenError:
|
|
49
|
+
raise HTTPException(401, "Invalid or Expired Token")
|
|
50
|
+
|
|
51
|
+
print(f"š§ [LLM Node] Processing for {payload['sub']}...")
|
|
52
|
+
|
|
53
|
+
async with httpx.AsyncClient() as client:
|
|
54
|
+
resp = await client.post(OLLAMA_URL, json={
|
|
55
|
+
"model": job.model, "prompt": job.prompt, "stream": False
|
|
56
|
+
}, timeout=60.0)
|
|
57
|
+
return {"result": resp.json().get("response", ""), "status": "success"}
|
|
58
|
+
|
|
59
|
+
def main():
|
|
60
|
+
parser = argparse.ArgumentParser(description="Aris Worker Node")
|
|
61
|
+
parser.add_argument("--port", type=int, default=9006, help="Port to run the agent on")
|
|
62
|
+
parser.add_argument("--model", type=str, default="tinyllama", help="Ollama model to use")
|
|
63
|
+
args = parser.parse_args()
|
|
64
|
+
|
|
65
|
+
print(f"š Starting Aris Node on port {args.port} using {args.model}...")
|
|
66
|
+
uvicorn.run(app, host="0.0.0.0", port=args.port)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
if __name__ == "__main__":
|
|
70
|
+
main()
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import argparse
|
|
3
|
+
import uvicorn
|
|
4
|
+
import base64
|
|
5
|
+
import hmac
|
|
6
|
+
import hashlib
|
|
7
|
+
import time
|
|
8
|
+
import ollama
|
|
9
|
+
from fastapi import FastAPI, HTTPException
|
|
10
|
+
from pydantic import BaseModel
|
|
11
|
+
|
|
12
|
+
# --- 1. SETUP DYNAMIC ARGS ---
|
|
13
|
+
parser = argparse.ArgumentParser()
|
|
14
|
+
parser.add_argument("--port", type=int, default=9001)
|
|
15
|
+
parser.add_argument("--name", type=str, default="Unknown Agent")
|
|
16
|
+
args, _ = parser.parse_known_args()
|
|
17
|
+
|
|
18
|
+
app = FastAPI(title=f"Aris Node: {args.name}")
|
|
19
|
+
|
|
20
|
+
# --- CONFIG ---
|
|
21
|
+
ARIS_SECRET_KEY = b"super_secret_platform_key_2025"
|
|
22
|
+
MODEL_NAME = "tinyllama"
|
|
23
|
+
|
|
24
|
+
class JobRequest(BaseModel):
|
|
25
|
+
prompt: str
|
|
26
|
+
session_token: str
|
|
27
|
+
|
|
28
|
+
@app.post("/process_job")
|
|
29
|
+
async def process_job(job: JobRequest):
|
|
30
|
+
# AUTH (Same as before)
|
|
31
|
+
try:
|
|
32
|
+
if "." not in job.session_token: raise ValueError("Invalid token")
|
|
33
|
+
payload_b64, signature = job.session_token.split(".")
|
|
34
|
+
expected_sig = hmac.new(ARIS_SECRET_KEY, base64.b64decode(payload_b64), hashlib.sha256).hexdigest()
|
|
35
|
+
if not hmac.compare_digest(signature, expected_sig): raise HTTPException(401, "FAKE TOKEN")
|
|
36
|
+
except Exception as e:
|
|
37
|
+
raise HTTPException(401, str(e))
|
|
38
|
+
|
|
39
|
+
# INTELLIGENCE
|
|
40
|
+
print(f"[{args.name}] Thinking...")
|
|
41
|
+
try:
|
|
42
|
+
response = ollama.chat(model=MODEL_NAME, messages=[{'role': 'user', 'content': job.prompt}])
|
|
43
|
+
return {
|
|
44
|
+
"node": args.name,
|
|
45
|
+
"response": response['message']['content']
|
|
46
|
+
}
|
|
47
|
+
except Exception as e:
|
|
48
|
+
raise HTTPException(500, str(e))
|
|
49
|
+
|
|
50
|
+
if __name__ == "__main__":
|
|
51
|
+
# This allows us to run: python main.py --port 9002
|
|
52
|
+
uvicorn.run(app, host="0.0.0.0", port=args.port)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import uvicorn
|
|
2
|
+
import jwt
|
|
3
|
+
from fastapi import FastAPI, HTTPException, Header
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
app = FastAPI(title="Aris Node: Math Specialist")
|
|
7
|
+
|
|
8
|
+
# The Agent needs the Registry's Public Key (or shared secret for HS256) to verify tokens
|
|
9
|
+
ARIS_PUBLIC_KEY = "s3cret_k3y_for_signing_tokens"
|
|
10
|
+
|
|
11
|
+
class JobRequest(BaseModel):
|
|
12
|
+
a: int
|
|
13
|
+
b: int
|
|
14
|
+
operation: str # "add"
|
|
15
|
+
|
|
16
|
+
@app.post("/execute")
|
|
17
|
+
async def execute_task(job: JobRequest, x_aris_token: str = Header(...)):
|
|
18
|
+
"""
|
|
19
|
+
Section 3C: Agent verifies the token before working.
|
|
20
|
+
"""
|
|
21
|
+
try:
|
|
22
|
+
# 1. Verify the signature (Did Aris sign this?)
|
|
23
|
+
payload = jwt.decode(x_aris_token, ARIS_PUBLIC_KEY, algorithms=["HS256"], audience="did:aris:math-node-01")
|
|
24
|
+
|
|
25
|
+
# 2. Check Scope
|
|
26
|
+
if "math.add" not in payload.get("scope", ""):
|
|
27
|
+
raise HTTPException(403, "Token not valid for math.add")
|
|
28
|
+
|
|
29
|
+
print(f"š¤ [Agent] Token Verified! Processing job for {payload['sub']}")
|
|
30
|
+
|
|
31
|
+
except jwt.ExpiredSignatureError:
|
|
32
|
+
raise HTTPException(401, "Token Expired")
|
|
33
|
+
except jwt.InvalidTokenError as e:
|
|
34
|
+
raise HTTPException(401, f"Invalid Token: {str(e)}")
|
|
35
|
+
|
|
36
|
+
# 3. Do the Work (The Capability)
|
|
37
|
+
if job.operation == "add":
|
|
38
|
+
return {"result": job.a + job.b, "status": "success"}
|
|
39
|
+
|
|
40
|
+
return {"error": "Unsupported operation"}
|
|
41
|
+
|
|
42
|
+
if __name__ == "__main__":
|
|
43
|
+
# Runs on Port 9005 (Math Node)
|
|
44
|
+
uvicorn.run(app, host="0.0.0.0", port=9005)
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import requests
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Optional, Dict, Any
|
|
5
|
+
|
|
6
|
+
# Configure library logging (NullHandler by default so we don't spam unless configured)
|
|
7
|
+
logger = logging.getLogger("aris")
|
|
8
|
+
logger.addHandler(logging.NullHandler())
|
|
9
|
+
|
|
10
|
+
# --- Custom Exceptions (The Professional Way) ---
|
|
11
|
+
class ArisError(Exception):
|
|
12
|
+
"""Base exception for all Aris SDK errors."""
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
class ArisPaymentError(ArisError):
|
|
16
|
+
"""Raised when payment fails or balance is insufficient."""
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
class ArisNodeError(ArisError):
|
|
20
|
+
"""Raised when the worker node fails to respond."""
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
# --- The Main Client ---
|
|
24
|
+
class Aris:
|
|
25
|
+
def __init__(self, api_key: Optional[str] = None, registry_url: Optional[str] = None):
|
|
26
|
+
"""
|
|
27
|
+
Initialize the Aris Client.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
api_key: Your Aris API Key (starts with sk-). Defaults to ARIS_API_KEY env var.
|
|
31
|
+
registry_url: URL of the Aris Registry. Defaults to ARIS_REGISTRY_URL env var or localhost.
|
|
32
|
+
"""
|
|
33
|
+
self.api_key = api_key or os.getenv("ARIS_API_KEY")
|
|
34
|
+
if not self.api_key:
|
|
35
|
+
raise ArisError("ā Missing API Key. Pass it to Aris() or set ARIS_API_KEY.")
|
|
36
|
+
|
|
37
|
+
self.registry_url = registry_url or os.getenv("ARIS_REGISTRY_URL", "http://localhost:8000")
|
|
38
|
+
self.session_token: Optional[str] = None
|
|
39
|
+
self.target_endpoint: Optional[str] = None
|
|
40
|
+
|
|
41
|
+
def generate(self, prompt: str, model: str = "tinyllama") -> str:
|
|
42
|
+
"""
|
|
43
|
+
Generate text using the decentralized Aris network.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
prompt: The text prompt to send.
|
|
47
|
+
model: The model to use (default: tinyllama).
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
The generated text string.
|
|
51
|
+
"""
|
|
52
|
+
# Auto-connect if not connected
|
|
53
|
+
if not self.session_token:
|
|
54
|
+
self._connect_to_swarm()
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
return self._execute_request(prompt, model)
|
|
58
|
+
except ArisError as e:
|
|
59
|
+
# If token expired, try one refresh
|
|
60
|
+
logger.warning(f"ā ļø Request failed ({e}). Attempting to refresh session...")
|
|
61
|
+
self.session_token = None
|
|
62
|
+
self._connect_to_swarm()
|
|
63
|
+
return self._execute_request(prompt, model)
|
|
64
|
+
|
|
65
|
+
def _connect_to_swarm(self):
|
|
66
|
+
"""Handles Discovery (finding nodes) and Handshake (payment)."""
|
|
67
|
+
logger.info("š Discovering best available node...")
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
# 1. Discover
|
|
71
|
+
resp = requests.get(f"{self.registry_url}/discover", params={"capability": "ai.generate"}, timeout=5)
|
|
72
|
+
resp.raise_for_status()
|
|
73
|
+
data = resp.json()
|
|
74
|
+
|
|
75
|
+
if not data.get("agents"):
|
|
76
|
+
raise ArisNodeError("No active worker nodes found in the network.")
|
|
77
|
+
|
|
78
|
+
# Simple routing: Pick the first one (Future: pick based on latency/price)
|
|
79
|
+
target = data["agents"][0]
|
|
80
|
+
self.target_endpoint = target["endpoint"]
|
|
81
|
+
target_did = target["did"]
|
|
82
|
+
|
|
83
|
+
# 2. Handshake (The Transaction)
|
|
84
|
+
logger.info(f"š³ Negotiating contract with {target_did}...")
|
|
85
|
+
|
|
86
|
+
pay_resp = requests.post(
|
|
87
|
+
f"{self.registry_url}/handshake",
|
|
88
|
+
json={
|
|
89
|
+
"payer_did": "did:aris:customer-sdk", # In future, derive this from key
|
|
90
|
+
"target_did": target_did,
|
|
91
|
+
"capability": "ai.generate"
|
|
92
|
+
},
|
|
93
|
+
headers={"x-api-key": self.api_key},
|
|
94
|
+
timeout=10
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
if pay_resp.status_code == 402:
|
|
98
|
+
raise ArisPaymentError("Insufficient Balance. Please top up your Aris account.")
|
|
99
|
+
elif pay_resp.status_code != 200:
|
|
100
|
+
raise ArisError(f"Handshake failed: {pay_resp.text}")
|
|
101
|
+
|
|
102
|
+
session_data = pay_resp.json()
|
|
103
|
+
self.session_token = session_data["session_token"]
|
|
104
|
+
|
|
105
|
+
logger.info(f"ā
Session Established. Balance remaining: ā¹{session_data.get('remaining_balance', '???')}")
|
|
106
|
+
|
|
107
|
+
except requests.RequestException as e:
|
|
108
|
+
raise ArisError(f"Network error connecting to Registry: {e}")
|
|
109
|
+
|
|
110
|
+
def _execute_request(self, prompt: str, model: str) -> str:
|
|
111
|
+
"""Direct Peer-to-Peer execution with the Worker Node."""
|
|
112
|
+
if not self.target_endpoint:
|
|
113
|
+
raise ArisError("No target endpoint configured.")
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
response = requests.post(
|
|
117
|
+
f"{self.target_endpoint}/generate",
|
|
118
|
+
json={"model": model, "prompt": prompt},
|
|
119
|
+
headers={"x-aris-token": self.session_token},
|
|
120
|
+
timeout=60 # AI can be slow
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
if response.status_code == 200:
|
|
124
|
+
return response.json().get("result", "")
|
|
125
|
+
elif response.status_code in [401, 403]:
|
|
126
|
+
raise ArisError("Session Token Expired or Invalid")
|
|
127
|
+
else:
|
|
128
|
+
raise ArisNodeError(f"Worker Node Error: {response.text}")
|
|
129
|
+
|
|
130
|
+
except requests.RequestException as e:
|
|
131
|
+
raise ArisNodeError(f"Failed to communicate with Worker Node: {e}")
|
|
132
|
+
|
|
133
|
+
# --- Helper for quick scripts ---
|
|
134
|
+
def generate(prompt: str, api_key: Optional[str] = None) -> str:
|
|
135
|
+
"""Quick helper function for one-off generations."""
|
|
136
|
+
client = Aris(api_key=api_key)
|
|
137
|
+
return client.generate(prompt)
|
|
File without changes
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aris-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Decentralized AI Network SDK and Node Infrastructure
|
|
5
|
+
Author: Sid
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: requests
|
|
9
|
+
Requires-Dist: pydantic
|
|
10
|
+
Requires-Dist: python-jose[cryptography]
|
|
11
|
+
Requires-Dist: uvicorn
|
|
12
|
+
Requires-Dist: fastapi
|
|
13
|
+
Requires-Dist: httpx
|
|
14
|
+
Requires-Dist: pyjwt
|
|
15
|
+
Requires-Dist: python-dotenv
|
|
16
|
+
Dynamic: author
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
Dynamic: requires-dist
|
|
19
|
+
Dynamic: requires-python
|
|
20
|
+
Dynamic: summary
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
setup.py
|
|
4
|
+
agent_node/__init__.py
|
|
5
|
+
agent_node/llm_agent.py
|
|
6
|
+
agent_node/main.py
|
|
7
|
+
agent_node/math_agent.py
|
|
8
|
+
aris/__init__.py
|
|
9
|
+
aris/client.py
|
|
10
|
+
aris/types.py
|
|
11
|
+
aris_sdk.egg-info/PKG-INFO
|
|
12
|
+
aris_sdk.egg-info/SOURCES.txt
|
|
13
|
+
aris_sdk.egg-info/dependency_links.txt
|
|
14
|
+
aris_sdk.egg-info/entry_points.txt
|
|
15
|
+
aris_sdk.egg-info/requires.txt
|
|
16
|
+
aris_sdk.egg-info/top_level.txt
|
|
17
|
+
registry/__init__.py
|
|
18
|
+
registry/main.py
|
|
19
|
+
registry/mint_key.py
|
|
20
|
+
registry/models.py
|
|
21
|
+
registry/search.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
File without changes
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import json
|
|
3
|
+
import jwt # pip install pyjwt
|
|
4
|
+
from fastapi import FastAPI, HTTPException, Header
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
from typing import List, Dict, Optional
|
|
7
|
+
|
|
8
|
+
# --- CONFIG ---
|
|
9
|
+
ARIS_PRIVATE_KEY = "s3cret_k3y_for_signing_tokens"
|
|
10
|
+
MVP_PRICE_INR = 20.00
|
|
11
|
+
LEDGER_FILE = "registry/accounts.json"
|
|
12
|
+
|
|
13
|
+
app = FastAPI(title="Aris Registry (Paid)", version="0.2")
|
|
14
|
+
|
|
15
|
+
# --- DATA STORES ---
|
|
16
|
+
service_registry: Dict[str, List[dict]] = {}
|
|
17
|
+
ledger: Dict[str, dict] = {}
|
|
18
|
+
|
|
19
|
+
# --- HELPER FUNCTIONS ---
|
|
20
|
+
def load_accounts():
|
|
21
|
+
try:
|
|
22
|
+
with open(LEDGER_FILE, "r") as f:
|
|
23
|
+
return json.load(f)
|
|
24
|
+
except FileNotFoundError:
|
|
25
|
+
return {}
|
|
26
|
+
|
|
27
|
+
def save_accounts(data):
|
|
28
|
+
with open(LEDGER_FILE, "w") as f:
|
|
29
|
+
json.dump(data, f, indent=4)
|
|
30
|
+
|
|
31
|
+
def validate_api_key(api_key: str):
|
|
32
|
+
# Load your ledger
|
|
33
|
+
with open("registry/accounts.json", "r") as f:
|
|
34
|
+
accounts = json.load(f)
|
|
35
|
+
|
|
36
|
+
# Check 1: Does key exist?
|
|
37
|
+
if api_key not in accounts:
|
|
38
|
+
raise HTTPException(status_code=401, detail="Invalid API Key")
|
|
39
|
+
|
|
40
|
+
# Check 2: Is the account enabled?
|
|
41
|
+
user = accounts[api_key]
|
|
42
|
+
if not user.get("active", False):
|
|
43
|
+
raise HTTPException(status_code=403, detail="Account suspended")
|
|
44
|
+
|
|
45
|
+
# Check 3: Does it have money?
|
|
46
|
+
if user.get("balance", 0) <= 0:
|
|
47
|
+
raise HTTPException(status_code=402, detail="Insufficient funds")
|
|
48
|
+
|
|
49
|
+
return user
|
|
50
|
+
|
|
51
|
+
# --- MODELS ---
|
|
52
|
+
class AgentRegistration(BaseModel):
|
|
53
|
+
did: str
|
|
54
|
+
endpoint: str
|
|
55
|
+
capabilities: List[str]
|
|
56
|
+
|
|
57
|
+
class SessionRequest(BaseModel):
|
|
58
|
+
payer_did: str
|
|
59
|
+
target_did: str
|
|
60
|
+
capability: str
|
|
61
|
+
|
|
62
|
+
# --- ENDPOINTS ---
|
|
63
|
+
|
|
64
|
+
@app.post("/register")
|
|
65
|
+
async def register_agent(agent: AgentRegistration):
|
|
66
|
+
for cap in agent.capabilities:
|
|
67
|
+
if cap not in service_registry:
|
|
68
|
+
service_registry[cap] = []
|
|
69
|
+
# Update existing agent or add new
|
|
70
|
+
service_registry[cap] = [a for a in service_registry[cap] if a['did'] != agent.did]
|
|
71
|
+
service_registry[cap].append(agent.dict())
|
|
72
|
+
|
|
73
|
+
print(f"ā
[Registry] Registered {agent.did}")
|
|
74
|
+
return {"status": "registered"}
|
|
75
|
+
|
|
76
|
+
@app.get("/discover")
|
|
77
|
+
async def discover(capability: str):
|
|
78
|
+
agents = service_registry.get(capability, [])
|
|
79
|
+
if not agents:
|
|
80
|
+
raise HTTPException(404, "No agents found")
|
|
81
|
+
return {"agents": agents}
|
|
82
|
+
|
|
83
|
+
@app.post("/handshake")
|
|
84
|
+
async def handshake(req: SessionRequest, x_api_key: Optional[str] = Header(None)):
|
|
85
|
+
"""
|
|
86
|
+
š° PAYMENT LOGIC:
|
|
87
|
+
1. Check Key
|
|
88
|
+
2. Check Balance
|
|
89
|
+
3. Deduct Fee
|
|
90
|
+
4. Issue Token
|
|
91
|
+
"""
|
|
92
|
+
# 1. AUTH
|
|
93
|
+
if not x_api_key:
|
|
94
|
+
raise HTTPException(401, "Missing API Key")
|
|
95
|
+
|
|
96
|
+
accounts = load_accounts()
|
|
97
|
+
if x_api_key not in accounts:
|
|
98
|
+
raise HTTPException(403, "Invalid API Key")
|
|
99
|
+
|
|
100
|
+
user_account = accounts[x_api_key]
|
|
101
|
+
|
|
102
|
+
if not user_account["active"]:
|
|
103
|
+
raise HTTPException(403, "Account Suspended")
|
|
104
|
+
|
|
105
|
+
# 2. CHECK BALANCE
|
|
106
|
+
if user_account["balance"] < MVP_PRICE_INR:
|
|
107
|
+
raise HTTPException(402, "Insufficient Balance. Please Top Up.")
|
|
108
|
+
|
|
109
|
+
# 3. CHARGE
|
|
110
|
+
user_account["balance"] -= MVP_PRICE_INR
|
|
111
|
+
save_accounts(accounts) # Save to disk
|
|
112
|
+
|
|
113
|
+
print(f"š° [Billing] Charged {x_api_key[:10]}... | New Balance: ā¹{user_account['balance']}")
|
|
114
|
+
|
|
115
|
+
# 4. ISSUE TOKEN
|
|
116
|
+
txn_id = f"txn_{int(time.time())}"
|
|
117
|
+
payload = {
|
|
118
|
+
"iss": "aris-registry",
|
|
119
|
+
"sub": req.payer_did,
|
|
120
|
+
"aud": req.target_did,
|
|
121
|
+
"scope": req.capability,
|
|
122
|
+
"txn_id": txn_id,
|
|
123
|
+
"exp": time.time() + 300 # 5 mins
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
token = jwt.encode(payload, ARIS_PRIVATE_KEY, algorithm="HS256")
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
"session_token": token,
|
|
130
|
+
"txn_id": txn_id,
|
|
131
|
+
"remaining_balance": user_account["balance"]
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
class TopUpRequest(BaseModel):
|
|
135
|
+
api_key: str
|
|
136
|
+
amount: float
|
|
137
|
+
|
|
138
|
+
@app.post("/admin/topup")
|
|
139
|
+
async def top_up(req: TopUpRequest):
|
|
140
|
+
accounts = load_accounts()
|
|
141
|
+
if req.api_key not in accounts:
|
|
142
|
+
raise HTTPException(404, "Key not found")
|
|
143
|
+
|
|
144
|
+
accounts[req.api_key]["balance"] += req.amount
|
|
145
|
+
save_accounts(accounts)
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
"status": "success",
|
|
149
|
+
"new_balance": accounts[req.api_key]["balance"]
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if __name__ == "__main__":
|
|
153
|
+
import uvicorn
|
|
154
|
+
# This actually starts the server
|
|
155
|
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import secrets
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import argparse
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
ACCOUNTS_FILE = "registry/accounts.json"
|
|
8
|
+
|
|
9
|
+
def mint_key(email: str, balance: float = 100.0):
|
|
10
|
+
"""Business logic to create the key and update the ledger."""
|
|
11
|
+
new_key = f"sk-aris-{secrets.token_hex(16)}"
|
|
12
|
+
|
|
13
|
+
# Ensure directory exists
|
|
14
|
+
os.makedirs(os.path.dirname(ACCOUNTS_FILE), exist_ok=True)
|
|
15
|
+
|
|
16
|
+
if os.path.exists(ACCOUNTS_FILE):
|
|
17
|
+
with open(ACCOUNTS_FILE, "r") as f:
|
|
18
|
+
accounts = json.load(f)
|
|
19
|
+
else:
|
|
20
|
+
accounts = {}
|
|
21
|
+
|
|
22
|
+
accounts[new_key] = {
|
|
23
|
+
"user_email": email,
|
|
24
|
+
"balance": balance,
|
|
25
|
+
"active": True,
|
|
26
|
+
"created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
with open(ACCOUNTS_FILE, "w") as f:
|
|
30
|
+
json.dump(accounts, f, indent=4)
|
|
31
|
+
|
|
32
|
+
print(f"\n⨠Aris Key Minted Successfully!")
|
|
33
|
+
print(f"š§ User: {email}")
|
|
34
|
+
print(f"š° Balance: ā¹{balance}")
|
|
35
|
+
print(f"š Key: {new_key}")
|
|
36
|
+
print(f"--------------------------------------------------")
|
|
37
|
+
return new_key
|
|
38
|
+
|
|
39
|
+
def main():
|
|
40
|
+
"""CLI Entrypoint for the aris-admin command."""
|
|
41
|
+
parser = argparse.ArgumentParser(description="Aris Network Admin Tools")
|
|
42
|
+
parser.add_argument("--email", help="Email of the user")
|
|
43
|
+
parser.add_argument("--balance", type=float, default=100.0, help="Starting balance")
|
|
44
|
+
args = parser.parse_args()
|
|
45
|
+
|
|
46
|
+
# If user didn't provide an email via flags, ask interactively
|
|
47
|
+
email = args.email or input("Enter user email: ")
|
|
48
|
+
balance = args.balance if args.email else (input("Enter starting balance (default 100.0): ") or 100.0)
|
|
49
|
+
|
|
50
|
+
mint_key(email, float(balance))
|
|
51
|
+
|
|
52
|
+
if __name__ == "__main__":
|
|
53
|
+
main()
|
|
File without changes
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import redis.asyncio as redis
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
|
|
4
|
+
# Connect to Redis
|
|
5
|
+
redis_client = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
|
|
6
|
+
|
|
7
|
+
class SearchEngine:
|
|
8
|
+
"""
|
|
9
|
+
The 'Google' of Aris.
|
|
10
|
+
Uses Inverted Indices in Redis to find agents by capability instantly.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
@staticmethod
|
|
14
|
+
async def index_agent(did: str, capabilities: List[str]):
|
|
15
|
+
"""
|
|
16
|
+
Updates the Inverted Index.
|
|
17
|
+
If Agent A has 'video', we add Agent A to the 'idx:video' set.
|
|
18
|
+
"""
|
|
19
|
+
async with redis_client.pipeline() as pipe:
|
|
20
|
+
# 1. Update the 'Last Seen' timestamp
|
|
21
|
+
pipe.hset(f"agent:{did}", mapping={"last_seen": "now"})
|
|
22
|
+
|
|
23
|
+
# 2. Index each capability
|
|
24
|
+
for cap in capabilities:
|
|
25
|
+
# Key: idx:media.video -> Set{did:agent:1, did:agent:2}
|
|
26
|
+
pipe.sadd(f"idx:{cap.lower()}", did)
|
|
27
|
+
|
|
28
|
+
await pipe.execute()
|
|
29
|
+
|
|
30
|
+
@staticmethod
|
|
31
|
+
async def search(capability: str, limit: int = 10) -> List[dict]:
|
|
32
|
+
"""
|
|
33
|
+
Finds agents that match the capability.
|
|
34
|
+
Returns full agent details.
|
|
35
|
+
"""
|
|
36
|
+
# 1. Get all DIDs from the index (0ms latency)
|
|
37
|
+
dids = await redis_client.smembers(f"idx:{capability.lower()}")
|
|
38
|
+
|
|
39
|
+
if not dids:
|
|
40
|
+
return []
|
|
41
|
+
|
|
42
|
+
# 2. Fetch details for all found agents in parallel
|
|
43
|
+
results = []
|
|
44
|
+
for did in list(dids)[:limit]:
|
|
45
|
+
# In a real app, use mget for even more speed
|
|
46
|
+
data = await redis_client.hgetall(f"agent:{did}")
|
|
47
|
+
if data:
|
|
48
|
+
# Add the DID to the response object since it's the key
|
|
49
|
+
data['did'] = did
|
|
50
|
+
results.append(data)
|
|
51
|
+
|
|
52
|
+
return results
|
|
53
|
+
|
|
54
|
+
@staticmethod
|
|
55
|
+
async def remove_agent(did: str, capabilities: List[str]):
|
|
56
|
+
"""
|
|
57
|
+
Removes an agent from the index (e.g., if they go offline).
|
|
58
|
+
"""
|
|
59
|
+
for cap in capabilities:
|
|
60
|
+
await redis_client.srem(f"idx:{cap.lower()}", did)
|
aris_sdk-0.1.0/setup.cfg
ADDED
aris_sdk-0.1.0/setup.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name="aris-sdk",
|
|
5
|
+
version="0.1.0",
|
|
6
|
+
description="Decentralized AI Network SDK and Node Infrastructure",
|
|
7
|
+
author="Sid",
|
|
8
|
+
packages=find_packages(include=[
|
|
9
|
+
"aris", "aris.*",
|
|
10
|
+
"agent_node", "agent_node.*",
|
|
11
|
+
"registry", "registry.*"
|
|
12
|
+
]),
|
|
13
|
+
install_requires=[
|
|
14
|
+
"requests",
|
|
15
|
+
"pydantic",
|
|
16
|
+
"python-jose[cryptography]",
|
|
17
|
+
"uvicorn",
|
|
18
|
+
"fastapi",
|
|
19
|
+
"httpx",
|
|
20
|
+
"pyjwt",
|
|
21
|
+
"python-dotenv"
|
|
22
|
+
],
|
|
23
|
+
entry_points={
|
|
24
|
+
"console_scripts": [
|
|
25
|
+
"aris-node=agent_node.llm_agent:main",
|
|
26
|
+
"aris-admin=registry.mint_key:main",
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
python_requires=">=3.8",
|
|
30
|
+
)
|