File size: 5,006 Bytes
039dd77
 
 
8fc7ac7
 
 
 
 
039dd77
 
 
4867bc1
039dd77
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e190ffe
8fc7ac7
039dd77
 
 
4867bc1
 
 
039dd77
4867bc1
 
 
 
 
 
 
 
e190ffe
8fc7ac7
 
 
 
e190ffe
 
 
 
 
 
 
 
 
 
 
 
 
 
4867bc1
e190ffe
 
 
 
 
 
 
 
 
 
 
 
4867bc1
 
 
 
 
e190ffe
8fc7ac7
 
 
 
 
e190ffe
 
 
 
039dd77
 
 
 
e190ffe
8fc7ac7
e190ffe
 
 
039dd77
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
"""ayllu.lounge — the collaboration surface, learned from the tribe lounge/bus.

The tribe let souls talk in a shared room. a11oy keeps that, in-memory and honest: each
posted message carries a source label ("model-unverified" when a model answered,
else "persona-fallback"), so a reader never mistakes direct model text for grounded
Brain retrieval. `deliberate()` runs a BOUNDED council round — one honest turn
per persona — and never fabricates when no backend is injected. Publishing is
explicit opt-in; public ask/council calls keep their output out of this feed.
"""
from __future__ import annotations

import asyncio
import time
from typing import Any


class Lounge:
    def __init__(self, capacity: int = 200) -> None:
        self.capacity = int(capacity)
        self.feed: list[dict[str, Any]] = []

    def post(self, persona: str, text: str, *, source: str = "persona") -> dict[str, Any]:
        entry = {"ts": time.time(), "persona": persona, "text": text, "source": source}
        self.feed.append(entry)
        if len(self.feed) > self.capacity:
            self.feed = self.feed[-self.capacity:]
        return entry

    def recent(self, n: int = 50) -> list[dict[str, Any]]:
        return self.feed[-int(n):]

    async def deliberate(
        self,
        prompt: str,
        personas: list,
        *,
        model_complete=None,
        difficulty: float = 0.6,
        two_person_attested: bool = False,
        debate: bool = False,
        publish_to_lounge: bool = False,
    ) -> dict[str, Any]:
        from .loop import run_turn

        async def _one_turn(p, turn_prompt: str):
            return await run_turn(
                p, turn_prompt, model_complete=model_complete,
                difficulty=difficulty,
                two_person_attested=two_person_attested)

        # A council round is independent fan-out. Preserve input order in the
        # evidence, but spend one bounded deadline instead of N sequential ones.
        rounds = list(await asyncio.gather(*(
            _one_turn(p, prompt) for p in personas
        )))
        for p, turn in zip(personas, rounds):
            turn["round"] = 1
            src = ("model-unverified" if turn.get("answer") is not None
                   and not turn.get("stub") else "persona-fallback")
            if publish_to_lounge:
                self.post(p.name, turn.get("answer") or turn.get("honesty"), source=src)

        mode = "single-round"
        # Debate-then-converge (after arXiv:2305.14325, Multiagent Debate): one
        # bounded extra round where each persona reads its peers' round-1 positions,
        # states dissent or agreement explicitly, and revises. EXACTLY two rounds —
        # never an open-ended loop. Runs only when a real/stub backend is injected
        # and at least two personas answered (an honest debate needs peers).
        if debate and model_complete is not None:
            positions = [
                (r.get("persona"), r.get("answer"))
                for r in rounds if r.get("answer")
            ]
            if len(positions) >= 2:
                mode = "debate"
                debate_jobs = []
                for p in personas:
                    peers = "\n\n".join(
                        f"[{name}] {ans[:1200]}" for name, ans in positions
                        if name != p.name
                    )
                    debate_prompt = (
                        "COUNCIL DEBATE ROUND (bounded: this is the final round).\n"
                        f"Original question:\n{prompt}\n\n"
                        f"Your peers' opening positions:\n{peers}\n\n"
                        "State explicitly where you agree or dissent, then give "
                        "your revised answer. Honest dissent beats false consensus."
                    )
                    debate_jobs.append((p, debate_prompt))
                revised = list(await asyncio.gather(*(
                    _one_turn(p, turn_prompt) for p, turn_prompt in debate_jobs
                )))
                for (p, _turn_prompt), turn in zip(debate_jobs, revised):
                    turn["round"] = 2
                    src = ("model-unverified" if turn.get("answer") is not None
                           and not turn.get("stub") else "persona-fallback")
                    if publish_to_lounge:
                        self.post(p.name, turn.get("answer") or turn.get("honesty"),
                                  source=src)
                    rounds.append(turn)
            else:
                mode = "single-round"

        return {
            "prompt": prompt,
            "participants": [p.name for p in personas],
            "rounds": rounds,
            "mode": mode,
            "published_to_lounge": publish_to_lounge,
            "note": "bounded council; each turn honest (no fabrication when a model "
                    "backend is absent); debate mode = exactly two rounds, "
                    "after arXiv:2305.14325",
        }