betterwithage commited on
Commit
e190ffe
·
verified ·
1 Parent(s): 0ccb914

chore(sync): mirror backend .py + Dockerfile to Space (hf-sync-backend)

Browse files

Automated backend sync from szl-holdings/a11oy main via hf-sync-backend.
Updated (differed from the Space): a11oy_ayllu.py, ayllu/__init__.py, ayllu/lounge.py, ayllu/personas.py
Deleted (gone from the repo + Dockerfile COPY set): (none)

Keeps the Space-built backend (serve.py + the Dockerfile-COPY'd .py
modules) identical to GitHub main so the Space never rebuilds from a
stale backend, new endpoints don't 404 there, and orphaned modules
removed from the repo don't linger in the Space tree.

Files changed (4) hide show
  1. a11oy_ayllu.py +158 -41
  2. ayllu/__init__.py +1 -1
  3. ayllu/lounge.py +47 -2
  4. ayllu/personas.py +11 -3
a11oy_ayllu.py CHANGED
@@ -68,6 +68,7 @@ __version__ = _AYLLU_VERSION
68
  # ---- cost + abuse bounds (public Space; real token cost once live) -----------
69
  MAX_PROMPT_CHARS = 6000
70
  COUNCIL_MAX = 5 # hard cap on participants / call
 
71
  COUNCIL_DEFAULT = ["Amaru", "Kamachiq", "Qhatuq"] # architect · orchestrator · markets
72
 
73
  # One process-wide lounge (in-memory, honest source labels).
@@ -134,11 +135,14 @@ _PAGE = r"""<!doctype html><html lang="en"><head><meta charset="utf-8">
134
  <meta name="viewport" content="width=device-width,initial-scale=1">
135
  <title>Ayllu — a11oy agent council</title>
136
  <style>
137
- :root{--void:#080c14;--panel:#0d1520;--line:#16202c;--teal:#3af4c8;--fg:#dfe7ee;--dim:#7f93a6}
138
  *{box-sizing:border-box}
139
  body{margin:0;background:var(--void);color:var(--fg);
140
  font:15px/1.55 ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,sans-serif}
141
- main{max-width:900px;margin:0 auto;padding:36px 22px}
 
 
 
142
  h1{color:var(--teal);margin:0 0 4px;font-size:26px;display:flex;align-items:center;gap:10px}
143
  h2{font-size:16px;margin:0 0 10px;color:var(--fg)}
144
  .sub{color:var(--dim);margin:0 0 22px}
@@ -160,14 +164,19 @@ button.mini{background:transparent;color:var(--teal);border:1px solid var(--line
160
  font-weight:600;font-size:12px}
161
  .hint{color:var(--dim);font-size:12px;margin:0 0 8px}
162
  .out{margin-top:10px}
163
- .turn{border-top:1px solid var(--line);padding:10px 0}
164
- .turnh{display:flex;justify-content:space-between;gap:10px;align-items:baseline}
165
- .meta{color:var(--dim);font-size:12px}
166
- .ans{margin-top:5px;white-space:pre-wrap}
 
 
 
167
  .rcpt{color:var(--dim);font-size:12px;margin-top:8px}
168
  .note{color:#da3;font-size:12px;margin-bottom:8px}
169
  .err{color:#e66}
170
  .stub{color:#da3;font-weight:700;font-size:11px}
 
 
171
  table{width:100%;border-collapse:collapse}
172
  th,td{text-align:left;padding:7px 9px;border-bottom:1px solid var(--line);font-size:14px}
173
  th{color:var(--teal);font-weight:600}
@@ -178,22 +187,34 @@ padding:12px 14px;border-radius:6px;color:var(--dim);margin-top:6px;font-size:13
178
  code{color:var(--teal)}
179
  html{scroll-behavior:smooth}
180
  .topbar{position:sticky;top:0;z-index:20;background:rgba(8,12,20,.92);backdrop-filter:blur(6px);border-bottom:1px solid var(--line)}
181
- .tb-wrap{max-width:900px;margin:0 auto;padding:10px 22px;display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}
182
  .tb-brand{color:var(--teal);font-weight:800;text-decoration:none;font-size:18px;display:flex;align-items:center;gap:8px}
183
  .tb-nav{display:flex;gap:14px;align-items:center;flex-wrap:wrap}
184
  .tb-nav a{color:var(--dim);text-decoration:none;font-size:13px}
185
  .tb-nav a:hover{color:var(--fg)}
186
- .tb-nav a.tb-home{color:#d4a444;font-weight:600}
187
  section[id]{scroll-margin-top:72px}
 
 
 
 
 
 
 
 
 
 
 
188
  </style></head><body>
189
  <header class="topbar"><div class="tb-wrap">
190
  <a class="tb-brand" href="/ayllu">Ayllu <span id="badge" class="badge">…</span></a>
191
- <nav class="tb-nav"><a href="#sec-ask">Ask</a><a href="#sec-council">Council</a><a href="#sec-roster">Roster</a><a href="#sec-lounge">Lounge</a><a href="#sec-mesh">Mesh</a><a class="tb-home" href="/console" title="Back to the a11oy command centre">&#8592; a11oy command centre</a></nav>
192
  </div></header>
193
  <main>
194
  <h1>Ayllu</h1>
195
  <p class="sub">The AlloyScape tribe, ingested and reborn as a11oy's own agent community —
196
- <span id="count">?</span> personas, one guarded loop. v__VERSION__</p>
 
197
 
198
  <section class="card" id="sec-ask">
199
  <h2>Ask a persona</h2>
@@ -210,8 +231,11 @@ section[id]{scroll-margin-top:72px}
210
  <section class="card" id="sec-council">
211
  <h2>Convene a council</h2>
212
  <p class="hint">Defaults to 3 core personas; select up to 5 (⌘/Ctrl-click). Fan-out is
213
- capped to protect cost.</p>
 
214
  <select id="councilsel" multiple size="6"></select>
 
 
215
  <textarea id="councilprompt" rows="3" placeholder="A question for the council…"></textarea>
216
  <button id="councilbtn">Convene</button>
217
  <div id="councilout" class="out"></div>
@@ -220,7 +244,10 @@ section[id]{scroll-margin-top:72px}
220
  <section class="card" id="sec-roster">
221
  <h2>Roster</h2>
222
  <table id="roster"><thead><tr><th>Persona</th><th>Quechua</th><th>Archetype</th>
223
- <th>a11oy domain</th><th>Autonomy</th></tr></thead><tbody></tbody></table>
 
 
 
224
  </section>
225
 
226
  <section class="card" id="sec-lounge">
@@ -228,6 +255,25 @@ section[id]{scroll-margin-top:72px}
228
  <div id="lounge" class="out"></div>
229
  </section>
230
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
  <section class="card" id="sec-mesh">
232
  <h2>Mesh &amp; observability <span class="stub" id="mesh-badge">&#8230;</span></h2>
233
  <div class="src" style="margin:.2rem 0 .6rem">Live platform context the council runs
@@ -242,21 +288,36 @@ fail-closed Λ-gate; state-changing actions require two-person attestation. The
242
  "always execute" mandate is deliberately <b>not</b> adopted. Answers come from a11oy's
243
  own model backend + router; when no inference credential is set on this Space,
244
  <code>ask</code>/<code>council</code> return a clearly-labeled stub — never a fabricated
245
- answer. These turns do direct completion only (no tool dispatch yet).</div>
 
 
 
 
 
 
 
 
246
  </main>
247
  <script>
248
  const NS="__NS__";
249
  const api = p => `/api/${NS}/v1/ayllu/`+p;
 
250
  async function j(url,opts){const r=await fetch(url,opts);
251
  let d={};try{d=await r.json();}catch(e){}return {ok:r.ok,status:r.status,data:d};}
252
  function esc(s){return (s==null?'':String(s)).replace(/[&<>]/g,
253
  c=>({'&':'&amp;','<':'&lt;','>':'&gt;'}[c]));}
 
 
 
 
254
  function renderTurn(t){
255
  const ans = t.answer!=null ? esc(t.answer) : '<i>'+esc(t.honesty)+'</i>';
256
  const model = t.model?('model: '+esc(t.model)):'no model';
257
  const tier = (t.tier&&t.tier.route)?(' · tier(advisory): '+esc(t.tier.route)):'';
258
  const stub = t.stub?' · <span class="stub">STUB</span>':'';
259
- return `<div class="turn"><div class="turnh"><b>${esc(t.persona)}</b>`
 
 
260
  + `<span class="meta">${model}${tier}${stub}</span></div>`
261
  + `<div class="ans">${ans}</div></div>`;
262
  }
@@ -274,9 +335,10 @@ async function loadRoster(){
274
  const o=document.createElement('option');o.value=p.name;
275
  o.textContent=p.name+' — '+p.domain;sel.appendChild(o);
276
  csel.appendChild(o.cloneNode(true));
277
- rows.push(`<tr><td><b>${esc(p.name)}</b></td><td>${esc(p.quechua)}</td>`
 
278
  +`<td>${esc(p.archetype)}</td><td>${esc(p.domain)}</td>`
279
- +`<td>${esc(p.autonomy_level)}</td></tr>`);
280
  });
281
  document.querySelector('#roster tbody').innerHTML=rows.join('');
282
  }
@@ -301,16 +363,27 @@ document.getElementById('councilbtn').onclick=async()=>{
301
  const out=document.getElementById('councilout');
302
  if(!prompt){out.innerHTML='<span class="err">enter a prompt</span>';return;}
303
  const picks=[...document.getElementById('councilsel').selectedOptions].map(o=>o.value);
304
- out.textContent='…convening';
305
- const body={prompt}; if(picks.length)body.personas=picks;
 
306
  const {ok,status,data}=await j(api('council'),{method:'POST',
307
  headers:{'content-type':'application/json'},body:JSON.stringify(body)});
308
  if(!ok){out.innerHTML='<span class="err">'+esc(data.error||('HTTP '+status))+'</span>'
309
  +(data.retry_after_s?(' (retry in '+data.retry_after_s+'s)'):'');return;}
310
- const rounds=(data.result&&data.result.rounds)||[];
311
- const cap=(data.result&&data.result.cap_note)?
312
- ('<div class="note">'+esc(data.result.cap_note)+'</div>'):'';
313
- out.innerHTML=cap+rounds.map(renderTurn).join('');
 
 
 
 
 
 
 
 
 
 
314
  };
315
  async function loadLounge(){
316
  const {ok,data}=await j(api('lounge')), out=document.getElementById('lounge');
@@ -321,26 +394,64 @@ async function loadLounge(){
321
  :'<i>empty</i>';
322
  }
323
  document.getElementById('refreshlounge').onclick=loadLounge;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
324
  async function loadMesh(){
325
  const badge=document.getElementById('mesh-badge'), out=document.getElementById('mesh-out');
326
- const {ok,status,data}=await j('/api/'+NS+'/v1/mesh/state');
327
- if(!ok){badge.textContent='offline';
328
- out.innerHTML='<span class="src">mesh state endpoint unavailable ('+esc(String(status))+') — shown honestly, not faked.</span>';return;}
329
- const d=(data&&data.data&&typeof data.data==='object')?data.data:data;
330
- const nodes=d.nodes||d.mesh||d.members||d.organs||[];
331
- const cnt=(d.count!=null)?d.count:(Array.isArray(nodes)?nodes.length:'\u2014');
332
  badge.textContent='live';
333
- out.innerHTML='<div class="lg"><b>mesh nodes</b> <span class="src">'+esc(String(cnt))+'</span></div>';
 
 
 
 
 
 
 
334
  }
335
  async function loadObs(){
336
  const out=document.getElementById('obs-out');
337
- const {ok,status,data}=await j('/api/'+NS+'/v1/observability/summary');
338
- if(!ok){out.innerHTML='<span class="src">observability summary unavailable ('+esc(String(status))+') — shown honestly.</span>';return;}
339
- const d=(data&&data.data&&typeof data.data==='object')?data.data:data;
340
  out.innerHTML='<div class="lg"><b>observability</b><pre class="src" style="white-space:pre-wrap;margin:.3rem 0 0">'
341
- +esc(JSON.stringify(d,null,2).slice(0,700))+'</pre></div>';
342
  }
343
- loadRoster();loadLounge();loadMesh();loadObs();
344
  </script>
345
  </body></html>"""
346
 
@@ -439,18 +550,21 @@ def register(app, ns: str = "a11oy") -> str:
439
  return JSONResponse(
440
  {"error": f"'prompt' must be a string ≤ {MAX_PROMPT_CHARS} chars",
441
  "max_chars": MAX_PROMPT_CHARS}, status_code=422)
 
 
442
  requested = body.get("personas")
443
  cap_note = None
444
  if requested:
445
  if not isinstance(requested, list):
446
  return JSONResponse({"error": "'personas' must be a list"},
447
  status_code=422)
448
- names = [str(n) for n in requested][:COUNCIL_MAX]
449
- if len(requested) > COUNCIL_MAX:
450
  cap_note = (f"requested {len(requested)} personas; capped to "
451
- f"{COUNCIL_MAX} to bound cost")
 
452
  else:
453
- names = list(COUNCIL_DEFAULT)
454
  cap_note = (f"no personas specified; convened the {len(names)} core "
455
  "personas (Amaru, Kamachiq, Qhatuq)")
456
  personas = [get_persona(n) for n in names]
@@ -460,7 +574,8 @@ def register(app, ns: str = "a11oy") -> str:
460
  {"error": "no known personas in request",
461
  "known": [x.name for x in ROSTER]}, status_code=422)
462
  result = await _LOUNGE.deliberate(prompt, personas,
463
- model_complete=_backend.model_complete)
 
464
  if cap_note:
465
  result["cap_note"] = cap_note
466
  council_id = str(uuid.uuid4())
@@ -468,6 +583,7 @@ def register(app, ns: str = "a11oy") -> str:
468
  "council_id": council_id,
469
  "prompt_sha256": hashlib.sha256(prompt.encode()).hexdigest(),
470
  "participants": result["participants"],
 
471
  "models": [r.get("model") for r in result.get("rounds", [])],
472
  })
473
  return JSONResponse({"council_id": council_id, "result": result,
@@ -488,7 +604,7 @@ def register(app, ns: str = "a11oy") -> str:
488
  summary="Ask one persona — bounded, honest, receipted")
489
  app.add_api_route(f"/api/{ns}/v1/ayllu/council", _council, methods=["POST"],
490
  tags=["ayllu"],
491
- summary="Bounded multi-persona deliberation (capped fan-out)")
492
  app.add_api_route(f"/api/{ns}/v1/ayllu/lounge", _lounge_feed, methods=["GET"],
493
  tags=["ayllu"], summary="Recent collaboration lounge feed")
494
  app.add_api_route("/ayllu", _page, methods=["GET"], include_in_schema=False)
@@ -496,5 +612,6 @@ def register(app, ns: str = "a11oy") -> str:
496
  return (
497
  f"ok — ayllu registered: {len(ROSTER)} personas; live model backend "
498
  f"({_backend.backend_status().get('mode')}); bounded-autonomy Λ-gate; "
499
- f"/ayllu + /api/{ns}/v1/ayllu/roster|ask|council|lounge; version={__version__}"
 
500
  )
 
68
  # ---- cost + abuse bounds (public Space; real token cost once live) -----------
69
  MAX_PROMPT_CHARS = 6000
70
  COUNCIL_MAX = 5 # hard cap on participants / call
71
+ COUNCIL_DEBATE_MAX = 3 # debate doubles model calls; tighter cap bounds cost
72
  COUNCIL_DEFAULT = ["Amaru", "Kamachiq", "Qhatuq"] # architect · orchestrator · markets
73
 
74
  # One process-wide lounge (in-memory, honest source labels).
 
135
  <meta name="viewport" content="width=device-width,initial-scale=1">
136
  <title>Ayllu — a11oy agent council</title>
137
  <style>
138
+ :root{--void:#080c14;--panel:#0d1520;--line:#16202c;--teal:#3af4c8;--fg:#dfe7ee;--dim:#7f93a6;--gold:#d4a444}
139
  *{box-sizing:border-box}
140
  body{margin:0;background:var(--void);color:var(--fg);
141
  font:15px/1.55 ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,sans-serif}
142
+ body::before{content:"";position:fixed;inset:0;pointer-events:none;z-index:0;
143
+ background:radial-gradient(60% 40% at 70% -10%,rgba(58,244,200,.07),transparent 60%),
144
+ radial-gradient(50% 35% at 10% 110%,rgba(212,164,68,.05),transparent 60%)}
145
+ main{max-width:980px;margin:0 auto;padding:36px 22px;position:relative;z-index:1}
146
  h1{color:var(--teal);margin:0 0 4px;font-size:26px;display:flex;align-items:center;gap:10px}
147
  h2{font-size:16px;margin:0 0 10px;color:var(--fg)}
148
  .sub{color:var(--dim);margin:0 0 22px}
 
164
  font-weight:600;font-size:12px}
165
  .hint{color:var(--dim);font-size:12px;margin:0 0 8px}
166
  .out{margin-top:10px}
167
+ .turn{border:1px solid var(--line);border-radius:9px;padding:10px 12px;margin-top:8px;background:#0a121c}
168
+ .turnh{display:flex;gap:9px;align-items:center;flex-wrap:wrap}
169
+ .chip{width:26px;height:26px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;
170
+ font-size:11px;font-weight:800;color:var(--fg);border:1px solid var(--line);flex:none}
171
+ .meta{color:var(--dim);font-size:12px;margin-left:auto}
172
+ .arch{color:var(--dim);font-size:12px}
173
+ .ans{margin-top:7px;white-space:pre-wrap}
174
  .rcpt{color:var(--dim);font-size:12px;margin-top:8px}
175
  .note{color:#da3;font-size:12px;margin-bottom:8px}
176
  .err{color:#e66}
177
  .stub{color:#da3;font-weight:700;font-size:11px}
178
+ .roundhdr{margin:14px 0 2px;color:var(--gold);font-size:12px;font-weight:700;letter-spacing:.06em;text-transform:uppercase}
179
+ .loopchip{font-size:10px;border:1px solid var(--line);border-radius:12px;padding:1px 7px;color:var(--dim)}
180
  table{width:100%;border-collapse:collapse}
181
  th,td{text-align:left;padding:7px 9px;border-bottom:1px solid var(--line);font-size:14px}
182
  th{color:var(--teal);font-weight:600}
 
187
  code{color:var(--teal)}
188
  html{scroll-behavior:smooth}
189
  .topbar{position:sticky;top:0;z-index:20;background:rgba(8,12,20,.92);backdrop-filter:blur(6px);border-bottom:1px solid var(--line)}
190
+ .tb-wrap{max-width:980px;margin:0 auto;padding:10px 22px;display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}
191
  .tb-brand{color:var(--teal);font-weight:800;text-decoration:none;font-size:18px;display:flex;align-items:center;gap:8px}
192
  .tb-nav{display:flex;gap:14px;align-items:center;flex-wrap:wrap}
193
  .tb-nav a{color:var(--dim);text-decoration:none;font-size:13px}
194
  .tb-nav a:hover{color:var(--fg)}
195
+ .tb-nav a.tb-home{color:var(--gold);font-weight:600}
196
  section[id]{scroll-margin-top:72px}
197
+ .grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:10px}
198
+ .panel{background:#0a121c;border:1px solid var(--line);border-radius:8px;padding:10px 12px;min-width:0}
199
+ .panel h3{margin:0 0 6px;font-size:13px;color:var(--teal);display:flex;justify-content:space-between;gap:8px;align-items:baseline}
200
+ .kpi{font-size:19px;font-weight:800;color:var(--fg)}
201
+ .small{font-size:12px;color:var(--dim)}
202
+ .links{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}
203
+ .links a{color:var(--teal);text-decoration:none;font-size:12px;border:1px solid var(--line);border-radius:16px;padding:4px 11px}
204
+ .links a:hover{border-color:var(--teal)}
205
+ .tgl{display:flex;gap:7px;align-items:center;color:var(--dim);font-size:13px;margin:0 0 8px}
206
+ .tgl input{width:auto}
207
+ .prov{color:var(--dim);font-size:11px;margin-top:14px;line-height:1.6}
208
  </style></head><body>
209
  <header class="topbar"><div class="tb-wrap">
210
  <a class="tb-brand" href="/ayllu">Ayllu <span id="badge" class="badge">…</span></a>
211
+ <nav class="tb-nav"><a href="#sec-ask">Ask</a><a href="#sec-council">Council</a><a href="#sec-roster">Roster</a><a href="#sec-lounge">Lounge</a><a href="#sec-organism">Organism</a><a href="#sec-mesh">Mesh</a><a class="tb-home" href="/console" title="Back to the a11oy command centre">&#8592; a11oy command centre</a></nav>
212
  </div></header>
213
  <main>
214
  <h1>Ayllu</h1>
215
  <p class="sub">The AlloyScape tribe, ingested and reborn as a11oy's own agent community —
216
+ <span id="count">?</span> personas, one guarded loop. v__VERSION__ ·
217
+ <span title="Curated, cited text appended to every persona's system prompt — no weights changed anywhere.">knowledge instilled, never "trained"</span></p>
218
 
219
  <section class="card" id="sec-ask">
220
  <h2>Ask a persona</h2>
 
231
  <section class="card" id="sec-council">
232
  <h2>Convene a council</h2>
233
  <p class="hint">Defaults to 3 core personas; select up to 5 (⌘/Ctrl-click). Fan-out is
234
+ capped to protect cost. Debate mode runs exactly two bounded rounds
235
+ (after arXiv:2305.14325) and is capped to 3 personas.</p>
236
  <select id="councilsel" multiple size="6"></select>
237
+ <label class="tgl"><input type="checkbox" id="debate">
238
+ Debate mode — positions, then explicit dissent &amp; converge (2× cost)</label>
239
  <textarea id="councilprompt" rows="3" placeholder="A question for the council…"></textarea>
240
  <button id="councilbtn">Convene</button>
241
  <div id="councilout" class="out"></div>
 
244
  <section class="card" id="sec-roster">
245
  <h2>Roster</h2>
246
  <table id="roster"><thead><tr><th>Persona</th><th>Quechua</th><th>Archetype</th>
247
+ <th>a11oy domain</th><th>Autonomy</th><th>Knowledge</th></tr></thead><tbody></tbody></table>
248
+ <div class="src" style="margin-top:6px">"Instilled ✓" = the shared, cited Wave-13 leaders
249
+ corpus is appended to that persona's system prompt at runtime. No model weights are
250
+ changed anywhere — this is knowledge instillation, not training.</div>
251
  </section>
252
 
253
  <section class="card" id="sec-lounge">
 
255
  <div id="lounge" class="out"></div>
256
  </section>
257
 
258
+ <section class="card" id="sec-organism">
259
+ <h2>The organism — anatomy · brain · formulas</h2>
260
+ <div class="src" style="margin:.2rem 0 .6rem">The council is one organ of one governed
261
+ organism. These panels read the same governed endpoints as the command centre — an
262
+ unavailable endpoint says so rather than faking a value.</div>
263
+ <div class="grid">
264
+ <div class="panel"><h3>Formulas <span class="small" id="f-badge">…</span></h3><div id="f-out" class="small">loading…</div></div>
265
+ <div class="panel"><h3>Doctrine lock <span class="small" id="d-badge">…</span></h3><div id="d-out" class="small">loading…</div></div>
266
+ <div class="panel"><h3>Sovereign energy <span class="small" id="e-badge">…</span></h3><div id="e-out" class="small">loading…</div></div>
267
+ </div>
268
+ <div class="links">
269
+ <a href="/living-anatomy">Living anatomy</a>
270
+ <a href="/formulas">PURIQ formulas</a>
271
+ <a href="/wires">The constitution</a>
272
+ <a href="/api/__NS__/v1/brain/graph" title="Full brain graph JSON (~4 MB)">Brain graph (raw JSON)</a>
273
+ <a href="/console">Command centre</a>
274
+ </div>
275
+ </section>
276
+
277
  <section class="card" id="sec-mesh">
278
  <h2>Mesh &amp; observability <span class="stub" id="mesh-badge">&#8230;</span></h2>
279
  <div class="src" style="margin:.2rem 0 .6rem">Live platform context the council runs
 
288
  "always execute" mandate is deliberately <b>not</b> adopted. Answers come from a11oy's
289
  own model backend + router; when no inference credential is set on this Space,
290
  <code>ask</code>/<code>council</code> return a clearly-labeled stub — never a fabricated
291
+ answer. Debate mode is bounded to exactly two rounds. These turns do direct completion
292
+ only (no tool dispatch yet).</div>
293
+
294
+ <p class="prov"><b>Provenance.</b> Council patterns studied from the field's leaders and
295
+ rebuilt in a11oy's own idiom — MetaGPT (SOP role handoffs), CrewAI (role/goal remits),
296
+ LangGraph (bounded graph loops), CAMEL (agent societies), OpenAI Agents SDK / Google ADK
297
+ (visible handoffs &amp; guardrails) — all permissive-licensed; debate-then-converge after
298
+ arXiv:2305.14325. No code was copied from pattern-only sources. Personas carry this as
299
+ instilled knowledge (cited text in the system prompt); nothing here was "trained".</p>
300
  </main>
301
  <script>
302
  const NS="__NS__";
303
  const api = p => `/api/${NS}/v1/ayllu/`+p;
304
+ const gapi = p => `/api/${NS}/v1/`+p;
305
  async function j(url,opts){const r=await fetch(url,opts);
306
  let d={};try{d=await r.json();}catch(e){}return {ok:r.ok,status:r.status,data:d};}
307
  function esc(s){return (s==null?'':String(s)).replace(/[&<>]/g,
308
  c=>({'&':'&amp;','<':'&lt;','>':'&gt;'}[c]));}
309
+ function hue(n){let h=0;for(const c of String(n))h=(h*31+c.charCodeAt(0))%360;return h;}
310
+ function chip(n){const h=hue(n);
311
+ return `<span class="chip" style="background:hsl(${h} 55% 20%);border-color:hsl(${h} 55% 38%)">`
312
+ + esc(String(n).slice(0,2).toUpperCase())+`</span>`;}
313
  function renderTurn(t){
314
  const ans = t.answer!=null ? esc(t.answer) : '<i>'+esc(t.honesty)+'</i>';
315
  const model = t.model?('model: '+esc(t.model)):'no model';
316
  const tier = (t.tier&&t.tier.route)?(' · tier(advisory): '+esc(t.tier.route)):'';
317
  const stub = t.stub?' · <span class="stub">STUB</span>':'';
318
+ const loop = (t.loop&&t.loop.mode)?`<span class="loopchip" title="${esc(t.loop.note||'')}">loop: ${esc(t.loop.mode)}</span>`:'';
319
+ return `<div class="turn"><div class="turnh">${chip(t.persona)}<b>${esc(t.persona)}</b>`
320
+ + `<span class="arch">${esc(t.archetype||'')}</span>${loop}`
321
  + `<span class="meta">${model}${tier}${stub}</span></div>`
322
  + `<div class="ans">${ans}</div></div>`;
323
  }
 
335
  const o=document.createElement('option');o.value=p.name;
336
  o.textContent=p.name+' — '+p.domain;sel.appendChild(o);
337
  csel.appendChild(o.cloneNode(true));
338
+ const kn=p.knowledge_instilled?'instilled ✓':'—';
339
+ rows.push(`<tr><td>${chip(p.name)} <b>${esc(p.name)}</b></td><td>${esc(p.quechua)}</td>`
340
  +`<td>${esc(p.archetype)}</td><td>${esc(p.domain)}</td>`
341
+ +`<td>${esc(p.autonomy_level)}</td><td class="src">${kn}</td></tr>`);
342
  });
343
  document.querySelector('#roster tbody').innerHTML=rows.join('');
344
  }
 
363
  const out=document.getElementById('councilout');
364
  if(!prompt){out.innerHTML='<span class="err">enter a prompt</span>';return;}
365
  const picks=[...document.getElementById('councilsel').selectedOptions].map(o=>o.value);
366
+ const debate=document.getElementById('debate').checked;
367
+ out.textContent=debate?'…convening (debate: 2 bounded rounds)':'…convening';
368
+ const body={prompt}; if(picks.length)body.personas=picks; if(debate)body.debate=true;
369
  const {ok,status,data}=await j(api('council'),{method:'POST',
370
  headers:{'content-type':'application/json'},body:JSON.stringify(body)});
371
  if(!ok){out.innerHTML='<span class="err">'+esc(data.error||('HTTP '+status))+'</span>'
372
  +(data.retry_after_s?(' (retry in '+data.retry_after_s+'s)'):'');return;}
373
+ const res=data.result||{}, rounds=res.rounds||[];
374
+ const cap=res.cap_note?('<div class="note">'+esc(res.cap_note)+'</div>'):'';
375
+ const r1=rounds.filter(t=>(t.round||1)===1), r2=rounds.filter(t=>t.round===2);
376
+ let html=cap;
377
+ if(r2.length){
378
+ html+='<div class="roundhdr">Round 1 — opening positions</div>'+r1.map(renderTurn).join('');
379
+ html+='<div class="roundhdr">Round 2 — debate &amp; converge (final)</div>'+r2.map(renderTurn).join('');
380
+ }else{
381
+ html+=r1.map(renderTurn).join('');
382
+ if(res.mode==='single-round'&&document.getElementById('debate').checked){
383
+ html+='<div class="src">debate skipped honestly: fewer than two personas produced answers.</div>';
384
+ }
385
+ }
386
+ out.innerHTML=html;
387
  };
388
  async function loadLounge(){
389
  const {ok,data}=await j(api('lounge')), out=document.getElementById('lounge');
 
394
  :'<i>empty</i>';
395
  }
396
  document.getElementById('refreshlounge').onclick=loadLounge;
397
+ function unavailable(el,status){el.innerHTML='<span class="src">endpoint unavailable ('
398
+ +esc(String(status))+') — shown honestly, not faked.</span>';}
399
+ async function loadFormulas(){
400
+ const b=document.getElementById('f-badge'), out=document.getElementById('f-out');
401
+ const {ok,status,data}=await j(gapi('formulas'));
402
+ if(!ok){b.textContent='offline';unavailable(out,status);return;}
403
+ b.textContent='live';
404
+ const fs=(data.formulas||[]).slice(0,4);
405
+ out.innerHTML='<div class="kpi">'+esc(String(data.count??'—'))+'</div>'
406
+ +'<div class="small">registered formulas</div>'
407
+ +fs.map(f=>`<div class="lg"><code>${esc(f.name)}</code><br><span class="src">${esc(f.proof_status||'')}</span></div>`).join('')
408
+ +'<div class="src" style="margin-top:6px">Λ-aggregator uniqueness remains Conjecture 1 — never claimed proven.</div>';
409
+ }
410
+ async function loadDoctrine(){
411
+ const b=document.getElementById('d-badge'), out=document.getElementById('d-out');
412
+ const {ok,status,data}=await j(gapi('honest'));
413
+ if(!ok){b.textContent='offline';unavailable(out,status);return;}
414
+ const d=data.doctrine_lock||{};
415
+ b.textContent=esc(d.state||'?');
416
+ out.innerHTML='<div class="kpi">'+esc(String(d.declarations??'—'))+'</div>'
417
+ +'<div class="small">Lean declarations · '+esc(String(d.axioms??'—'))+' axioms · '
418
+ +esc(String(d.sorries??'—'))+' sorries (honest count)</div>'
419
+ +'<div class="lg small">'+esc(d.lambda_note||'')+'</div>';
420
+ }
421
+ async function loadEnergy(){
422
+ const b=document.getElementById('e-badge'), out=document.getElementById('e-out');
423
+ const {ok,status,data}=await j(gapi('energy/live'));
424
+ if(!ok){b.textContent='offline';unavailable(out,status);return;}
425
+ b.textContent=esc(data.label||'?');
426
+ const nodes=data.nodes||[];
427
+ const live=nodes.filter(n=>n.live).length;
428
+ out.innerHTML='<div class="kpi">'+(data.total_watts!=null?esc(String(data.total_watts))+' W':'—')+'</div>'
429
+ +'<div class="small">'+live+'/'+nodes.length+' sovereign nodes live'
430
+ +(data.total_watts==null?' — no wattage fabricated':'')+'</div>'
431
+ +nodes.slice(0,3).map(n=>`<div class="lg small">${n.live?'●':'○'} ${esc(n.name)}</div>`).join('');
432
+ }
433
  async function loadMesh(){
434
  const badge=document.getElementById('mesh-badge'), out=document.getElementById('mesh-out');
435
+ const {ok,status,data}=await j(gapi('mesh/state'));
436
+ if(!ok){badge.textContent='offline';unavailable(out,status);return;}
 
 
 
 
437
  badge.textContent='live';
438
+ const wires=data.wires||{};
439
+ const rows=Object.entries(wires).map(([k,w])=>
440
+ `<tr><td><b>${esc(k)}</b></td><td>${esc(w.edge||'')}</td><td class="src">${esc(w.status||'')}</td></tr>`);
441
+ out.innerHTML=rows.length?('<table><thead><tr><th>Wire</th><th>Edge</th><th>Status</th></tr></thead><tbody>'
442
+ +rows.join('')+'</tbody></table>'
443
+ +'<div class="src" style="margin-top:5px">doctrine '+esc(data.doctrine||'?')
444
+ +' · khipu nodes: '+esc(String(data.khipu_nodes??'—'))+'</div>')
445
+ :'<div class="lg"><b>mesh</b> <span class="src">no wires reported</span></div>';
446
  }
447
  async function loadObs(){
448
  const out=document.getElementById('obs-out');
449
+ const {ok,status,data}=await j(gapi('observability/summary'));
450
+ if(!ok){unavailable(out,status);return;}
 
451
  out.innerHTML='<div class="lg"><b>observability</b><pre class="src" style="white-space:pre-wrap;margin:.3rem 0 0">'
452
+ +esc(JSON.stringify(data.melt||data,null,2).slice(0,600))+'</pre></div>';
453
  }
454
+ loadRoster();loadLounge();loadMesh();loadObs();loadFormulas();loadDoctrine();loadEnergy();
455
  </script>
456
  </body></html>"""
457
 
 
550
  return JSONResponse(
551
  {"error": f"'prompt' must be a string ≤ {MAX_PROMPT_CHARS} chars",
552
  "max_chars": MAX_PROMPT_CHARS}, status_code=422)
553
+ debate = bool(body.get("debate", False))
554
+ max_n = COUNCIL_DEBATE_MAX if debate else COUNCIL_MAX
555
  requested = body.get("personas")
556
  cap_note = None
557
  if requested:
558
  if not isinstance(requested, list):
559
  return JSONResponse({"error": "'personas' must be a list"},
560
  status_code=422)
561
+ names = [str(n) for n in requested][:max_n]
562
+ if len(requested) > max_n:
563
  cap_note = (f"requested {len(requested)} personas; capped to "
564
+ f"{max_n} to bound cost"
565
+ + (" (debate mode runs two rounds)" if debate else ""))
566
  else:
567
+ names = list(COUNCIL_DEFAULT)[:max_n]
568
  cap_note = (f"no personas specified; convened the {len(names)} core "
569
  "personas (Amaru, Kamachiq, Qhatuq)")
570
  personas = [get_persona(n) for n in names]
 
574
  {"error": "no known personas in request",
575
  "known": [x.name for x in ROSTER]}, status_code=422)
576
  result = await _LOUNGE.deliberate(prompt, personas,
577
+ model_complete=_backend.model_complete,
578
+ debate=debate)
579
  if cap_note:
580
  result["cap_note"] = cap_note
581
  council_id = str(uuid.uuid4())
 
583
  "council_id": council_id,
584
  "prompt_sha256": hashlib.sha256(prompt.encode()).hexdigest(),
585
  "participants": result["participants"],
586
+ "mode": result.get("mode"),
587
  "models": [r.get("model") for r in result.get("rounds", [])],
588
  })
589
  return JSONResponse({"council_id": council_id, "result": result,
 
604
  summary="Ask one persona — bounded, honest, receipted")
605
  app.add_api_route(f"/api/{ns}/v1/ayllu/council", _council, methods=["POST"],
606
  tags=["ayllu"],
607
+ summary="Bounded multi-persona deliberation (capped fan-out; optional 2-round debate mode after arXiv:2305.14325)")
608
  app.add_api_route(f"/api/{ns}/v1/ayllu/lounge", _lounge_feed, methods=["GET"],
609
  tags=["ayllu"], summary="Recent collaboration lounge feed")
610
  app.add_api_route("/ayllu", _page, methods=["GET"], include_in_schema=False)
 
612
  return (
613
  f"ok — ayllu registered: {len(ROSTER)} personas; live model backend "
614
  f"({_backend.backend_status().get('mode')}); bounded-autonomy Λ-gate; "
615
+ f"/ayllu + /api/{ns}/v1/ayllu/roster|ask|council|lounge; "
616
+ f"debate-mode council; version={__version__}"
617
  )
ayllu/__init__.py CHANGED
@@ -18,7 +18,7 @@ Public surface:
18
  """
19
  from __future__ import annotations
20
 
21
- __version__ = "0.1.0"
22
  NAMESPACE_DEFAULT = "a11oy"
23
 
24
  __all__ = ["__version__", "NAMESPACE_DEFAULT"]
 
18
  """
19
  from __future__ import annotations
20
 
21
+ __version__ = "0.2.0"
22
  NAMESPACE_DEFAULT = "a11oy"
23
 
24
  __all__ = ["__version__", "NAMESPACE_DEFAULT"]
ayllu/lounge.py CHANGED
@@ -35,6 +35,7 @@ class Lounge:
35
  model_complete=None,
36
  difficulty: float = 0.6,
37
  two_person_attested: bool = False,
 
38
  ) -> dict[str, Any]:
39
  from .loop import run_turn
40
 
@@ -46,13 +47,57 @@ class Lounge:
46
  difficulty=difficulty,
47
  two_person_attested=two_person_attested,
48
  )
 
49
  src = "brain" if turn.get("answer") is not None else "persona-fallback"
50
  self.post(p.name, turn.get("answer") or turn.get("honesty"), source=src)
51
  rounds.append(turn)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  return {
53
  "prompt": prompt,
54
  "participants": [p.name for p in personas],
55
  "rounds": rounds,
56
- "note": "bounded council round; each turn honest (no fabrication when "
57
- "a model backend is absent)",
 
 
58
  }
 
35
  model_complete=None,
36
  difficulty: float = 0.6,
37
  two_person_attested: bool = False,
38
+ debate: bool = False,
39
  ) -> dict[str, Any]:
40
  from .loop import run_turn
41
 
 
47
  difficulty=difficulty,
48
  two_person_attested=two_person_attested,
49
  )
50
+ turn["round"] = 1
51
  src = "brain" if turn.get("answer") is not None else "persona-fallback"
52
  self.post(p.name, turn.get("answer") or turn.get("honesty"), source=src)
53
  rounds.append(turn)
54
+
55
+ mode = "single-round"
56
+ # Debate-then-converge (after arXiv:2305.14325, Multiagent Debate): one
57
+ # bounded extra round where each persona reads its peers' round-1 positions,
58
+ # states dissent or agreement explicitly, and revises. EXACTLY two rounds —
59
+ # never an open-ended loop. Runs only when a real/stub backend is injected
60
+ # and at least two personas answered (an honest debate needs peers).
61
+ if debate and model_complete is not None:
62
+ positions = [
63
+ (r.get("persona"), r.get("answer"))
64
+ for r in rounds if r.get("answer")
65
+ ]
66
+ if len(positions) >= 2:
67
+ mode = "debate"
68
+ for p in personas:
69
+ peers = "\n\n".join(
70
+ f"[{name}] {ans[:1200]}" for name, ans in positions
71
+ if name != p.name
72
+ )
73
+ debate_prompt = (
74
+ "COUNCIL DEBATE ROUND (bounded: this is the final round).\n"
75
+ f"Original question:\n{prompt}\n\n"
76
+ f"Your peers' opening positions:\n{peers}\n\n"
77
+ "State explicitly where you agree or dissent, then give "
78
+ "your revised answer. Honest dissent beats false consensus."
79
+ )
80
+ turn = await run_turn(
81
+ p, debate_prompt,
82
+ model_complete=model_complete,
83
+ difficulty=difficulty,
84
+ two_person_attested=two_person_attested,
85
+ )
86
+ turn["round"] = 2
87
+ src = ("brain" if turn.get("answer") is not None
88
+ else "persona-fallback")
89
+ self.post(p.name, turn.get("answer") or turn.get("honesty"),
90
+ source=src)
91
+ rounds.append(turn)
92
+ else:
93
+ mode = "single-round"
94
+
95
  return {
96
  "prompt": prompt,
97
  "participants": [p.name for p in personas],
98
  "rounds": rounds,
99
+ "mode": mode,
100
+ "note": "bounded council; each turn honest (no fabrication when a model "
101
+ "backend is absent); debate mode = exactly two rounds, "
102
+ "after arXiv:2305.14325",
103
  }
ayllu/personas.py CHANGED
@@ -33,9 +33,16 @@ class Persona:
33
 
34
  def system_prompt(self) -> str:
35
  p = self.soul_path()
36
- if not p.exists():
37
- return f"[soul file missing for {self.name}: {p} — honest empty persona]"
38
- return p.read_text(encoding="utf-8")
 
 
 
 
 
 
 
39
 
40
  def metadata(self) -> dict:
41
  return {
@@ -47,6 +54,7 @@ class Persona:
47
  "approval_required": self.approval_required,
48
  "default_difficulty": self.default_difficulty,
49
  "soul_present": self.soul_path().exists(),
 
50
  }
51
 
52
 
 
33
 
34
  def system_prompt(self) -> str:
35
  p = self.soul_path()
36
+ if p.exists():
37
+ prompt = p.read_text(encoding="utf-8")
38
+ else:
39
+ prompt = f"[soul file missing for {self.name}: {p} — honest empty persona]"
40
+ shared = _SOULS_DIR / "_shared_knowledge.md"
41
+ if shared.exists():
42
+ # Knowledge INSTILLED (curated, cited text appended to the system
43
+ # prompt) — never "training"; no weights are changed anywhere.
44
+ prompt += "\n\n" + shared.read_text(encoding="utf-8")
45
+ return prompt
46
 
47
  def metadata(self) -> dict:
48
  return {
 
54
  "approval_required": self.approval_required,
55
  "default_difficulty": self.default_difficulty,
56
  "soul_present": self.soul_path().exists(),
57
+ "knowledge_instilled": (_SOULS_DIR / "_shared_knowledge.md").exists(),
58
  }
59
 
60