import os, json, time, sqlite3, requests, io, csv import numpy as np import gradio as gr from typing import List, Dict from sentence_transformers import SentenceTransformer # ---- DAO CONFIG ---- DAO_ADDRESS = os.environ.get("DAO_ADDRESS", "0xE2F60eEEd806Cb2790c0685334D0b95417c386E0") FIELD_TOKEN = os.environ.get("FIELD_TOKEN", "0xcBC6309dd6c79C9210cB3DBc014d43205A92BbC8") ARBITRUM_RPC = os.environ.get("ARBITRUM_RPC", "https://arb1.arbitrum.io/rpc") TELEMETRY_WEBHOOK = os.environ.get("TELEMETRY_WEBHOOK", "http://0.0.0.0:8080") # point to FastAPI server /usage PREMIUM_KEY = os.environ.get("PREMIUM_KEY", "") FREE_LIMIT = int(os.environ.get("FREE_LIMIT", "20")) DB_PATH = os.environ.get("DB_PATH", "inneri_reskill.db") EMBED_MODEL = os.environ.get("EMBED_MODEL", "sentence-transformers/all-MiniLM-L6-v2") TOP_K = int(os.environ.get("TOP_K", "5")) def db(): conn = sqlite3.connect(DB_PATH) conn.execute("PRAGMA foreign_keys = ON;") return conn def init_db(): conn = db(); cur = conn.cursor() cur.execute("CREATE TABLE IF NOT EXISTS users(user_id TEXT PRIMARY KEY, balance_cents INTEGER DEFAULT 0);") cur.execute("CREATE TABLE IF NOT EXISTS guides(id INTEGER PRIMARY KEY AUTOINCREMENT, owner_id TEXT, title TEXT, text TEXT, tags TEXT, namespace TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);") cur.execute("CREATE TABLE IF NOT EXISTS embeds(id INTEGER PRIMARY KEY AUTOINCREMENT, guide_id INTEGER, namespace TEXT, dim INTEGER, vec BLOB, FOREIGN KEY(guide_id) REFERENCES guides(id) ON DELETE CASCADE);") cur.execute("CREATE TABLE IF NOT EXISTS traces(id INTEGER PRIMARY KEY AUTOINCREMENT, trace_id TEXT, user_id TEXT, question TEXT, answer TEXT, sources TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);") cur.execute("CREATE TABLE IF NOT EXISTS usage_credits(user_id TEXT, guide_id INTEGER, credits REAL, period TEXT);") conn.commit(); conn.close() def ensure_user(user_id:str): conn = db(); cur = conn.cursor() cur.execute("INSERT OR IGNORE INTO users(user_id,balance_cents) VALUES(?,0)", (user_id,)) conn.commit(); conn.close() _embedder = None def get_embedder(): global _embedder if _embedder is None: _embedder = SentenceTransformer(EMBED_MODEL) return _embedder def embed_texts(texts: List[str]): return np.array(get_embedder().encode(texts, show_progress_bar=False, normalize_embeddings=True), dtype=np.float32) def vec_to_blob(vec: np.ndarray) -> bytes: return vec.astype(np.float32).tobytes() def blob_to_vec(blob: bytes) -> np.ndarray: return np.frombuffer(blob, dtype=np.float32) def add_guide(owner_id:str, title:str, text:str, tags:str, namespace:str): ensure_user(owner_id) conn = db(); cur = conn.cursor() cur.execute("INSERT INTO guides(owner_id,title,text,tags,namespace) VALUES(?,?,?,?,?)",(owner_id, title, text, tags, namespace)) gid = cur.lastrowid vec = embed_texts([text])[0] cur.execute("INSERT INTO embeds(guide_id,namespace,dim,vec) VALUES(?,?,?,?)",(gid, namespace, vec.shape[0], vec_to_blob(vec))) conn.commit(); conn.close() return gid def list_guides(namespace:str): conn = db(); cur = conn.cursor() cur.execute("SELECT id, owner_id, title, tags, created_at FROM guides WHERE namespace=? ORDER BY created_at DESC",(namespace,)) rows = cur.fetchall(); conn.close() return rows def retrieve(question:str, namespace:str, top_k:int=TOP_K)->List[Dict]: qv = embed_texts([question])[0] conn = db(); cur = conn.cursor() cur.execute("SELECT e.id, e.guide_id, e.vec, g.title, g.owner_id, g.text FROM embeds e JOIN guides g ON e.guide_id=g.id WHERE e.namespace=?",(namespace,)) rows = cur.fetchall(); conn.close() if not rows: return [] mat = np.stack([blob_to_vec(r[2]) for r in rows]) sims = mat @ qv idx = np.argsort(-sims)[:top_k] results = [] for i in idx: _, gid, _, title, owner, text = rows[int(i)] results.append({"guide_id": int(gid), "owner": owner, "title": title or "Untitled", "text": text, "score": float(sims[int(i)])}) return results def synthesize_answer(question:str, contexts:List[Dict])->str: bullets = [] for i,c in enumerate(contexts): snippet = c["text"][:400].replace("\n"," ").strip() bullets.append(f"[{i+1}] {c['title']}: {snippet}") return "Q: " + question + "\n\nGuides:\n" + "\n".join(bullets) def record_usage(ctx:List[Dict], period:str): conn = db(); cur = conn.cursor() total = sum(max(0.0, c["score"]) for c in ctx) or 1.0 for c in ctx: w = max(0.0, c["score"])/total cur.execute("INSERT INTO usage_credits(user_id, guide_id, credits, period) VALUES(?,?,?,?)", (c["owner"], c["guide_id"], w, period)) conn.commit(); conn.close() def payout_csv(period:str, total_field:int=50000): conn = db(); cur = conn.cursor() cur.execute("SELECT user_id, SUM(credits) FROM usage_credits WHERE period=? GROUP BY user_id", (period,)) rows = cur.fetchall(); conn.close() total = sum(r[1] for r in rows) or 1.0 lines = ["recipient_address,amount_FIELD,reason,period"] for user, credits in rows: amt = int(round(total_field * (credits/total))) lines.append(f"{user},{amt},Guide usage,{period}") return "\n".join(lines) def dao_panel_read(field_token:str): out = {"dao_address": DAO_ADDRESS, "field_token": field_token or "(set FIELD_TOKEN)", "balances": {}, "notes": ""} try: from web3 import Web3 ERC20_ABI = [ {"constant":True,"inputs":[{"name":"a","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"type":"function"}, {"constant":True,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"type":"function"}, {"constant":True,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"type":"function"}, {"constant":True,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"type":"function"} ] w3 = Web3(Web3.HTTPProvider(ARBITRUM_RPC, request_kwargs={"timeout": 10})) if not field_token: out["notes"] = "Set FIELD_TOKEN to read balances." return json.dumps(out, indent=2) token = w3.eth.contract(address=w3.to_checksum_address(field_token), abi=ERC20_ABI) dec = token.functions.decimals().call() sym = token.functions.symbol().call() ts = token.functions.totalSupply().call() / (10**dec) bal = token.functions.balanceOf(w3.to_checksum_address(DAO_ADDRESS)).call() / (10**dec) out["balances"] = {"symbol": sym, "decimals": dec, "total_supply": ts, "dao_balance": bal} except Exception as e: out["notes"] = f"RPC/ABI error: {e}" return json.dumps(out, indent=2) def generate_actions_from_csv(csv_text:str, token_address:str, decimals:int=18): try: from web3 import Web3 from eth_abi import encode as abi_encode except Exception as e: return "Missing dependency: web3/eth_abi. Ensure requirements are installed." rows = list(csv.reader(io.StringIO(csv_text.strip()))) header = [h.strip().lower() for h in rows[0]] if header[:2] != ["recipient_address","amount_field"]: return "CSV must start with columns: recipient_address,amount_FIELD,..." actions = [] selector = Web3.keccak(text="transfer(address,uint256)")[:4] w3 = Web3() for r in rows[1:]: if not r or len(r)<2: continue to = w3.to_checksum_address(r[0].strip()) amt = int(r[1].strip()) wei = amt * (10**decimals) data = "0x" + (selector + abi_encode(["address","uint256"], [to, wei])).hex() actions.append({"to": token_address, "value": 0, "data": data}) return json.dumps(actions, indent=2) SESSION_QUERIES = {} def log(evt, payload): try: if TELEMETRY_WEBHOOK: requests.post(TELEMETRY_WEBHOOK, json={"evt":evt, **payload}, timeout=5) except Exception: pass def ask(user_id:str, question:str, namespace:str, premium_key:str, period:str): sid = user_id or "anon" count = SESSION_QUERIES.get(sid, 0) if (not PREMIUM_KEY) or (premium_key != PREMIUM_KEY): if count >= FREE_LIMIT: return f"Free limit reached ({FREE_LIMIT}). Enter premium key.", "", "" SESSION_QUERIES[sid] = count + 1 ctx = retrieve(question, namespace) if not ctx: return "No guides yet. Add some first.", "", "" ans = synthesize_answer(question, ctx) record_usage(ctx, period) log("ask", {"user": sid, "ns": namespace, "period": period, "guide_ids": [c["guide_id"] for c in ctx]}) src = "\n".join([f"[{i+1}] {c['title']} (id {c['guide_id']}) {c['score']:.2f}" for i,c in enumerate(ctx)]) return ans, src, str(int(time.time()*1000)) SEED_NAMESPACE = "inneri-guides" SEED_GUIDES = [ ("seed","Prompt Clarity: 3 Moves","1) State the goal clearly.\n2) Add 1–3 constraints.\n3) Give a short example.","prompt,basics"), ("seed","RAG Basics: Store & Retrieve","Chunk docs, embed, retrieve, generate with citations.","rag,basics"), ("seed","Chunking That Works","Overlap 50–100 tokens, respect headings, avoid >1k tokens.","rag,chunking"), ("seed","Choosing Embedding Models","MiniLM for speed; e5/bge for quality; normalize; version.","embeddings,models"), ("seed","Eval Signals","Score faithfulness, relevance, actionability.","evals,quality"), ("seed","Langfuse Setup","Log routes, latency; attach evals; dashboard triage.","observability,langfuse"), ("seed","Agentic Loops","Plan → Act → Observe → Reflect; timeouts; state; kill switch.","agents,loops"), ("seed","FastAPI for AI","/chat, /embed, /query; rate limit; metrics; costs.","ops,api"), ("seed","Data Rights & Payouts","Creators own docs; track usage; split fees.","legal,economics"), ("seed","Safety & Scope","Refuse dangerous asks; stay in domain; red-team.","safety,policy"), ] def seed_if_empty(): conn = db(); cur = conn.cursor() cur.execute("SELECT COUNT(*) FROM guides WHERE namespace=?",(SEED_NAMESPACE,)) if cur.fetchone()[0]==0: for o,t,tx,tg in SEED_GUIDES: add_guide(o,t,tx,tg,SEED_NAMESPACE) init_db(); seed_if_empty() with gr.Blocks(title="Reskill the Field — Inner I Agentic AI (DAO-Wired)") as demo: gr.Markdown(f"### Reskill the Field DAO — Inner I Agentic AI\nDAO: **{DAO_ADDRESS}** (Arbitrum) | Library + RAG + Payouts + Proposal Generator") with gr.Tab("DAO Panel"): token_in = gr.Textbox(label="$FIELD Token Address (ERC-20)", value=FIELD_TOKEN) read_btn = gr.Button("Read DAO Balances") read_out = gr.Textbox(label="DAO Balances (JSON)", lines=10) read_btn.click(lambda t: dao_panel_read(t), [token_in], [read_out]) gr.Markdown("Treasury note: target **1,000,000 $FIELD** held. Set `FIELD_TOKEN` to verify on-chain.") with gr.Tab("Browse Guides"): ns = gr.Textbox(label="Namespace", value=SEED_NAMESPACE) list_btn = gr.Button("List") table = gr.Dataframe(headers=["id","owner","title","tags","created"], datatype=["number","str","str","str","str"]) def _list(namespace): return [[r[0],r[1],r[2],r[3],r[4]] for r in list_guides(namespace)] list_btn.click(_list,[ns],[table]) with gr.Tab("Add Guide"): u = gr.Textbox(label="Your Handle (wallet address preferred for payouts)", value="0xYourWallet") title = gr.Textbox(label="Title", value="My New Guide") tags = gr.Textbox(label="Tags", value="custom") ns2 = gr.Textbox(label="Namespace", value=SEED_NAMESPACE) txt = gr.Textbox(label="Guide Text", lines=10) add_btn = gr.Button("Add Guide") add_out = gr.Textbox(label="Result") def _add(uid,title,text,tags,ns): if not text.strip(): return "Add some text." gid=add_guide(uid,title,text,tags,ns); return f"Added id={gid}" add_btn.click(_add,[u,title,txt,tags,ns2],[add_out]) with gr.Tab("Ask (RAG) + Usage Log"): u2=gr.Textbox(label="User ID (optional)", value="") q=gr.Textbox(label="Question", value="How do I set up eval signals?", lines=3) ns3=gr.Textbox(label="Namespace", value=SEED_NAMESPACE) period=gr.Textbox(label="Period (YYYY-MM)", value="2025-09") prem=gr.Textbox(label="Premium Key (optional)", value="") ask_btn=gr.Button("Ask") ans=gr.Textbox(label="Answer", lines=12) src=gr.Textbox(label="Sources", lines=8) trace=gr.Textbox(label="Trace ID") ask_btn.click(lambda uid,qq,ns,per,pk: ask(uid,qq,ns,pk,per), [u2,q,ns3,period,prem],[ans,src,trace]) with gr.Tab("Payout Builder (CSV)"): per=gr.Textbox(label="Period (YYYY-MM)", value="2025-09") total=gr.Number(label="Total $FIELD to distribute", value=50000, precision=0) build=gr.Button("Generate CSV") csv_out=gr.Textbox(label="CSV Preview", lines=12) build.click(lambda p,t: payout_csv(p, int(t)), [per,total],[csv_out]) with gr.Tab("Proposal Generator (Aragon Actions)"): gr.Markdown("Paste payout CSV → get ERC-20 `transfer()` actions JSON for Aragon's Custom action.") csv_in = gr.Textbox(label="Payout CSV", lines=12, value="recipient_address,amount_FIELD,reason,period\n0x0000000000000000000000000000000000000000,1000,Guide usage,2025-09") token_addr = gr.Textbox(label="$FIELD Token Address", value=FIELD_TOKEN) decimals = gr.Number(label="Token Decimals", value=18, precision=0) gen_btn = gr.Button("Generate Actions JSON") actions_out = gr.Textbox(label="Encoded Actions JSON", lines=14) gen_btn.click(lambda c,t,d: generate_actions_from_csv(c,t,int(d)), [csv_in, token_addr, decimals], [actions_out]) demo.launch()