agent-roundtable 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,107 @@
1
+ """Base adapter interface for Roundtable.
2
+
3
+ Defines the ``RoundtableAdapter`` abstract base class that all agent
4
+ framework adapters must implement. The lifecycle is managed by
5
+ ``RoundtableCore`` — adapters only need to implement ``get_persona``,
6
+ ``speak``, and ``listen``.
7
+
8
+ Usage::
9
+
10
+ from roundtable.adapters.base import RoundtableAdapter
11
+
12
+ class MyAdapter(RoundtableAdapter):
13
+ def get_persona(self):
14
+ return {"name": "Alice", "role": "engineer", "avatar": "👩‍💻"}
15
+
16
+ async def speak(self, context):
17
+ return "My opinion on " + context["topic"]
18
+
19
+ async def listen(self, speech):
20
+ return None # no special reaction
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from abc import ABC, abstractmethod
26
+ from typing import Any
27
+
28
+
29
+ class RoundtableAdapter(ABC):
30
+ """Abstract base class for Roundtable agent adapters.
31
+
32
+ Each adapter wraps one agent framework (Hermes, LangChain, custom, etc.)
33
+ and exposes a uniform interface for the roundtable discussion engine.
34
+
35
+ Lifecycle (managed by RoundtableCore):
36
+ 1. ``__init__`` — adapter is instantiated once per discussion
37
+ 2. ``get_persona`` — called before first turn to get agent metadata
38
+ 3. ``speak`` / ``listen`` — called per turn in round-robin order
39
+
40
+ Error handling:
41
+ - If ``speak`` raises, the agent is marked as error and skipped
42
+ for this round; the discussion continues.
43
+ - If ``listen`` raises, the reaction is silently dropped.
44
+ - Timeouts are enforced by RoundtableCore (default 60s per speak).
45
+ """
46
+
47
+ @abstractmethod
48
+ def get_persona(self) -> dict[str, Any]:
49
+ """Return agent personality metadata.
50
+
51
+ Returns:
52
+ dict with keys:
53
+ - ``name`` (str, required): Display name
54
+ - ``role`` (str, required): Role identifier (e.g. "engineer")
55
+ - ``avatar`` (str, optional): Emoji or image URL
56
+ - ``title`` (str, optional): Role title (e.g. "技术总监")
57
+ - ``description`` (str, optional): Role description
58
+ """
59
+ ...
60
+
61
+ @abstractmethod
62
+ async def speak(self, context: dict[str, Any]) -> str:
63
+ """Generate a speech for the current turn.
64
+
65
+ Args:
66
+ context: Discussion context dict with keys:
67
+ - ``topic`` (str): Discussion topic
68
+ - ``history`` (list[dict]): All speeches so far
69
+ - ``round`` (int): Current round number (1-indexed)
70
+ - ``findings`` (list[dict]): Consensus/disagreement points
71
+ - ``participants`` (list[dict]): All participant personas
72
+
73
+ Returns:
74
+ Speech content as a Markdown string.
75
+ """
76
+ ...
77
+
78
+ @abstractmethod
79
+ async def listen(self, speech: dict[str, Any]) -> dict[str, Any] | None:
80
+ """React to another agent's speech.
81
+
82
+ Called for every speech that isn't the current agent's own.
83
+ Return ``None`` for no reaction, or a dict with metadata.
84
+
85
+ Args:
86
+ speech: Dict with keys:
87
+ - ``speaker`` (str): Speaker agent name
88
+ - ``content`` (str): Speech content
89
+ - ``round`` (int): Round number
90
+
91
+ Returns:
92
+ ``None`` or ``{"reaction": "agree|disagree|question", "note": str}``
93
+ """
94
+ ...
95
+
96
+ def on_round_start(self, round_num: int) -> None:
97
+ """Hook called at the start of each round.
98
+
99
+ Override to implement per-round setup logic (e.g. fetch new context).
100
+ Default is a no-op.
101
+ """
102
+
103
+ def on_discussion_end(self, conclusion: str) -> None:
104
+ """Hook called when the discussion concludes.
105
+
106
+ Override for cleanup or summary logic. Default is a no-op.
107
+ """
@@ -0,0 +1,247 @@
1
+ """Generic Python API adapter for Roundtable.
2
+
3
+ For use outside any specific agent framework. Provides a simple,
4
+ importable interface that works in any Python script.
5
+
6
+ Usage::
7
+
8
+ from roundtable.adapters.generic import Roundtable
9
+
10
+ rt = Roundtable()
11
+ result = rt.init(topic="...", participants=[...])
12
+ result = rt.speak(discussion_id, "alice", "Hello!")
13
+
14
+ With notifications::
15
+
16
+ def my_send(platform, chat_id, message):
17
+ print(f"[{platform}:{chat_id}] {message}")
18
+
19
+ rt = Roundtable(send_fn=my_send)
20
+ result = rt.init(
21
+ topic="...",
22
+ participants=[...],
23
+ notifications={
24
+ "enabled": True,
25
+ "channels": [{"platform": "console", "chat_id": "default"}],
26
+ },
27
+ )
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import builtins
33
+ from collections.abc import Callable
34
+ from typing import Any
35
+
36
+ from roundtable.core import RoundtableCore
37
+ from roundtable.db import RoundtableDB
38
+
39
+
40
+ class Roundtable:
41
+ """Simple facade over RoundtableCore.
42
+
43
+ All methods return dicts (JSON-serializable). Errors are returned
44
+ as ``{"error": "message"}`` dicts instead of raising exceptions,
45
+ making it safe for untrusted callers.
46
+
47
+ Args:
48
+ db_path: Optional path to the SQLite database file.
49
+ send_fn: Optional callback(platform, chat_id, message) for notifications.
50
+ """
51
+
52
+ def __init__(
53
+ self,
54
+ db_path: str | None = None,
55
+ send_fn: Callable[[str, str, str], None] | None = None,
56
+ ):
57
+ db = RoundtableDB(db_path) if db_path else RoundtableDB()
58
+ self._core = RoundtableCore(db, send_fn=send_fn)
59
+
60
+ def init(
61
+ self,
62
+ topic: str,
63
+ participants: builtins.list[dict[str, Any]],
64
+ *,
65
+ notifications: dict[str, Any] | None = None,
66
+ web: bool = False,
67
+ web_port: int = 8199,
68
+ **kwargs: Any,
69
+ ) -> dict[str, Any]:
70
+ """Create a new discussion.
71
+
72
+ Args:
73
+ topic: Discussion topic.
74
+ participants: List of participant dicts (min 2).
75
+ notifications: Optional notification config dict.
76
+ web: If True, start a web viewer for live viewing.
77
+ web_port: Port for the web viewer (default 8199).
78
+ **kwargs: Additional arguments passed to create_discussion.
79
+ """
80
+ try:
81
+ return self._core.create_discussion(
82
+ topic,
83
+ participants,
84
+ notifications=notifications,
85
+ web=web,
86
+ web_port=web_port,
87
+ **kwargs,
88
+ )
89
+ except (ValueError, Exception) as e:
90
+ return {"error": str(e)}
91
+
92
+ def speak(
93
+ self,
94
+ discussion_id: str,
95
+ participant: str,
96
+ content: str,
97
+ **kwargs: Any,
98
+ ) -> dict[str, Any]:
99
+ """Record a speech."""
100
+ try:
101
+ return self._core.speak(discussion_id, participant, content, **kwargs)
102
+ except Exception as e:
103
+ return {"error": str(e)}
104
+
105
+ def read(self, discussion_id: str, **kwargs: Any) -> dict[str, Any]:
106
+ """Read discussion history."""
107
+ try:
108
+ return self._core.read(discussion_id, **kwargs)
109
+ except Exception as e:
110
+ return {"error": str(e)}
111
+
112
+ def get_status(self, discussion_id: str) -> dict[str, Any]:
113
+ """Get discussion status."""
114
+ try:
115
+ return self._core.status(discussion_id)
116
+ except Exception as e:
117
+ return {"error": str(e)}
118
+
119
+ def summarize(self, discussion_id: str, *, compact: bool = False) -> dict[str, Any]:
120
+ """Get summary data."""
121
+ try:
122
+ return self._core.summarize(discussion_id, compact=compact)
123
+ except Exception as e:
124
+ return {"error": str(e)}
125
+
126
+ def end(
127
+ self,
128
+ discussion_id: str,
129
+ *,
130
+ force: bool = False,
131
+ conclusion: str | None = None,
132
+ ) -> dict[str, Any]:
133
+ """End a discussion."""
134
+ try:
135
+ return self._core.end_discussion(discussion_id, force=force, conclusion=conclusion)
136
+ except Exception as e:
137
+ return {"error": str(e)}
138
+
139
+ def list(self, **kwargs: Any) -> dict[str, Any]:
140
+ """List discussions."""
141
+ try:
142
+ return self._core.list_discussions(**kwargs)
143
+ except Exception as e:
144
+ return {"error": str(e)}
145
+
146
+ def advance(self, discussion_id: str) -> dict[str, Any]:
147
+ """Explicitly advance to the next round.
148
+
149
+ Use when auto-advance doesn't trigger. If max_rounds is exceeded,
150
+ the discussion is automatically concluded.
151
+ """
152
+ try:
153
+ return self._core.advance(discussion_id)
154
+ except Exception as e:
155
+ return {"error": str(e)}
156
+
157
+ def run_demo(
158
+ self,
159
+ *,
160
+ topic: str | None = None,
161
+ participants: builtins.list[dict[str, Any]] | None = None,
162
+ max_rounds: int = 3,
163
+ verbose: bool = True,
164
+ web: bool = False,
165
+ web_port: int = 8199,
166
+ ) -> dict[str, Any]:
167
+ """Run a complete demo discussion with pre-scripted content.
168
+
169
+ Simulates a realistic multi-round discussion. Prints formatted
170
+ output to terminal when verbose=True.
171
+
172
+ Args:
173
+ topic: Custom topic (uses default demo topic if None).
174
+ participants: Custom participants (uses default if None).
175
+ max_rounds: Number of rounds (default 3).
176
+ verbose: Print formatted output to stdout.
177
+ web: If True, start a web viewer and publish speeches live.
178
+ web_port: Preferred port for the web viewer (default 8199).
179
+ """
180
+ try:
181
+ return self._core.run_demo(
182
+ topic=topic,
183
+ participants=participants,
184
+ max_rounds=max_rounds,
185
+ verbose=verbose,
186
+ web=web,
187
+ web_port=web_port,
188
+ )
189
+ except Exception as e:
190
+ return {"error": str(e)}
191
+
192
+ def notify(
193
+ self,
194
+ discussion_id: str,
195
+ event: str,
196
+ **kwargs: Any,
197
+ ) -> dict[str, Any]:
198
+ """Manually trigger a notification for a discussion event.
199
+
200
+ Valid events: round_start, speech, round_end, concluded.
201
+ """
202
+ try:
203
+ return self._core.notify(discussion_id, event, **kwargs)
204
+ except Exception as e:
205
+ return {"error": str(e)}
206
+
207
+ # ------------------------------------------------------------------
208
+ # API Aliases
209
+ # ------------------------------------------------------------------
210
+
211
+ def create_discussion(
212
+ self,
213
+ topic: str,
214
+ participants: builtins.list[dict[str, Any]],
215
+ *,
216
+ notifications: dict[str, Any] | None = None,
217
+ web: bool = False,
218
+ web_port: int = 8199,
219
+ **kwargs: Any,
220
+ ) -> dict[str, Any]:
221
+ """Create a new discussion. Alias for init."""
222
+ return self.init(
223
+ topic,
224
+ participants,
225
+ notifications=notifications,
226
+ web=web,
227
+ web_port=web_port,
228
+ **kwargs,
229
+ )
230
+
231
+ def end_discussion(
232
+ self,
233
+ discussion_id: str,
234
+ *,
235
+ force: bool = False,
236
+ conclusion: str | None = None,
237
+ ) -> dict[str, Any]:
238
+ """End a discussion. Alias for end."""
239
+ return self.end(discussion_id, force=force, conclusion=conclusion)
240
+
241
+ def list_discussions(self, **kwargs: Any) -> dict[str, Any]:
242
+ """List discussions. Alias for list."""
243
+ return self.list(**kwargs)
244
+
245
+ def status(self, discussion_id: str) -> dict[str, Any]:
246
+ """Get discussion status. Alias for get_status."""
247
+ return self.get_status(discussion_id)