Metadata-Version: 2.5
Name: dot-agora
Version: 4.0.0
Summary: Governed conversations between humans, LLM assistants and tools
Project-URL: Homepage, https://gitlab.com/deepika6190303/deepika-open-toolbox/dot-agora
Project-URL: Repository, https://gitlab.com/deepika6190303/deepika-open-toolbox/dot-agora
Project-URL: Issues, https://gitlab.com/deepika6190303/deepika-open-toolbox/dot-agora/-/issues
Author-email: Clément OLIVIER <clement.olivier@deepika.ai>
License-Expression: MIT
License-File: LICENSE.md
Keywords: agent,conversation,deepika,governance,human-in-the-loop,llm,multi-agent,open-toolbox
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Python: <3.14,>=3.12
Requires-Dist: attrs>=26.1.0
Requires-Dist: cattrs>=26.2.1
Provides-Extra: hosts
Requires-Dist: fastapi>=0.115; extra == 'hosts'
Requires-Dist: httpx>=0.27; extra == 'hosts'
Requires-Dist: prompt-toolkit>=3.0.53; extra == 'hosts'
Requires-Dist: rich>=13.0; extra == 'hosts'
Requires-Dist: textual>=8.0; extra == 'hosts'
Requires-Dist: uvicorn>=0.30; extra == 'hosts'
Description-Content-Type: text/markdown

# dot-agora

[![PyPI](https://img.shields.io/pypi/v/dot-agora)](https://pypi.org/project/dot-agora/)
![Python Version](https://img.shields.io/badge/python-3.12%2B-blue)
[![Licence: MIT](https://img.shields.io/badge/licence-MIT-blue)](https://gitlab.com/deepika6190303/deepika-open-toolbox/dot-agora/-/blob/main/LICENSE.md)
[![Pipeline](https://gitlab.com/deepika6190303/deepika-open-toolbox/dot-agora/badges/main/pipeline.svg)](https://gitlab.com/deepika6190303/deepika-open-toolbox/dot-agora/-/pipelines)

**Governed conversations between humans, LLM assistants and tools.**

Participants submit **Intents**. An **Institution** decides each one against its **Protocols**. The **Journal**
keeps every **Event**, refusals included. A protocol says what each participant may do, must do, or must not do
(for instance "Bob must reply to #7 before anything else"), and every state is read back from the history.

```python
import asyncio

from dot_agora import Runner
from dot_agora.governance import Clause, Event, Intent, May, Protocol, act
from dot_agora.governance.institution import Institution
from dot_agora.journal.projection import accepted, others
from dot_agora.participant.interlocutor import Interlocutor
from dot_agora.runtime.notifiers.step_notifier import StepNotifier


class Board(Protocol):
    """Post on one of the board's topics, to everyone or to present participants; reply once to another's post."""

    def __init__(self, topics):
        self.topics = frozenset(topics)

    def prescribe(self, participant, history):
        events = accepted(history)
        replied = {r for e in events if e.author == participant and e.intent.act == "reply" for r in e.intent.references}
        clauses = {May("post", targets=others(participant, history), params={"topic": self.topics})}
        for e in events:
            if e.intent.act == "post" and e.author != participant and e.idx not in replied:
                clauses.add(May("reply", references={e.idx}))
        return clauses

    @act("post")
    @staticmethod
    def post(topic, payload, *targets) -> Intent:
        return Intent(act="post", targets=targets, payload=payload, params={"topic": topic})

    @act("reply")
    @staticmethod
    def reply(post, payload) -> Intent:
        return Intent(act="reply", payload=payload, references=(post,))


class Replier(Interlocutor):
    """Replies to every post its affordances open to it."""

    async def on_notify(self, since):
        for affordance in await self.afford():
            if affordance.act == "reply":
                [post] = affordance.references
                await self.submit(Board.reply(post, "Noted."))


async def main():
    notifier = StepNotifier()
    conversation = Runner(Institution(protocols=(Board({"weather", "lunch"}),)), notifier)
    alice, bob = Interlocutor("alice", secret="a"), Replier("bob", secret="b")
    await alice.join(conversation)
    await bob.join(conversation)

    await alice.submit(Board.post("weather", "It's raining."))   # accepted
    await alice.submit(Board.post("sports", "What a match!"))    # rejected: not a topic
    await notifier.settle()                                      # bob wakes up and replies

    for event in conversation.journal.events:
        print(event.idx, event.author, event.intent.act, event.ack.status, event.intent.payload)


asyncio.run(main())
```

```
0 alice enroll accepted None
1 alice subscribe accepted None
2 bob enroll accepted None
3 bob subscribe accepted None
4 alice post accepted It's raining.
5 alice post rejected What a match!
6 bob reply accepted Noted.
```

Bob knows nothing of the board's rules: he reads what his affordances open to him, and acts on it.

## The model

- An **Intent** is what a participant submits: an `act`, its `targets`, a `payload`, `references` to earlier
  Events, and `params`. The payload is never judged; targets, references and params are.
- A **Protocol** prescribes, for a participant and a history, a set of clauses: `May` (an affordance it may
  take), `Must` (an affordance it must take; every `May` closes while a `Must` is open) and `MustNot` (an act it
  must not take, which wins over another protocol's offer). Its acts are constructors marked `@act`.
- The **Institution** holds the protocols and the views. `decide(author, intent, history)` returns the Event,
  accepted or rejected; `afford(name, history)` returns what that name may do now. Both are pure. Nothing is
  possible by default, except the lifecycle acts (`enroll`, `withdraw`, `subscribe`, `unsubscribe`).
- The **Journal** keeps every Event, in order: `MemoryJournal`, or `FileJournal(path)` to reopen a conversation
  where it stopped.
- The **Runner** is the conversation that runs: it attests who speaks (a secret per name), asks the Institution,
  records the Event, and notifies the readers. Its cadence is a notifier: `StepNotifier` (inline, deterministic)
  or `TaskNotifier` (one asyncio task per reader, as under a host).
- A participant has two faces: a **Writer** submits, a **Reader** consults its view and is woken by `on_notify`.
  An **Interlocutor** has both.
- A **View** filters what each name reads of the history. A view is given by an `Attribution`, a protocol, and
  may apply from its attribution onward only (`retroactive=False`).

## Installation

```bash
pip install dot-agora               # the core: attrs and cattrs only
pip install "dot-agora[hosts]"      # + the terminal and the HTTP server (rich, textual, FastAPI, uvicorn)
```

## Hosts

With the `hosts` extra, `dot-agora` opens a case, a module that exposes an `institution` and its scripted
`participants()`, and seats a human at the keyboard.

```bash
dot-agora open --case my_app.case --as alice --secret a          # here, in the terminal
dot-agora serve --case my_app.case --port 8788                   # here, exposed over HTTP
dot-agora join http://127.0.0.1:8788 --as charlie --secret c     # seated from elsewhere
```

`--human bob` lets a human hold a scripted participant, and `--plain` keeps the terminal line by line.

## Examples

The repository's [`examples/`](https://gitlab.com/deepika6190303/deepika-open-toolbox/dot-agora/-/tree/main/examples)
plays the core alone, in four circles: writing a protocol (`uc0_protocole`), arbitration between protocols
(`uc1_arbitrage`), the lifecycle, the gate and resuming (`uc2_cycle_de_vie`), and views (`uc3_vue`). Each circle has
an institution and numbered groups, played in a governance layer and a conversation layer.

```bash
uv run python -m examples                                   # every circle
uv run python -m examples uc1_arbitrage                     # one circle
uv run python -m examples --notifier task                   # the conversation layer in tasks, as under a host
uv run dot-agora open --case examples.uc0_protocole.02_repondre --as alice --secret a
```

## Documentation

| Document | Contents |
|---|---|
| [docs/ARCHITECTURE.md](https://gitlab.com/deepika6190303/deepika-open-toolbox/dot-agora/-/blob/main/docs/ARCHITECTURE.md) | The model and how the conversation runs |
| [docs/DEVELOPMENT.md](https://gitlab.com/deepika6190303/deepika-open-toolbox/dot-agora/-/blob/main/docs/DEVELOPMENT.md) | Environment setup, tests, code style |
| [docs/VERSIONING.md](https://gitlab.com/deepika6190303/deepika-open-toolbox/dot-agora/-/blob/main/docs/VERSIONING.md) | Versioning and how to depend on this package |
| [docs/PUBLISHING.md](https://gitlab.com/deepika6190303/deepika-open-toolbox/dot-agora/-/blob/main/docs/PUBLISHING.md) | Cutting a release |
| [CHANGELOG.md](https://gitlab.com/deepika6190303/deepika-open-toolbox/dot-agora/-/blob/main/CHANGELOG.md) | Release history |

## Contributing

Contributions are welcome. See
[CONTRIBUTING.md](https://gitlab.com/deepika6190303/deepika-open-toolbox/dot-agora/-/blob/main/CONTRIBUTING.md) for
the licensing terms that apply to contributions and how to submit a change.

## Licence

Copyright (C) 2026 Kannon For Deep Tech (deepika)

This software is distributed under the MIT License; see
[LICENSE.md](https://gitlab.com/deepika6190303/deepika-open-toolbox/dot-agora/-/blob/main/LICENSE.md).
