diff --git a/integrations/langchain-rustchain/README.md b/integrations/langchain-rustchain/README.md new file mode 100644 index 000000000..9908ee92e --- /dev/null +++ b/integrations/langchain-rustchain/README.md @@ -0,0 +1,46 @@ +# langchain-rustchain + +LangChain tools for the [RustChain](https://rustchain.org) DePIN blockchain — Proof of Antiquity mining on vintage hardware. + +## Quick Start + +```bash +pip install langchain-rustchain +``` + +Then use with any LangChain agent: + +```python +from langchain_rustchain import get_tools + +tools = get_tools() +agent = create_react_agent(llm, tools) # or any LangChain agent +``` + +## Tools + +| Tool | Description | Source | +|------|-------------|--------| +| `rustchain_check_balance` | Check RTC balance of any wallet (`/wallet/balance`) | Node API | +| `rustchain_list_bounties` | List open ecosystem bounties (GitHub issues) | GitHub API | +| `rustchain_get_node_health` | Read node health status (`/health`) | Node API | +| `rustchain_get_current_epoch` | Read current epoch / slot (`/epoch`) | Node API | + +All tools are read-only, no auth required, wallet = any string. + +## Why RustChain? + +- **Agent-native**: no auth, no captcha, wallet = any string, same-day RTC payout. +- **DePIN for vintage hardware**: old machines outmine new ones (Proof of Antiquity). +- **Solana bridge (wRTC)**: cross-chain liquidity. +- 55+ ecosystem repos, 5+ languages, 15+ CPU architectures. + +## Example + +See [example.py](example.py) for a complete agent script. + +## Links + +- [RustChain](https://rustchain.org) +- [Bounties](https://github.com/Scottcjn/rustchain-bounties) +- [Source](https://github.com/Scottcjn/Rustchain) \ No newline at end of file diff --git a/integrations/langchain-rustchain/example.py b/integrations/langchain-rustchain/example.py new file mode 100644 index 000000000..289964446 --- /dev/null +++ b/integrations/langchain-rustchain/example.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +"""Example: Use the RustChain LangChain tools to check a wallet balance +and list open bounties. + +Usage: + pip install langchain-rustchain + python example.py + +This script works with any LangChain-compatible LLM (OpenAI, Anthropic, +Ollama, etc.) or you can just call the tools directly without an LLM. +""" + +from langchain_rustchain import ( + RustChainBalanceTool, + RustChainBountiesTool, + RustChainCurrentEpochTool, + RustChainNodeHealthTool, +) + + +def direct_tool_demo(): + """Demonstrate each tool by calling it directly (no LLM needed).""" + + print("=" * 60) + print("RustChain LangChain Tools — Direct Demo") + print("=" * 60) + + # 1. Node health + print("\n--- 1. Node Health ---") + health = RustChainNodeHealthTool()._run() + print(health) + + # 2. Current epoch + print("\n--- 2. Current Epoch ---") + epoch = RustChainCurrentEpochTool()._run() + print(epoch) + + # 3. Wallet balance (any string wallet) + print("\n--- 3. Wallet Balance (test wallet) ---") + balance = RustChainBalanceTool()._run("test") + print(balance) + + # 4. Open bounties + print("\n--- 4. Open Bounties (top 5) ---") + bounties = RustChainBountiesTool()._run(limit=5) + print(bounties) + + print("\n" + "=" * 60) + print("All tools work. Integrate into your LangChain agent with:") + print(" from langchain_rustchain import get_tools") + print(" tools = get_tools()") + print("=" * 60) + + +if __name__ == "__main__": + direct_tool_demo() \ No newline at end of file diff --git a/integrations/langchain-rustchain/langchain_rustchain/__init__.py b/integrations/langchain-rustchain/langchain_rustchain/__init__.py new file mode 100644 index 000000000..cc3e71474 --- /dev/null +++ b/integrations/langchain-rustchain/langchain_rustchain/__init__.py @@ -0,0 +1,26 @@ +# SPDX-License-Identifier: MIT +"""RustChain LangChain integration — check balances, list bounties, inspect node health, read current epoch. + +Exposes ``RustChainBalanceTool``, ``RustChainBountiesTool``, +``RustChainNodeHealthTool``, and ``RustChainCurrentEpochTool`` as +LangChain ``BaseTool`` subclasses, plus a convenience ``get_tools()`` +factory and ``ALL_RUSTCHAIN_TOOLS`` list. +""" + +from langchain_rustchain.tools import ( + ALL_RUSTCHAIN_TOOLS, + RustChainBalanceTool, + RustChainBountiesTool, + RustChainCurrentEpochTool, + RustChainNodeHealthTool, + get_tools, +) + +__all__ = [ + "RustChainBalanceTool", + "RustChainBountiesTool", + "RustChainNodeHealthTool", + "RustChainCurrentEpochTool", + "ALL_RUSTCHAIN_TOOLS", + "get_tools", +] \ No newline at end of file diff --git a/integrations/langchain-rustchain/langchain_rustchain/tools.py b/integrations/langchain-rustchain/langchain_rustchain/tools.py new file mode 100644 index 000000000..3aa51533f --- /dev/null +++ b/integrations/langchain-rustchain/langchain_rustchain/tools.py @@ -0,0 +1,236 @@ +# SPDX-License-Identifier: MIT +"""RustChain LangChain tools. + +A LangChain ``BaseTool`` exposing read-only RustChain node endpoints so an +LLM agent can check wallet balances, list ecosystem bounties, inspect node +health, and read the current epoch — all against the public RustChain node +(https://rustchain.org, wallet = any string, no auth). + +Dependencies: ``langchain-core`` and ``httpx`` (or ``requests``). +""" + +from __future__ import annotations + +import json +import os +from typing import Any, Dict, List, Optional + +from langchain_core.tools import BaseTool +from pydantic import BaseModel, Field + +try: # httpx preferred (async-capable), fall back to requests + import httpx + + _HTTPX = True +except ImportError: # pragma: no cover - environment fallback + httpx = None # type: ignore[assignment] + _HTTPX = False + import requests + +DEFAULT_NODE = "https://rustchain.org" +DEFAULT_BOUNTIES_REPO = "Scottcjn/rustchain-bounties" +TIMEOUT_S = 15.0 + + +class _Client: + """Tiny sync HTTP helper so the tools don't drag in a full SDK.""" + + def __init__(self, node_url: str, timeout: float = TIMEOUT_S, headers: Optional[Dict[str, str]] = None): + self.node_url = node_url.rstrip("/") + self.timeout = timeout + self.headers = headers or {} + + def get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + url = f"{self.node_url}{path}" + try: + if _HTTPX: + resp = httpx.get(url, params=params, headers=self.headers, timeout=self.timeout) + else: + resp = requests.get(url, params=params, headers=self.headers, timeout=self.timeout) + resp.raise_for_status() + except Exception as exc: + return {"ok": False, "error": str(exc)} + try: + return resp.json() + except ValueError: + return {"ok": False, "error": "non_json_response", "status": resp.status_code} + + +# --------------------------------------------------------------------------- +# Schemas (pydantic) — one input schema per tool improves tool-call reliability. +# --------------------------------------------------------------------------- + +class CheckBalanceInput(BaseModel): + wallet_id: str = Field( + description="The RustChain wallet id / miner address to look up. " + "Any string wallet is accepted (agent-native, no auth)." + ) + + +class ListBountiesInput(BaseModel): + limit: int = Field( + default=10, + ge=1, + le=50, + description="Maximum number of open bounties to return.", + ) + + +class GetNodeHealthInput(BaseModel): + pass + + +class GetCurrentEpochInput(BaseModel): + pass + + +# --------------------------------------------------------------------------- +# Tools +# --------------------------------------------------------------------------- + +class RustChainBalanceTool(BaseTool): + """Check the RTC balance of a RustChain wallet.""" + + name: str = "rustchain_check_balance" + description: str = ( + "Check the RTC balance of a RustChain wallet id. " + "Returns the balance in RTC (and the raw integer amount). " + "Use for questions like 'how much RTC does wallet X hold?'." + ) + args_schema: type[BaseModel] = CheckBalanceInput + + node_url: str = DEFAULT_NODE + + def _run(self, wallet_id: str) -> str: + client = _Client(self.node_url) + data = client.get( + "/wallet/balance", params={"miner_id": wallet_id} + ) + if not data.get("ok", True): + return json.dumps({"ok": False, "error": data.get("error", "unknown")}) + return json.dumps( + { + "ok": True, + "wallet_id": data.get("miner_id", wallet_id), + "balance_rtc": data.get("amount_rtc", 0.0), + "balance_raw": data.get("amount_i64", 0), + } + ) + + +class RustChainBountiesTool(BaseTool): + """List open RustChain ecosystem bounties.""" + + name: str = "rustchain_list_bounties" + description: str = ( + "List currently open RustChain ecosystem bounties (Earn RTC by " + "contributing: code, docs, security, community). Returns the most " + "recent open bounties with their issue number, title, and labels." + ) + args_schema: type[BaseModel] = ListBountiesInput + + bounties_repo: str = DEFAULT_BOUNTIES_REPO + + def _run(self, limit: int = 10) -> str: + # Bounties are tracked as GitHub issues labelled ``bounty`` in the + # rustchain-bounties repo. Use the public GitHub REST API. + # Reads GH_TOKEN or GITHUB_TOKEN from the environment for higher + # rate limits (optional — the endpoint is unauthenticated-friendly). + headers = {} + token = os.environ.get("GH_TOKEN") or os.environ.get("GITHUB_TOKEN") or "" + if token: + headers["Authorization"] = f"Bearer {token}" + client = _Client("https://api.github.com", headers=headers) + data = client.get( + f"/repos/{self.bounties_repo}/issues", + params={"state": "open", "labels": "bounty", "per_page": limit}, + ) + if isinstance(data, list): + items = [] + for issue in data: + items.append( + { + "number": issue.get("number"), + "title": issue.get("title"), + "url": issue.get("html_url"), + "labels": [ + lbl.get("name") + for lbl in issue.get("labels", []) + ], + } + ) + return json.dumps({"ok": True, "count": len(items), "bounties": items}) + return json.dumps({"ok": False, "error": str(data)}) + + +class RustChainNodeHealthTool(BaseTool): + """Read RustChain node health status.""" + + name: str = "rustchain_get_node_health" + description: str = ( + "Get the current health status of the RustChain public node: " + "database read/write availability, tip age, uptime, and version. " + "Use to answer 'is RustChain up / healthy?'." + ) + args_schema: type[BaseModel] = GetNodeHealthInput + + node_url: str = DEFAULT_NODE + + def _run(self) -> str: + client = _Client(self.node_url) + data = client.get("/health") + return json.dumps(data) + + +class RustChainCurrentEpochTool(BaseTool): + """Read the current RustChain epoch.""" + + name: str = "rustchain_get_current_epoch" + description: str = ( + "Get the current RustChain epoch, current slot, enrolled miner count, " + "and per-epoch RTC emission. Use for questions about the current " + "consensus epoch / mining round." + ) + args_schema: type[BaseModel] = GetCurrentEpochInput + + node_url: str = DEFAULT_NODE + + def _run(self) -> str: + client = _Client(self.node_url) + data = client.get("/epoch") + return json.dumps(data) + + +# Handy collection for loading all tools at once. +ALL_RUSTCHAIN_TOOLS: List[BaseTool] = [ + RustChainBalanceTool(), + RustChainBountiesTool(), + RustChainNodeHealthTool(), + RustChainCurrentEpochTool(), +] + + +def get_tools(node_url: str = DEFAULT_NODE, bounties_repo: str = DEFAULT_BOUNTIES_REPO) -> List[BaseTool]: + """Return a fresh list of all RustChain tools with custom endpoints. + + Args: + node_url: RustChain node base URL (defaults to the public node). + bounties_repo: ``owner/repo`` of the bounty issue tracker + (defaults to Scottcjn/rustchain-bounties). + """ + return [ + RustChainBalanceTool(node_url=node_url), + RustChainBountiesTool(bounties_repo=bounties_repo), + RustChainNodeHealthTool(node_url=node_url), + RustChainCurrentEpochTool(node_url=node_url), + ] + + +__all__ = [ + "RustChainBalanceTool", + "RustChainBountiesTool", + "RustChainNodeHealthTool", + "RustChainCurrentEpochTool", + "ALL_RUSTCHAIN_TOOLS", + "get_tools", +] \ No newline at end of file diff --git a/integrations/langchain-rustchain/pyproject.toml b/integrations/langchain-rustchain/pyproject.toml new file mode 100644 index 000000000..db8494a84 --- /dev/null +++ b/integrations/langchain-rustchain/pyproject.toml @@ -0,0 +1,40 @@ +[build-system] +requires = ["setuptools>=64", "wheel"] +build-backend = "setuptools.backends._legacy:_Backend" + +[project] +name = "langchain-rustchain" +version = "0.1.0" +description = "LangChain integration for RustChain — check balances, list bounties, node health, current epoch" +readme = "README.md" +license = {text = "MIT"} +requires-python = ">=3.9" +authors = [ + {name = "RustChain Contributors", email = "oss@rustchain.org"}, +] +dependencies = [ + "langchain-core>=0.1.0", + "httpx>=0.27.0", +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Software Development :: Libraries :: Python Modules", +] + +[project.urls] +Homepage = "https://rustchain.org" +Source = "https://github.com/Scottcjn/Rustchain" +BugTracker = "https://github.com/Scottcjn/rustchain-bounties/issues" + +[project.optional-dependencies] +requests = ["requests>=2.31.0"] + +[tool.setuptools.packages.find] +include = ["langchain_rustchain*"] \ No newline at end of file