892 lines
36 KiB
Python
892 lines
36 KiB
Python
"""Extended JAE-AI agent tools (Phase 3).
|
|
|
|
Adds 27 new tools across anonymous / operator / elite / admin tiers.
|
|
Imported at the end of agent_tools.py so it runs at app startup.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import re
|
|
import time
|
|
import subprocess
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import requests as req
|
|
|
|
from agent_tools import (
|
|
Tool, register, DATA_DIR, _load_json, cache_get, cache_set,
|
|
)
|
|
|
|
MEM_DIR = DATA_DIR / 'agent_memories'
|
|
MEM_DIR.mkdir(parents=True, exist_ok=True)
|
|
ALERTS_PATH = DATA_DIR / 'alerts.json'
|
|
GUESTBOOK_PATH = DATA_DIR / 'guestbook.json'
|
|
BROADCAST_PATH = DATA_DIR / 'broadcast.json'
|
|
|
|
MAX_MEMORIES_PER_WALLET = 100
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# Helpers
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
def _current_wallet() -> str | None:
|
|
"""Pull the authenticated wallet address from the request session."""
|
|
try:
|
|
from auth_routes import read_session
|
|
sess = read_session()
|
|
return sess.get('address') if sess else None
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def _is_admin(address: str | None) -> bool:
|
|
if not address:
|
|
return False
|
|
try:
|
|
from agent_tiers import ADMIN_WALLETS
|
|
return address in ADMIN_WALLETS
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
def _safe_read_json(path: Path, default: Any) -> Any:
|
|
try:
|
|
if path.exists():
|
|
with open(path) as f:
|
|
return json.load(f)
|
|
except Exception:
|
|
pass
|
|
return default
|
|
|
|
|
|
def _safe_write_json(path: Path, data: Any) -> bool:
|
|
try:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
tmp = path.with_suffix('.tmp')
|
|
with open(tmp, 'w') as f:
|
|
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
tmp.replace(path)
|
|
return True
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
def _search_nested(obj: Any, q: str, limit: int = 5) -> list[dict]:
|
|
"""Walk a JSON tree and return objects whose combined text contains q."""
|
|
q = q.lower()
|
|
results: list[dict] = []
|
|
|
|
def blob_of(item) -> str:
|
|
try:
|
|
return json.dumps(item, ensure_ascii=False)[:4000].lower()
|
|
except Exception:
|
|
return str(item).lower()
|
|
|
|
def walk(node, depth=0):
|
|
if len(results) >= limit or depth > 5:
|
|
return
|
|
if isinstance(node, dict):
|
|
if q in blob_of(node):
|
|
results.append(node)
|
|
return
|
|
for v in node.values():
|
|
walk(v, depth + 1)
|
|
elif isinstance(node, list):
|
|
for v in node:
|
|
walk(v, depth + 1)
|
|
if len(results) >= limit:
|
|
return
|
|
|
|
walk(obj)
|
|
return results
|
|
|
|
|
|
def _trim_addr(addr: str) -> str:
|
|
if not addr:
|
|
return ''
|
|
return addr[:4] + '…' + addr[-4:]
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# TIER: ANONYMOUS
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
def tool_search_unredacted(args: dict) -> dict:
|
|
query = (args.get('query') or '').strip()
|
|
if not query:
|
|
return {'error': 'query required'}
|
|
data = _load_json('unredacted.json')
|
|
if data is None:
|
|
return {'error': 'unredacted archive unavailable', 'results': []}
|
|
matches = _search_nested(data, query, limit=5)
|
|
out = []
|
|
for m in matches:
|
|
out.append({
|
|
'title': m.get('title') or m.get('name') or m.get('document') or m.get('id') or 'Untitled',
|
|
'collection': m.get('collection') or m.get('category') or m.get('source'),
|
|
'summary': (m.get('summary') or m.get('description') or '')[:300],
|
|
'url': m.get('url') or m.get('pdf') or '/depot/unredacted',
|
|
})
|
|
return {'results': out, 'count': len(out)}
|
|
|
|
|
|
def tool_search_crimescene(args: dict) -> dict:
|
|
query = (args.get('query') or '').strip()
|
|
if not query:
|
|
return {'error': 'query required'}
|
|
data = _load_json('crimescene.json')
|
|
if data is None:
|
|
return {'error': 'crimescene archive unavailable', 'results': []}
|
|
matches = _search_nested(data, query, limit=5)
|
|
out = []
|
|
for m in matches:
|
|
out.append({
|
|
'case': m.get('case') or m.get('title') or m.get('name'),
|
|
'year': m.get('year') or m.get('date'),
|
|
'summary': (m.get('summary') or m.get('description') or '')[:300],
|
|
'status': m.get('status'),
|
|
'url': m.get('url') or '/depot/crimescene',
|
|
})
|
|
return {'results': out, 'count': len(out)}
|
|
|
|
|
|
def tool_search_radar(args: dict) -> dict:
|
|
query = (args.get('query') or '').strip().lower()
|
|
if not query:
|
|
return {'error': 'query required'}
|
|
try:
|
|
from app import get_radar_items # type: ignore
|
|
items = get_radar_items() or []
|
|
except Exception as e:
|
|
return {'error': f'radar unavailable: {e}', 'results': []}
|
|
results = []
|
|
for it in items:
|
|
blob = f"{it.get('title','')} {it.get('summary','')} {it.get('source','')}".lower()
|
|
if query in blob:
|
|
results.append({
|
|
'title': it.get('title'),
|
|
'source': it.get('source'),
|
|
'published': it.get('published'),
|
|
'url': it.get('url'),
|
|
'summary': (it.get('summary') or '')[:250],
|
|
})
|
|
if len(results) >= 5:
|
|
break
|
|
return {'results': results, 'count': len(results)}
|
|
|
|
|
|
def tool_get_gov_domains_stats(args: dict) -> dict:
|
|
data = _load_json('govdomains.json')
|
|
if data is None:
|
|
return {'error': 'govdomains data unavailable'}
|
|
domains = data.get('domains') if isinstance(data, dict) else (data if isinstance(data, list) else [])
|
|
total = len(domains) if isinstance(domains, list) else 0
|
|
now = time.time()
|
|
recent = 0
|
|
if isinstance(domains, list):
|
|
for d in domains:
|
|
try:
|
|
added = d.get('added') or d.get('first_seen') or d.get('date')
|
|
if not added:
|
|
continue
|
|
if isinstance(added, (int, float)):
|
|
if now - added < 86400:
|
|
recent += 1
|
|
else:
|
|
# ISO string
|
|
import datetime as _dt
|
|
try:
|
|
ts = _dt.datetime.fromisoformat(str(added).replace('Z', '+00:00')).timestamp()
|
|
if now - ts < 86400:
|
|
recent += 1
|
|
except Exception:
|
|
pass
|
|
except Exception:
|
|
continue
|
|
return {
|
|
'total_domains': total,
|
|
'added_last_24h': recent,
|
|
'source': 'api/data/govdomains.json',
|
|
'url': '/depot/recon',
|
|
}
|
|
|
|
|
|
def tool_search_docs(args: dict) -> dict:
|
|
query = (args.get('query') or '').strip()
|
|
if not query:
|
|
return {'error': 'query required'}
|
|
combined = []
|
|
try:
|
|
u = tool_search_unredacted({'query': query}).get('results', [])
|
|
for r in u:
|
|
r['source'] = 'unredacted'
|
|
combined.extend(u)
|
|
except Exception:
|
|
pass
|
|
try:
|
|
c = tool_search_crimescene({'query': query}).get('results', [])
|
|
for r in c:
|
|
r['source'] = 'crimescene'
|
|
combined.extend(c)
|
|
except Exception:
|
|
pass
|
|
try:
|
|
from agent_tools import tool_get_sitrep
|
|
latest = tool_get_sitrep({'date': 'latest'})
|
|
if query.lower() in (latest.get('summary') or '').lower():
|
|
combined.append({
|
|
'source': 'sitrep',
|
|
'title': f"SITREP {latest.get('date','')}",
|
|
'summary': (latest.get('summary') or '')[:300],
|
|
'url': '/transmissions/sitrep',
|
|
})
|
|
except Exception:
|
|
pass
|
|
return {'results': combined[:10], 'count': len(combined[:10])}
|
|
|
|
|
|
def tool_get_server_status(args: dict) -> dict:
|
|
cached = cache_get('server_status', 15)
|
|
if cached is not None:
|
|
return cached
|
|
try:
|
|
from telemetry_snapshot import gather
|
|
snap = gather()
|
|
except Exception as e:
|
|
return {'error': f'telemetry unavailable: {e}'}
|
|
out = {
|
|
'cpu_pct': snap.get('cpu_pct'),
|
|
'ram_pct': snap.get('ram_pct'),
|
|
'disk_pct': snap.get('disk_pct'),
|
|
'load_avg': snap.get('load_avg'),
|
|
'uptime_seconds': snap.get('uptime_seconds'),
|
|
'timestamp': snap.get('ts') or time.time(),
|
|
}
|
|
cache_set('server_status', out)
|
|
return out
|
|
|
|
|
|
_FORTUNES_SFW = [
|
|
'You will find the bug before lunch.',
|
|
'A stranger will ask you for a Solana airdrop. Decline politely.',
|
|
"Today's commit will be remembered.",
|
|
'The moon is closer than your merge conflict.',
|
|
"Don't feed the AI after midnight.",
|
|
'Debugging is twice as hard as writing the code in the first place.',
|
|
'Your next deploy will be flawless. Probably.',
|
|
'The quieter you become, the more you can grep.',
|
|
'A watched service never restarts cleanly.',
|
|
'Trust the process. Then kill it with SIGKILL.',
|
|
]
|
|
_FORTUNES_SPICY = [
|
|
'Go outside. The server will still be broken when you get back.',
|
|
'Your rubber duck has filed a grievance.',
|
|
'Stack Overflow is down. Good luck, nerd.',
|
|
'That variable name is a war crime.',
|
|
'You are not a 10x developer. You are a 10x debugger.',
|
|
]
|
|
|
|
|
|
def tool_random_fortune(args: dict) -> dict:
|
|
import random
|
|
offensive = bool(args.get('offensive'))
|
|
# Try system fortune first
|
|
for path in ('/usr/games/fortune', '/usr/bin/fortune'):
|
|
if os.path.exists(path):
|
|
try:
|
|
flags = ['-o'] if offensive else ['-s']
|
|
r = subprocess.run([path] + flags, capture_output=True, text=True, timeout=3)
|
|
line = (r.stdout or '').strip()
|
|
if line:
|
|
return {'fortune': line, 'source': 'fortune(1)'}
|
|
except Exception:
|
|
pass
|
|
pool = _FORTUNES_SPICY + _FORTUNES_SFW if offensive else _FORTUNES_SFW
|
|
return {'fortune': random.choice(pool), 'source': 'builtin'}
|
|
|
|
|
|
# Tiny built-in block-letter ASCII for A-Z 0-9 (single-row, 3x5 font)
|
|
_ASCII_FONT = {
|
|
'A': [' █ ', '█▀█', '█ █'],
|
|
'B': ['██ ', '██ ', '██ '],
|
|
'C': ['██ ', '█ ', '██ '],
|
|
'D': ['██ ', '█ █', '██ '],
|
|
'E': ['███', '██ ', '███'],
|
|
'F': ['███', '██ ', '█ '],
|
|
'G': ['██ ', '█ █', '███'],
|
|
'H': ['█ █', '███', '█ █'],
|
|
'I': ['███', ' █ ', '███'],
|
|
'J': ['███', ' █', '██ '],
|
|
'K': ['█ █', '██ ', '█ █'],
|
|
'L': ['█ ', '█ ', '███'],
|
|
'M': ['█▄█', '█▀█', '█ █'],
|
|
'N': ['█▄█', '█▀█', '█ █'],
|
|
'O': ['███', '█ █', '███'],
|
|
'P': ['██ ', '██ ', '█ '],
|
|
'Q': ['███', '█ █', '██▄'],
|
|
'R': ['██ ', '██ ', '█ █'],
|
|
'S': ['██ ', ' █ ', '██ '],
|
|
'T': ['███', ' █ ', ' █ '],
|
|
'U': ['█ █', '█ █', '███'],
|
|
'V': ['█ █', '█ █', ' █ '],
|
|
'W': ['█ █', '███', '█▀█'],
|
|
'X': ['█ █', ' █ ', '█ █'],
|
|
'Y': ['█ █', ' █ ', ' █ '],
|
|
'Z': ['███', ' █ ', '███'],
|
|
'0': ['███', '█ █', '███'],
|
|
'1': [' █ ', ' █ ', ' █ '],
|
|
'2': ['██ ', ' █ ', ' ██'],
|
|
'3': ['██ ', ' █ ', '██ '],
|
|
'4': ['█ █', '███', ' █'],
|
|
'5': ['███', '██ ', ' ██'],
|
|
'6': ['██ ', '███', '███'],
|
|
'7': ['███', ' █', ' █'],
|
|
'8': ['███', '███', '███'],
|
|
'9': ['███', '███', ' █'],
|
|
' ': [' ', ' ', ' '],
|
|
'.': [' ', ' ', ' █ '],
|
|
'!': [' █ ', ' █ ', ' ▪ '],
|
|
'?': ['██ ', ' █ ', ' ▪ '],
|
|
'-': [' ', '███', ' '],
|
|
}
|
|
|
|
|
|
def tool_ascii_banner(args: dict) -> dict:
|
|
text = (args.get('text') or '').upper().strip()
|
|
if not text:
|
|
return {'error': 'text required'}
|
|
text = text[:30]
|
|
try:
|
|
import pyfiglet # type: ignore
|
|
art = pyfiglet.figlet_format(text)
|
|
return {'banner': art, 'font': 'figlet', 'lines': art.count('\n')}
|
|
except Exception:
|
|
pass
|
|
rows = ['', '', '']
|
|
for ch in text:
|
|
glyph = _ASCII_FONT.get(ch, _ASCII_FONT[' '])
|
|
for i in range(3):
|
|
rows[i] += glyph[i] + ' '
|
|
return {'banner': '\n'.join(rows), 'font': 'builtin', 'lines': 3}
|
|
|
|
|
|
def tool_get_leaderboards(args: dict) -> dict:
|
|
try:
|
|
r = req.get('http://127.0.0.1:5000/api/leaderboards', timeout=8)
|
|
if r.status_code == 200:
|
|
j = r.json()
|
|
return {
|
|
'top_countries': (j.get('countries') or j.get('top_countries') or [])[:5],
|
|
'top_pages': (j.get('pages') or j.get('top_pages') or [])[:5],
|
|
'top_referrers': (j.get('referrers') or j.get('top_referrers') or [])[:5],
|
|
'source': 'internal',
|
|
}
|
|
except Exception as e:
|
|
return {'error': f'leaderboards fetch failed: {e}'}
|
|
return {'error': 'no data'}
|
|
|
|
|
|
def tool_get_network_graph_data(args: dict) -> dict:
|
|
try:
|
|
r = req.get('http://127.0.0.1:5000/api/visitor/recent-arcs', timeout=8)
|
|
arcs = r.json() if r.status_code == 200 else []
|
|
except Exception:
|
|
arcs = []
|
|
cc_count: dict[str, int] = {}
|
|
for a in arcs if isinstance(arcs, list) else []:
|
|
cc = a.get('country_code') or a.get('cc')
|
|
if cc:
|
|
cc_count[cc] = cc_count.get(cc, 0) + 1
|
|
top = sorted(cc_count.items(), key=lambda kv: -kv[1])[:5]
|
|
return {
|
|
'total_arcs': len(arcs) if isinstance(arcs, list) else 0,
|
|
'top_countries': [{'code': c, 'count': n} for c, n in top],
|
|
'source': '/api/visitor/recent-arcs',
|
|
}
|
|
|
|
|
|
def tool_get_guestbook(args: dict) -> dict:
|
|
limit = max(1, min(int(args.get('limit') or 10), 50))
|
|
data = _safe_read_json(GUESTBOOK_PATH, {'entries': []})
|
|
entries = data.get('entries', []) if isinstance(data, dict) else []
|
|
entries = sorted(entries, key=lambda e: e.get('timestamp', 0), reverse=True)[:limit]
|
|
return {'entries': entries, 'count': len(entries)}
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# TIER: OPERATOR (wallet required)
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
def _require_wallet() -> tuple[str | None, dict | None]:
|
|
addr = _current_wallet()
|
|
if not addr:
|
|
return None, {'error': 'wallet authentication required'}
|
|
return addr, None
|
|
|
|
|
|
def _user_mem_path(addr: str) -> Path:
|
|
safe = re.sub(r'[^A-Za-z0-9]', '_', addr)[:64]
|
|
return MEM_DIR / f'{safe}.json'
|
|
|
|
|
|
def _load_user_mem(addr: str) -> dict:
|
|
return _safe_read_json(_user_mem_path(addr), {
|
|
'address': addr, 'memories': [], 'watchlist': [], 'created_at': time.time(),
|
|
})
|
|
|
|
|
|
def _save_user_mem(addr: str, data: dict) -> bool:
|
|
return _safe_write_json(_user_mem_path(addr), data)
|
|
|
|
|
|
def tool_save_memory(args: dict) -> dict:
|
|
addr, err = _require_wallet()
|
|
if err:
|
|
return err
|
|
fact = (args.get('fact') or '').strip()
|
|
if not fact or len(fact) < 3:
|
|
return {'error': 'fact required (min 3 chars)'}
|
|
fact = fact[:500]
|
|
mem = _load_user_mem(addr)
|
|
memories = mem.get('memories', [])
|
|
if len(memories) >= MAX_MEMORIES_PER_WALLET:
|
|
memories = memories[-(MAX_MEMORIES_PER_WALLET - 1):]
|
|
memories.append({'id': len(memories), 'fact': fact, 'timestamp': time.time()})
|
|
# Reindex ids
|
|
for i, m in enumerate(memories):
|
|
m['id'] = i
|
|
mem['memories'] = memories
|
|
if not _save_user_mem(addr, mem):
|
|
return {'error': 'failed to persist memory'}
|
|
return {'saved': True, 'id': memories[-1]['id'], 'total': len(memories)}
|
|
|
|
|
|
def tool_list_memories(args: dict) -> dict:
|
|
addr, err = _require_wallet()
|
|
if err:
|
|
return err
|
|
mem = _load_user_mem(addr)
|
|
return {'memories': mem.get('memories', []), 'count': len(mem.get('memories', []))}
|
|
|
|
|
|
def tool_delete_memory(args: dict) -> dict:
|
|
addr, err = _require_wallet()
|
|
if err:
|
|
return err
|
|
try:
|
|
idx = int(args.get('id'))
|
|
except (TypeError, ValueError):
|
|
return {'error': 'valid id required'}
|
|
mem = _load_user_mem(addr)
|
|
memories = mem.get('memories', [])
|
|
if idx < 0 or idx >= len(memories):
|
|
return {'error': f'id {idx} out of range (0..{len(memories)-1})'}
|
|
removed = memories.pop(idx)
|
|
# Reindex
|
|
for i, m in enumerate(memories):
|
|
m['id'] = i
|
|
mem['memories'] = memories
|
|
_save_user_mem(addr, mem)
|
|
return {'deleted': True, 'removed_fact': removed.get('fact'), 'remaining': len(memories)}
|
|
|
|
|
|
def tool_get_my_wallet_summary(args: dict) -> dict:
|
|
addr, err = _require_wallet()
|
|
if err:
|
|
return err
|
|
try:
|
|
from agent_tools import tool_wallet_xray
|
|
return tool_wallet_xray({'address': addr})
|
|
except Exception as e:
|
|
return {'error': f'summary failed: {e}'}
|
|
|
|
|
|
def tool_get_my_transactions(args: dict) -> dict:
|
|
addr, err = _require_wallet()
|
|
if err:
|
|
return err
|
|
limit = max(1, min(int(args.get('limit') or 5), 25))
|
|
try:
|
|
from auth_routes import get_rpc_url
|
|
rpc = get_rpc_url()
|
|
r = req.post(rpc, json={
|
|
'jsonrpc': '2.0', 'id': 1, 'method': 'getSignaturesForAddress',
|
|
'params': [addr, {'limit': limit}],
|
|
}, timeout=10)
|
|
sigs = r.json().get('result', []) or []
|
|
return {
|
|
'address': addr,
|
|
'transactions': [{
|
|
'signature': s.get('signature'),
|
|
'slot': s.get('slot'),
|
|
'block_time': s.get('blockTime'),
|
|
'err': bool(s.get('err')),
|
|
'solscan_url': f"https://solscan.io/tx/{s.get('signature')}",
|
|
} for s in sigs[:limit]],
|
|
'count': len(sigs[:limit]),
|
|
}
|
|
except Exception as e:
|
|
return {'error': f'tx fetch failed: {e}'}
|
|
|
|
|
|
def tool_post_guestbook_entry(args: dict) -> dict:
|
|
addr, err = _require_wallet()
|
|
if err:
|
|
return err
|
|
message = (args.get('message') or '').strip()
|
|
if not message or len(message) < 2:
|
|
return {'error': 'message required (min 2 chars)'}
|
|
# Sanitise: strip HTML, limit length
|
|
message = re.sub(r'<[^>]+>', '', message)[:280]
|
|
data = _safe_read_json(GUESTBOOK_PATH, {'entries': []})
|
|
entries = data.get('entries', []) if isinstance(data, dict) else []
|
|
entries.append({
|
|
'address': addr,
|
|
'truncated_address': _trim_addr(addr),
|
|
'message': message,
|
|
'timestamp': time.time(),
|
|
})
|
|
entries = entries[-500:] # cap total
|
|
_safe_write_json(GUESTBOOK_PATH, {'entries': entries})
|
|
return {'posted': True, 'message': message, 'total_entries': len(entries)}
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# TIER: ELITE
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
def tool_deep_research(args: dict) -> dict:
|
|
topic = (args.get('topic') or '').strip()
|
|
if not topic:
|
|
return {'error': 'topic required'}
|
|
from agent_tools import tool_search_site, tool_search_contraband
|
|
report: dict = {'topic': topic, 'sources': {}}
|
|
try:
|
|
report['sources']['site'] = tool_search_site({'query': topic}).get('results', [])
|
|
except Exception as e:
|
|
report['sources']['site_error'] = str(e)
|
|
try:
|
|
report['sources']['contraband'] = tool_search_contraband({'query': topic}).get('results', [])
|
|
except Exception as e:
|
|
report['sources']['contraband_error'] = str(e)
|
|
try:
|
|
report['sources']['unredacted'] = tool_search_unredacted({'query': topic}).get('results', [])
|
|
except Exception as e:
|
|
report['sources']['unredacted_error'] = str(e)
|
|
try:
|
|
report['sources']['radar'] = tool_search_radar({'query': topic}).get('results', [])
|
|
except Exception as e:
|
|
report['sources']['radar_error'] = str(e)
|
|
total = sum(len(v) for v in report['sources'].values() if isinstance(v, list))
|
|
report['total_results'] = total
|
|
report['summary'] = f'Gathered {total} hits across site, contraband, unredacted, and RADAR on "{topic}".'
|
|
return report
|
|
|
|
|
|
def tool_generate_report(args: dict) -> dict:
|
|
# 🟡 Placeholder: calling Venice from inside a tool handler re-entrantly is
|
|
# non-trivial. Return a stub that advises the user to ask the agent directly.
|
|
topic = (args.get('topic') or '').strip()
|
|
fmt = (args.get('format') or 'markdown').lower()
|
|
if not topic:
|
|
return {'error': 'topic required'}
|
|
return {
|
|
'status': 'placeholder',
|
|
'topic': topic,
|
|
'format': fmt,
|
|
'note': ('Long-form report generation is queued in the next release. '
|
|
'For now, ask JAE-AI directly — the Elite-tier Kimi-K2 model will '
|
|
'produce a detailed write-up inline. Use deep_research() first for sourcing.'),
|
|
}
|
|
|
|
|
|
def tool_track_token(args: dict) -> dict:
|
|
addr, err = _require_wallet()
|
|
if err:
|
|
return err
|
|
mint = (args.get('mint_address') or '').strip()
|
|
if not mint or not (32 <= len(mint) <= 44):
|
|
return {'error': 'valid Solana mint address required'}
|
|
mem = _load_user_mem(addr)
|
|
watchlist = mem.get('watchlist', [])
|
|
if mint in watchlist:
|
|
return {'already_tracked': True, 'mint': mint, 'total': len(watchlist)}
|
|
if len(watchlist) >= 50:
|
|
return {'error': 'watchlist full (max 50)'}
|
|
watchlist.append(mint)
|
|
mem['watchlist'] = watchlist
|
|
_save_user_mem(addr, mem)
|
|
return {'tracked': True, 'mint': mint, 'total': len(watchlist)}
|
|
|
|
|
|
def tool_set_alert(args: dict) -> dict:
|
|
addr, err = _require_wallet()
|
|
if err:
|
|
return err
|
|
alert_type = (args.get('type') or '').strip().lower()
|
|
if alert_type not in ('price_above', 'price_below', 'volume_above'):
|
|
return {'error': 'type must be one of: price_above, price_below, volume_above'}
|
|
try:
|
|
threshold = float(args.get('threshold'))
|
|
except (TypeError, ValueError):
|
|
return {'error': 'numeric threshold required'}
|
|
token = (args.get('token') or '').strip().upper()
|
|
if not token:
|
|
return {'error': 'token symbol required'}
|
|
data = _safe_read_json(ALERTS_PATH, {'alerts': []})
|
|
alerts = data.get('alerts', []) if isinstance(data, dict) else []
|
|
user_alerts = [a for a in alerts if a.get('address') == addr]
|
|
if len(user_alerts) >= 20:
|
|
return {'error': 'max 20 active alerts per wallet'}
|
|
new_alert = {
|
|
'id': f"alrt_{int(time.time()*1000)}",
|
|
'address': addr,
|
|
'type': alert_type,
|
|
'threshold': threshold,
|
|
'token': token,
|
|
'created_at': time.time(),
|
|
'active': True,
|
|
}
|
|
alerts.append(new_alert)
|
|
_safe_write_json(ALERTS_PATH, {'alerts': alerts})
|
|
return {'created': True, 'alert': new_alert, 'note': 'Notification delivery (email/discord) coming soon.'}
|
|
|
|
|
|
def tool_list_alerts(args: dict) -> dict:
|
|
addr, err = _require_wallet()
|
|
if err:
|
|
return err
|
|
data = _safe_read_json(ALERTS_PATH, {'alerts': []})
|
|
alerts = data.get('alerts', []) if isinstance(data, dict) else []
|
|
mine = [a for a in alerts if a.get('address') == addr and a.get('active', True)]
|
|
return {'alerts': mine, 'count': len(mine)}
|
|
|
|
|
|
def tool_ask_agent_chain(args: dict) -> dict:
|
|
# 🟡 Placeholder — spawning a nested tool-chain from inside a tool call
|
|
# risks infinite recursion under the current single-pass loop. Advise user.
|
|
prompt = (args.get('prompt') or '').strip()
|
|
max_steps = int(args.get('max_steps') or 3)
|
|
if not prompt:
|
|
return {'error': 'prompt required'}
|
|
return {
|
|
'status': 'placeholder',
|
|
'prompt': prompt,
|
|
'max_steps': max_steps,
|
|
'note': ('Recursive agent chains run in the superior agent loop by default — '
|
|
'ask JAE-AI directly with "plan a %d-step research on X" to get the same behaviour.' % max_steps),
|
|
}
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# TIER: ADMIN
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
def tool_deploy_sitrep_now(args: dict) -> dict:
|
|
if not _is_admin(_current_wallet()):
|
|
return {'error': 'admin only'}
|
|
sitrep = Path(__file__).parent / 'sitrep_generator.py'
|
|
if not sitrep.exists():
|
|
return {'error': 'sitrep_generator.py not found'}
|
|
try:
|
|
r = subprocess.run(
|
|
['python3', str(sitrep)],
|
|
capture_output=True, text=True, timeout=180, cwd=str(sitrep.parent),
|
|
)
|
|
return {
|
|
'triggered': True,
|
|
'exit_code': r.returncode,
|
|
'stdout_tail': (r.stdout or '')[-600:],
|
|
'stderr_tail': (r.stderr or '')[-300:],
|
|
}
|
|
except subprocess.TimeoutExpired:
|
|
return {'error': 'sitrep generation timed out after 180s'}
|
|
except Exception as e:
|
|
return {'error': f'failed: {e}'}
|
|
|
|
|
|
def tool_broadcast_banner(args: dict) -> dict:
|
|
if not _is_admin(_current_wallet()):
|
|
return {'error': 'admin only'}
|
|
message = (args.get('message') or '').strip()
|
|
if not message:
|
|
return {'error': 'message required'}
|
|
try:
|
|
duration_h = float(args.get('duration_hours') or 1)
|
|
except (TypeError, ValueError):
|
|
duration_h = 1.0
|
|
duration_h = max(0.1, min(duration_h, 72.0))
|
|
expires_at = time.time() + duration_h * 3600
|
|
_safe_write_json(BROADCAST_PATH, {
|
|
'message': message[:400],
|
|
'created_at': time.time(),
|
|
'expires_at': expires_at,
|
|
'active': True,
|
|
})
|
|
return {'broadcasting': True, 'message': message, 'expires_in_hours': duration_h}
|
|
|
|
|
|
def tool_purge_cache(args: dict) -> dict:
|
|
if not _is_admin(_current_wallet()):
|
|
return {'error': 'admin only'}
|
|
name = (args.get('cache_name') or 'all').strip().lower()
|
|
cleared = []
|
|
try:
|
|
from agent_tools import _cache
|
|
if name == 'all':
|
|
n = len(_cache)
|
|
_cache.clear()
|
|
cleared.append(f'agent_tools._cache ({n} entries)')
|
|
else:
|
|
keys = [k for k in _cache if name in k.lower()]
|
|
for k in keys:
|
|
_cache.pop(k, None)
|
|
cleared.append(f'agent_tools._cache matching "{name}" ({len(keys)} entries)')
|
|
except Exception as e:
|
|
cleared.append(f'agent_tools._cache error: {e}')
|
|
# RADAR cache
|
|
if name in ('all', 'radar'):
|
|
try:
|
|
from app import RADAR_CACHE # type: ignore
|
|
RADAR_CACHE.clear() if hasattr(RADAR_CACHE, 'clear') else RADAR_CACHE.update({'items': [], 'ts': 0})
|
|
cleared.append('RADAR_CACHE')
|
|
except Exception as e:
|
|
cleared.append(f'RADAR_CACHE error: {e}')
|
|
return {'cleared': cleared, 'name': name}
|
|
|
|
|
|
def tool_view_private_stats(args: dict) -> dict:
|
|
if not _is_admin(_current_wallet()):
|
|
return {'error': 'admin only'}
|
|
out: dict = {'timestamp': time.time()}
|
|
try:
|
|
r = req.get('http://127.0.0.1:5000/api/leaderboards', timeout=5)
|
|
out['leaderboards'] = r.json() if r.status_code == 200 else None
|
|
except Exception as e:
|
|
out['leaderboards_error'] = str(e)
|
|
try:
|
|
r = req.get('http://127.0.0.1:5000/api/telemetry/overview', timeout=5)
|
|
out['telemetry'] = r.json() if r.status_code == 200 else None
|
|
except Exception as e:
|
|
out['telemetry_error'] = str(e)
|
|
try:
|
|
r = req.get('http://127.0.0.1:5000/api/telemetry/visitors', timeout=5)
|
|
out['recent_visitors'] = r.json() if r.status_code == 200 else None
|
|
except Exception as e:
|
|
out['visitors_error'] = str(e)
|
|
return out
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# REGISTRATION
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
_EXTENDED = [
|
|
# Anonymous
|
|
Tool('search_unredacted', 'Search the UNREDACTED UFO/declassified archive. Top 5 matches.',
|
|
{'type': 'object', 'properties': {'query': {'type': 'string'}}, 'required': ['query']},
|
|
tool_search_unredacted, 'anonymous', '20/min', 10),
|
|
Tool('search_crimescene', 'Search the CRIMESCENE cold-case archive. Top 5 matches.',
|
|
{'type': 'object', 'properties': {'query': {'type': 'string'}}, 'required': ['query']},
|
|
tool_search_crimescene, 'anonymous', '20/min', 10),
|
|
Tool('search_radar', 'Search the live RADAR news feed aggregator. Top 5 matches.',
|
|
{'type': 'object', 'properties': {'query': {'type': 'string'}}, 'required': ['query']},
|
|
tool_search_radar, 'anonymous', '30/min', 10),
|
|
Tool('get_gov_domains_stats', 'Government domains monitor stats (total count + recent additions).',
|
|
{'type': 'object', 'properties': {}, 'required': []},
|
|
tool_get_gov_domains_stats, 'anonymous', '30/min', 8),
|
|
Tool('search_docs', 'Unified search across unredacted, crimescene, and latest SITREP. Top 10 hybrid results.',
|
|
{'type': 'object', 'properties': {'query': {'type': 'string'}}, 'required': ['query']},
|
|
tool_search_docs, 'anonymous', '10/min', 15),
|
|
Tool('get_server_status', 'Live CPU, RAM, disk, load, and uptime for jaeswift.xyz.',
|
|
{'type': 'object', 'properties': {}, 'required': []},
|
|
tool_get_server_status, 'anonymous', '30/min', 8),
|
|
Tool('random_fortune', 'Return a random one-liner fortune. Pass offensive=true for spicier pool.',
|
|
{'type': 'object', 'properties': {'offensive': {'type': 'boolean', 'default': False}}, 'required': []},
|
|
tool_random_fortune, 'anonymous', '30/min', 5),
|
|
Tool('ascii_banner', 'Generate ASCII-art banner of short text (≤30 chars).',
|
|
{'type': 'object', 'properties': {'text': {'type': 'string'}}, 'required': ['text']},
|
|
tool_ascii_banner, 'anonymous', '20/min', 5),
|
|
Tool('get_leaderboards', 'Top countries, pages, and referrers visiting jaeswift.xyz.',
|
|
{'type': 'object', 'properties': {}, 'required': []},
|
|
tool_get_leaderboards, 'anonymous', '30/min', 10),
|
|
Tool('get_network_graph_data', 'Live globe arc counts + top source countries of recent visitors.',
|
|
{'type': 'object', 'properties': {}, 'required': []},
|
|
tool_get_network_graph_data, 'anonymous', '30/min', 10),
|
|
Tool('get_guestbook', 'Read the public guestbook (latest N entries, default 10).',
|
|
{'type': 'object', 'properties': {'limit': {'type': 'integer', 'default': 10}}, 'required': []},
|
|
tool_get_guestbook, 'anonymous', '30/min', 5),
|
|
|
|
# Operator
|
|
Tool('save_memory', 'Save a personal fact keyed to your wallet (max 100, 500 chars each).',
|
|
{'type': 'object', 'properties': {'fact': {'type': 'string'}}, 'required': ['fact']},
|
|
tool_save_memory, 'operator', '10/min', 5),
|
|
Tool('list_memories', 'List all your saved memories.',
|
|
{'type': 'object', 'properties': {}, 'required': []},
|
|
tool_list_memories, 'operator', '30/min', 5),
|
|
Tool('delete_memory', 'Forget a memory by its index id.',
|
|
{'type': 'object', 'properties': {'id': {'type': 'integer'}}, 'required': ['id']},
|
|
tool_delete_memory, 'operator', '10/min', 5),
|
|
Tool('get_my_wallet_summary', 'Summary of your authenticated wallet: balance, tokens, recent tx count.',
|
|
{'type': 'object', 'properties': {}, 'required': []},
|
|
tool_get_my_wallet_summary, 'operator', '20/min', 15),
|
|
Tool('get_my_transactions', 'Your last N transactions on Solana (default 5, max 25).',
|
|
{'type': 'object', 'properties': {'limit': {'type': 'integer', 'default': 5}}, 'required': []},
|
|
tool_get_my_transactions, 'operator', '10/min', 15),
|
|
Tool('post_guestbook_entry', 'Post a message (≤280 chars) to the public guestbook, signed by your wallet.',
|
|
{'type': 'object', 'properties': {'message': {'type': 'string'}}, 'required': ['message']},
|
|
tool_post_guestbook_entry, 'operator', '3/hour', 5),
|
|
|
|
# Elite
|
|
Tool('deep_research', 'Run a multi-source research pass (site + contraband + unredacted + radar) on a topic.',
|
|
{'type': 'object', 'properties': {'topic': {'type': 'string'}}, 'required': ['topic']},
|
|
tool_deep_research, 'elite', '5/hour', 45),
|
|
Tool('generate_report', 'Request a long-form report on a topic. (Currently returns a placeholder.)',
|
|
{'type': 'object', 'properties': {
|
|
'topic': {'type': 'string'},
|
|
'format': {'type': 'string', 'default': 'markdown'},
|
|
}, 'required': ['topic']},
|
|
tool_generate_report, 'elite', '2/hour', 10),
|
|
Tool('track_token', 'Add a Solana token mint to your watchlist (max 50).',
|
|
{'type': 'object', 'properties': {'mint_address': {'type': 'string'}}, 'required': ['mint_address']},
|
|
tool_track_token, 'elite', '10/hour', 5),
|
|
Tool('set_alert', 'Create a price/volume alert for a token. type ∈ {price_above, price_below, volume_above}.',
|
|
{'type': 'object', 'properties': {
|
|
'type': {'type': 'string'},
|
|
'threshold': {'type': 'number'},
|
|
'token': {'type': 'string'},
|
|
}, 'required': ['type', 'threshold', 'token']},
|
|
tool_set_alert, 'elite', '10/hour', 5),
|
|
Tool('list_alerts', 'List your active alerts.',
|
|
{'type': 'object', 'properties': {}, 'required': []},
|
|
tool_list_alerts, 'elite', '30/min', 5),
|
|
Tool('ask_agent_chain', 'Spawn a nested sub-agent to chain up to N tool calls on a prompt. (Placeholder.)',
|
|
{'type': 'object', 'properties': {
|
|
'prompt': {'type': 'string'},
|
|
'max_steps': {'type': 'integer', 'default': 3},
|
|
}, 'required': ['prompt']},
|
|
tool_ask_agent_chain, 'elite', '3/hour', 60),
|
|
|
|
# Admin
|
|
Tool('deploy_sitrep_now', 'Trigger the SITREP generator immediately.',
|
|
{'type': 'object', 'properties': {}, 'required': []},
|
|
tool_deploy_sitrep_now, 'admin', '2/hour', 180),
|
|
Tool('broadcast_banner', 'Show a sitewide banner to all visitors (duration 0.1-72 hours).',
|
|
{'type': 'object', 'properties': {
|
|
'message': {'type': 'string'},
|
|
'duration_hours': {'type': 'number', 'default': 1},
|
|
}, 'required': ['message']},
|
|
tool_broadcast_banner, 'admin', '5/day', 5),
|
|
Tool('purge_cache', 'Clear in-memory caches. cache_name ∈ {all, radar, sol_price, crypto_price, server_status}.',
|
|
{'type': 'object', 'properties': {'cache_name': {'type': 'string', 'default': 'all'}}, 'required': []},
|
|
tool_purge_cache, 'admin', '10/hour', 5),
|
|
Tool('view_private_stats', 'Full visitor/telemetry/leaderboard data (admin only).',
|
|
{'type': 'object', 'properties': {}, 'required': []},
|
|
tool_view_private_stats, 'admin', '30/min', 15),
|
|
]
|
|
|
|
for _t in _EXTENDED:
|
|
register(_t)
|