steward functionality to bost reality
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Ekaropolus 2026-01-03 21:30:49 -06:00
parent 4d788c1acc
commit 4c56529c96
12 changed files with 563 additions and 0 deletions

View File

@ -65,6 +65,7 @@ INSTALLED_APPS = [
'pxy_routing', 'pxy_routing',
'pxy_sites', 'pxy_sites',
"pxy_simulations", "pxy_simulations",
"pxy_stewards",
"rest_framework", "rest_framework",
"pxy_api", "pxy_api",

View File

@ -47,6 +47,7 @@ urlpatterns = [
path("api/", include("pxy_bots.api.urls")), path("api/", include("pxy_bots.api.urls")),
path("api/langchain/", include("pxy_langchain.api.urls")), path("api/langchain/", include("pxy_langchain.api.urls")),
path("api/", include("pxy_stewards.api.urls")),
path("", include("pxy_openai.urls")), path("", include("pxy_openai.urls")),

1
pxy_stewards/__init__.py Normal file
View File

@ -0,0 +1 @@
"""Stewardship app for NFC + bot-driven cell powers."""

24
pxy_stewards/admin.py Normal file
View File

@ -0,0 +1,24 @@
from django.contrib import admin
from .models import Stewardship, StewardBlessing, StewardSignal
@admin.register(Stewardship)
class StewardshipAdmin(admin.ModelAdmin):
list_display = ("platform", "user_id", "status", "geohash", "target_bot", "expires_at", "updated_at")
list_filter = ("platform", "status", "target_bot")
search_fields = ("user_id", "geohash", "target_bot")
@admin.register(StewardBlessing)
class StewardBlessingAdmin(admin.ModelAdmin):
list_display = ("geohash", "target_bot", "multiplier", "starts_at", "ends_at")
list_filter = ("target_bot",)
search_fields = ("geohash",)
@admin.register(StewardSignal)
class StewardSignalAdmin(admin.ModelAdmin):
list_display = ("geohash", "target_bot", "kind", "created_at")
list_filter = ("target_bot", "kind")
search_fields = ("geohash", "note")

View File

@ -0,0 +1 @@
"""API for steward workflows."""

9
pxy_stewards/api/urls.py Normal file
View File

@ -0,0 +1,9 @@
from django.urls import path
from .views import steward_handle, blessings_active, steward_health
urlpatterns = [
path("stewards/handle", steward_handle, name="stewards_handle"),
path("stewards/blessings", blessings_active, name="stewards_blessings"),
path("stewards/health", steward_health, name="stewards_health"),
]

328
pxy_stewards/api/views.py Normal file
View File

@ -0,0 +1,328 @@
import json
import os
from datetime import timedelta
from typing import Any, Dict, Optional
from django.http import JsonResponse, HttpRequest
from django.utils import timezone
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_GET, require_http_methods
from pxy_stewards.models import Stewardship, StewardBlessing, StewardSignal
from pxy_stewards.utils import encode_geohash
STEWARD_TTL_HOURS = int(os.getenv("STEWARD_TTL_HOURS", "168"))
STEWARD_SESSION_TTL_MIN = int(os.getenv("STEWARD_SESSION_TTL_MIN", "30"))
STEWARD_GEOHASH_PRECISION = int(os.getenv("STEWARD_GEOHASH_PRECISION", "7"))
STEWARD_ENERGY_MAX = int(os.getenv("STEWARD_ENERGY_MAX", "100"))
STEWARD_BLESS_COST = int(os.getenv("STEWARD_BLESS_COST", "25"))
STEWARD_BLESS_MINUTES = int(os.getenv("STEWARD_BLESS_MINUTES", "60"))
STEWARD_BLESS_MULTIPLIER = float(os.getenv("STEWARD_BLESS_MULTIPLIER", "2.0"))
TARGET_BOTS = {
"coins": {"label": "Citizens (Pepe Basurita)", "bot": "PepeBasuritaCoinsBot"},
"motito": {"label": "Motito (On-demand)", "bot": "PepeMotitoBot"},
"camion": {"label": "Camioncito (Route)", "bot": "PepeCamioncitoBot"},
}
def _render_text(text: str, buttons: Optional[list] = None) -> JsonResponse:
spec = {
"schema_version": "render.v1",
"messages": [{"type": "text", "text": text}],
}
if buttons:
spec["buttons"] = buttons
return JsonResponse(spec)
def _load_body(request: HttpRequest) -> Dict[str, Any]:
try:
raw = (request.body or b"").decode("utf-8")
return json.loads(raw or "{}")
except Exception:
return {}
def _extract_args(body: Dict[str, Any]) -> str:
args_raw = (body.get("input") or {}).get("args_raw") or ""
if args_raw.startswith("/"):
parts = args_raw.split(" ", 1)
return parts[1].strip() if len(parts) > 1 else ""
return args_raw.strip()
def _parse_latlon(raw: str) -> Optional[tuple[float, float]]:
parts = [p.strip() for p in (raw or "").split(",")]
if len(parts) != 2:
return None
try:
lat = float(parts[0])
lon = float(parts[1])
except Exception:
return None
return lat, lon
def _platform(body: Dict[str, Any]) -> str:
ctx = body.get("context") or {}
return (ctx.get("platform") or "telegram").strip().lower()
def _user_id(body: Dict[str, Any]) -> Optional[str]:
user = body.get("user") or {}
if user.get("id") is None:
return None
return str(user.get("id"))
def _bot_name(body: Dict[str, Any]) -> str:
bot = body.get("bot") or {}
return (bot.get("username") or "unknown").strip()
def _get_steward(platform: str, user_id: str) -> Stewardship:
steward, _ = Stewardship.objects.get_or_create(
platform=platform,
user_id=user_id,
defaults={"bot_name": "unknown", "energy": 0},
)
return steward
def _expire_if_needed(steward: Stewardship) -> None:
if steward.status == Stewardship.STATUS_ACTIVE and steward.expires_at:
if steward.expires_at <= timezone.now():
steward.status = Stewardship.STATUS_EXPIRED
steward.energy = 0
steward.save(update_fields=["status", "energy", "updated_at"])
def _cell_taken(geohash: str, exclude_user_id: Optional[str] = None) -> bool:
now = timezone.now()
qs = Stewardship.objects.filter(
geohash=geohash,
status=Stewardship.STATUS_ACTIVE,
expires_at__gt=now,
)
if exclude_user_id:
qs = qs.exclude(user_id=exclude_user_id)
return qs.exists()
def _activation_buttons() -> list:
buttons = []
for key, meta in TARGET_BOTS.items():
buttons.append({
"label": meta["label"],
"kind": "callback_api",
"action": "choose",
"params": {"b": key},
})
return buttons
@csrf_exempt
@require_http_methods(["POST"])
def steward_handle(request: HttpRequest):
body = _load_body(request)
platform = _platform(body)
user_id = _user_id(body)
bot_name = _bot_name(body)
if not user_id:
return _render_text("Missing user context. Try again from the bot.")
cmd = ((body.get("command") or {}).get("name") or "").strip().lstrip("/").lower()
trigger = ((body.get("command") or {}).get("trigger") or "").strip().lower()
args = _extract_args(body)
location = (body.get("input") or {}).get("location") or {}
steward = _get_steward(platform, user_id)
_expire_if_needed(steward)
if steward.bot_name != bot_name:
steward.bot_name = bot_name
steward.save(update_fields=["bot_name", "updated_at"])
if trigger == "callback":
data_raw = ((body.get("callback") or {}).get("data") or "")
try:
data = json.loads(data_raw)
except Exception:
data = {}
action = (data.get("a") or data.get("action") or "").lower()
params = data.get("p") or data.get("params") or {}
if action == "choose":
key = (params.get("b") or params.get("bot") or "").lower()
meta = TARGET_BOTS.get(key)
if not meta:
return _render_text("Unknown target bot. Try /steward again.")
if steward.status != Stewardship.STATUS_AWAITING_TARGET:
return _render_text("No pending activation. Use /steward to start.")
if steward.geohash and _cell_taken(steward.geohash, exclude_user_id=steward.user_id):
return _render_text("That cell already has a Steward. Choose a new location.")
steward.target_bot = meta["bot"]
steward.status = Stewardship.STATUS_ACTIVE
steward.expires_at = timezone.now() + timedelta(hours=STEWARD_TTL_HOURS)
steward.energy = STEWARD_ENERGY_MAX
steward.pending_until = None
steward.save()
text = (
f"Steward activated for cell {steward.geohash}.\n"
f"Target bot: {meta['label']}.\n"
f"Power window: {STEWARD_TTL_HOURS}h.\n"
"Commands: /status /bless /signal"
)
return _render_text(text)
return _render_text("Unknown action. Use /steward to start.")
if cmd in {"steward", "start"}:
if steward.status == Stewardship.STATUS_ACTIVE and steward.expires_at and steward.expires_at > timezone.now():
return _render_text(
f"You are already Steward of {steward.geohash}.\n"
"Use /status, /bless, or /signal."
)
latlon = _parse_latlon(args)
if latlon:
location = {"lat": latlon[0], "lon": latlon[1]}
steward.status = Stewardship.STATUS_AWAITING_LOCATION
steward.pending_until = timezone.now() + timedelta(minutes=STEWARD_SESSION_TTL_MIN)
steward.save()
if location.get("lat") is not None and location.get("lon") is not None:
cmd = "__location__"
else:
return _render_text(
"Steward activation started.\n"
"Send your location to anchor your cell.\n"
"Your identity stays private."
)
if cmd == "__location__":
if steward.status != Stewardship.STATUS_AWAITING_LOCATION:
return _render_text("Send /steward to start steward activation.")
if location.get("lat") is None or location.get("lon") is None:
return _render_text("Location missing. Send your location to continue.")
lat = float(location.get("lat"))
lon = float(location.get("lon"))
geohash = encode_geohash(lat, lon, precision=STEWARD_GEOHASH_PRECISION)
if _cell_taken(geohash, exclude_user_id=steward.user_id):
return _render_text(
"That cell already has a Steward.\n"
"Send another location to try a different cell."
)
steward.lat = lat
steward.lon = lon
steward.geohash = geohash
steward.status = Stewardship.STATUS_AWAITING_TARGET
steward.pending_until = timezone.now() + timedelta(minutes=STEWARD_SESSION_TTL_MIN)
steward.save()
text = (
f"Cell captured: {geohash}.\n"
"Choose which bot to empower for this cell:"
)
return _render_text(text, buttons=_activation_buttons())
if cmd == "status":
if steward.status != Stewardship.STATUS_ACTIVE or not steward.geohash:
return _render_text("No active stewardship. Use /steward to begin.")
active_bless = StewardBlessing.objects.filter(
geohash=steward.geohash,
ends_at__gt=timezone.now(),
).count()
remaining = int((steward.expires_at - timezone.now()).total_seconds() / 3600) if steward.expires_at else 0
text = (
f"Steward status\n"
f"Cell: {steward.geohash}\n"
f"Target bot: {steward.target_bot}\n"
f"Energy: {steward.energy}/{STEWARD_ENERGY_MAX}\n"
f"Blessings active: {active_bless}\n"
f"Power window: {remaining}h"
)
return _render_text(text)
if cmd == "bless":
if steward.status != Stewardship.STATUS_ACTIVE or not steward.geohash:
return _render_text("No active stewardship. Use /steward to begin.")
if steward.energy < STEWARD_BLESS_COST:
return _render_text("Not enough energy to bless. Try later.")
minutes = STEWARD_BLESS_MINUTES
if args:
try:
minutes = max(10, min(240, int(args.split()[0])))
except Exception:
minutes = STEWARD_BLESS_MINUTES
starts = timezone.now()
ends = starts + timedelta(minutes=minutes)
StewardBlessing.objects.create(
steward=steward,
geohash=steward.geohash,
target_bot=steward.target_bot or "unknown",
multiplier=STEWARD_BLESS_MULTIPLIER,
starts_at=starts,
ends_at=ends,
)
steward.energy = max(0, steward.energy - STEWARD_BLESS_COST)
steward.save(update_fields=["energy", "updated_at"])
text = (
f"Blessing active for {minutes} min.\n"
f"Cell: {steward.geohash}\n"
f"Boost: x{STEWARD_BLESS_MULTIPLIER:.1f} Green Points"
)
return _render_text(text)
if cmd == "signal":
if steward.status != Stewardship.STATUS_ACTIVE or not steward.geohash:
return _render_text("No active stewardship. Use /steward to begin.")
note = args or "cell_needs_attention"
StewardSignal.objects.create(
steward=steward,
geohash=steward.geohash,
target_bot=steward.target_bot or "unknown",
kind="signal",
note=note,
)
return _render_text(
f"Signal recorded for {steward.geohash}.\n"
"The city agents will adjust in your cell."
)
return _render_text(
"Steward commands:\n"
"/steward - activate stewardship\n"
"/status - view status\n"
"/bless [minutes] - boost Green Points\n"
"/signal <note> - log a cell signal"
)
@csrf_exempt
@require_GET
def blessings_active(request: HttpRequest):
geohash = (request.GET.get("geohash") or "").strip()
target_bot = (request.GET.get("bot") or "").strip()
if not geohash:
return JsonResponse({"ok": False, "code": "missing_geohash"}, status=400)
now = timezone.now()
qs = StewardBlessing.objects.filter(geohash=geohash, ends_at__gt=now)
if target_bot:
qs = qs.filter(target_bot=target_bot)
data = [
{
"geohash": b.geohash,
"target_bot": b.target_bot,
"multiplier": b.multiplier,
"starts_at": b.starts_at.isoformat(),
"ends_at": b.ends_at.isoformat(),
}
for b in qs
]
return JsonResponse({"ok": True, "blessings": data})
@csrf_exempt
@require_GET
def steward_health(request: HttpRequest):
return JsonResponse({"ok": True, "service": "pxy_stewards"})

6
pxy_stewards/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class PxyStewardsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "pxy_stewards"

View File

@ -0,0 +1,75 @@
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="Stewardship",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("platform", models.CharField(default="telegram", max_length=20)),
("user_id", models.CharField(max_length=64)),
("bot_name", models.CharField(max_length=80)),
("status", models.CharField(choices=[("awaiting_location", "Awaiting location"), ("awaiting_target", "Awaiting target bot"), ("active", "Active"), ("expired", "Expired")], default="awaiting_location", max_length=32)),
("geohash", models.CharField(blank=True, max_length=12, null=True)),
("lat", models.FloatField(blank=True, null=True)),
("lon", models.FloatField(blank=True, null=True)),
("target_bot", models.CharField(blank=True, max_length=80, null=True)),
("energy", models.PositiveIntegerField(default=0)),
("expires_at", models.DateTimeField(blank=True, null=True)),
("pending_until", models.DateTimeField(blank=True, null=True)),
("created_at", models.DateTimeField(default=django.utils.timezone.now, editable=False)),
("updated_at", models.DateTimeField(auto_now=True)),
],
options={
"indexes": [
models.Index(fields=["platform", "user_id"], name="pxy_stewar_platform_2a65e9_idx"),
models.Index(fields=["geohash", "status", "expires_at"], name="pxy_stewar_geohash_626644_idx"),
],
"unique_together": {("platform", "user_id")},
},
),
migrations.CreateModel(
name="StewardBlessing",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("geohash", models.CharField(max_length=12)),
("target_bot", models.CharField(max_length=80)),
("multiplier", models.FloatField(default=2.0)),
("starts_at", models.DateTimeField(default=django.utils.timezone.now)),
("ends_at", models.DateTimeField()),
("created_at", models.DateTimeField(default=django.utils.timezone.now, editable=False)),
("steward", models.ForeignKey(on_delete=models.deletion.CASCADE, related_name="blessings", to="pxy_stewards.stewardship")),
],
options={
"indexes": [
models.Index(fields=["geohash", "ends_at"], name="pxy_stewar_geohash_245c81_idx"),
models.Index(fields=["target_bot", "ends_at"], name="pxy_stewar_target__1c2e1f_idx"),
],
},
),
migrations.CreateModel(
name="StewardSignal",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("geohash", models.CharField(max_length=12)),
("target_bot", models.CharField(max_length=80)),
("kind", models.CharField(default="signal", max_length=40)),
("note", models.TextField(blank=True, default="")),
("created_at", models.DateTimeField(default=django.utils.timezone.now, editable=False)),
("steward", models.ForeignKey(on_delete=models.deletion.CASCADE, related_name="signals", to="pxy_stewards.stewardship")),
],
options={
"indexes": [
models.Index(fields=["geohash", "created_at"], name="pxy_stewar_geohash_62497e_idx"),
models.Index(fields=["target_bot", "created_at"], name="pxy_stewar_target__73ec24_idx"),
],
},
),
]

View File

@ -0,0 +1 @@
"""Migrations for pxy_stewards."""

81
pxy_stewards/models.py Normal file
View File

@ -0,0 +1,81 @@
from django.db import models
from django.utils import timezone
class Stewardship(models.Model):
STATUS_AWAITING_LOCATION = "awaiting_location"
STATUS_AWAITING_TARGET = "awaiting_target"
STATUS_ACTIVE = "active"
STATUS_EXPIRED = "expired"
STATUS_CHOICES = [
(STATUS_AWAITING_LOCATION, "Awaiting location"),
(STATUS_AWAITING_TARGET, "Awaiting target bot"),
(STATUS_ACTIVE, "Active"),
(STATUS_EXPIRED, "Expired"),
]
platform = models.CharField(max_length=20, default="telegram")
user_id = models.CharField(max_length=64)
bot_name = models.CharField(max_length=80)
status = models.CharField(max_length=32, choices=STATUS_CHOICES, default=STATUS_AWAITING_LOCATION)
geohash = models.CharField(max_length=12, blank=True, null=True)
lat = models.FloatField(null=True, blank=True)
lon = models.FloatField(null=True, blank=True)
target_bot = models.CharField(max_length=80, blank=True, null=True)
energy = models.PositiveIntegerField(default=0)
expires_at = models.DateTimeField(null=True, blank=True)
pending_until = models.DateTimeField(null=True, blank=True)
created_at = models.DateTimeField(default=timezone.now, editable=False)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
unique_together = ("platform", "user_id")
indexes = [
models.Index(fields=["platform", "user_id"]),
models.Index(fields=["geohash", "status", "expires_at"]),
]
def __str__(self) -> str:
cell = self.geohash or "unset"
return f"{self.platform}:{self.user_id} @ {cell} ({self.status})"
class StewardBlessing(models.Model):
steward = models.ForeignKey(Stewardship, on_delete=models.CASCADE, related_name="blessings")
geohash = models.CharField(max_length=12)
target_bot = models.CharField(max_length=80)
multiplier = models.FloatField(default=2.0)
starts_at = models.DateTimeField(default=timezone.now)
ends_at = models.DateTimeField()
created_at = models.DateTimeField(default=timezone.now, editable=False)
class Meta:
indexes = [
models.Index(fields=["geohash", "ends_at"]),
models.Index(fields=["target_bot", "ends_at"]),
]
def __str__(self) -> str:
return f"Blessing {self.geohash} -> {self.target_bot}"
class StewardSignal(models.Model):
steward = models.ForeignKey(Stewardship, on_delete=models.CASCADE, related_name="signals")
geohash = models.CharField(max_length=12)
target_bot = models.CharField(max_length=80)
kind = models.CharField(max_length=40, default="signal")
note = models.TextField(blank=True, default="")
created_at = models.DateTimeField(default=timezone.now, editable=False)
class Meta:
indexes = [
models.Index(fields=["geohash", "created_at"]),
models.Index(fields=["target_bot", "created_at"]),
]
def __str__(self) -> str:
return f"Signal {self.geohash} -> {self.target_bot} ({self.kind})"

35
pxy_stewards/utils.py Normal file
View File

@ -0,0 +1,35 @@
_BASE32 = "0123456789bcdefghjkmnpqrstuvwxyz"
_BITS = [16, 8, 4, 2, 1]
def encode_geohash(lat: float, lon: float, precision: int = 7) -> str:
lat_interval = [-90.0, 90.0]
lon_interval = [-180.0, 180.0]
geohash = []
bit = 0
ch = 0
even = True
while len(geohash) < precision:
if even:
mid = (lon_interval[0] + lon_interval[1]) / 2
if lon >= mid:
ch |= _BITS[bit]
lon_interval[0] = mid
else:
lon_interval[1] = mid
else:
mid = (lat_interval[0] + lat_interval[1]) / 2
if lat >= mid:
ch |= _BITS[bit]
lat_interval[0] = mid
else:
lat_interval[1] = mid
even = not even
if bit < 4:
bit += 1
else:
geohash.append(_BASE32[ch])
bit = 0
ch = 0
return "".join(geohash)