proscenium 0.0.1__py3-none-any.whl → 0.0.3__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.
Files changed (36) hide show
  1. proscenium/__init__.py +3 -0
  2. proscenium/admin/__init__.py +37 -0
  3. proscenium/bin/bot.py +142 -0
  4. proscenium/core/__init__.py +152 -0
  5. proscenium/interfaces/__init__.py +3 -0
  6. proscenium/interfaces/slack.py +265 -0
  7. proscenium/patterns/__init__.py +3 -0
  8. proscenium/patterns/chunk_space.py +51 -0
  9. proscenium/{scripts → patterns}/document_enricher.py +15 -11
  10. proscenium/{scripts → patterns}/entity_resolver.py +24 -18
  11. proscenium/patterns/graph_rag.py +61 -0
  12. proscenium/{scripts → patterns}/knowledge_graph.py +4 -2
  13. proscenium/{scripts → patterns}/rag.py +6 -12
  14. proscenium/{scripts → patterns}/tools.py +13 -45
  15. proscenium/verbs/__init__.py +3 -0
  16. proscenium/verbs/chunk.py +2 -0
  17. proscenium/verbs/complete.py +24 -28
  18. proscenium/verbs/display/__init__.py +1 -1
  19. proscenium/verbs/display.py +3 -0
  20. proscenium/verbs/extract.py +8 -4
  21. proscenium/verbs/invoke.py +3 -0
  22. proscenium/verbs/read.py +6 -8
  23. proscenium/verbs/remember.py +5 -0
  24. proscenium/verbs/vector_database.py +13 -20
  25. proscenium/verbs/write.py +3 -0
  26. {proscenium-0.0.1.dist-info → proscenium-0.0.3.dist-info}/METADATA +5 -8
  27. proscenium-0.0.3.dist-info/RECORD +34 -0
  28. {proscenium-0.0.1.dist-info → proscenium-0.0.3.dist-info}/WHEEL +1 -1
  29. proscenium-0.0.3.dist-info/entry_points.txt +3 -0
  30. proscenium/scripts/__init__.py +0 -0
  31. proscenium/scripts/chunk_space.py +0 -33
  32. proscenium/scripts/graph_rag.py +0 -43
  33. proscenium/verbs/display/huggingface.py +0 -0
  34. proscenium/verbs/know.py +0 -9
  35. proscenium-0.0.1.dist-info/RECORD +0 -30
  36. {proscenium-0.0.1.dist-info → proscenium-0.0.3.dist-info}/LICENSE +0 -0
proscenium/__init__.py CHANGED
@@ -0,0 +1,3 @@
1
+ import logging
2
+
3
+ logging.getLogger(__name__).addHandler(logging.NullHandler())
@@ -0,0 +1,37 @@
1
+ from typing import Generator
2
+ from typing import List
3
+ from typing import Optional
4
+
5
+ import logging
6
+
7
+ from proscenium.core import Prop
8
+ from proscenium.core import Character
9
+ from rich.console import Console
10
+
11
+ logging.getLogger(__name__).addHandler(logging.NullHandler())
12
+
13
+ log = logging.getLogger(__name__)
14
+
15
+ system_message = """
16
+ You are an administrator of a chatbot.
17
+ """
18
+
19
+
20
+ def props(console: Optional[Console]) -> List[Prop]:
21
+
22
+ return []
23
+
24
+
25
+ class Admin(Character):
26
+
27
+ def __init__(self, admin_channel_id: str):
28
+ super().__init__(admin_channel_id)
29
+ self.admin_channel_id = admin_channel_id
30
+
31
+ def handle(
32
+ channel_id: str,
33
+ speaker_id: str,
34
+ question: str,
35
+ ) -> Generator[tuple[str, str], None, None]:
36
+
37
+ yield channel_id, "I am the administrator of this chat system."
proscenium/bin/bot.py ADDED
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import os
4
+ import sys
5
+ import logging
6
+ import typer
7
+ import importlib
8
+ from rich.console import Console
9
+
10
+ from proscenium.admin import Admin
11
+
12
+ from proscenium.interfaces.slack import (
13
+ get_slack_auth,
14
+ channel_table,
15
+ bot_user_id,
16
+ places_table,
17
+ channel_maps,
18
+ make_slack_listener,
19
+ connect,
20
+ send_curtain_up,
21
+ listen,
22
+ send_curtain_down,
23
+ shutdown,
24
+ )
25
+
26
+ from proscenium.verbs.display import header
27
+
28
+ logging.basicConfig(
29
+ stream=sys.stdout,
30
+ format="%(asctime)s %(levelname)-8s %(name)s: %(message)s",
31
+ level=logging.WARNING,
32
+ )
33
+
34
+ logging.basicConfig(
35
+ stream=sys.stdout,
36
+ format="%(asctime)s %(levelname)-8s %(name)s: %(message)s",
37
+ level=logging.WARNING,
38
+ )
39
+
40
+ app = typer.Typer(help="Proscenium Bot")
41
+
42
+ log = logging.getLogger(__name__)
43
+
44
+
45
+ @app.command(help="""Start the Proscenium Bot.""")
46
+ def start(
47
+ verbose: bool = False,
48
+ production_module_name: str = typer.Option(
49
+ "demo.production",
50
+ "-p",
51
+ "--production",
52
+ help="The name of the python module in PYTHONPATH in which the variable production of type proscenium.core.Production is defined.",
53
+ ),
54
+ force_rebuild: bool = False,
55
+ ):
56
+
57
+ console = Console()
58
+ sub_console = None
59
+
60
+ if verbose:
61
+ log.setLevel(logging.INFO)
62
+ logging.getLogger("proscenium").setLevel(logging.INFO)
63
+ logging.getLogger("demo").setLevel(logging.INFO)
64
+ sub_console = console
65
+
66
+ console.print(header())
67
+
68
+ production_module = importlib.import_module(production_module_name, package=None)
69
+
70
+ slack_admin_channel_id = os.environ.get("SLACK_ADMIN_CHANNEL_ID")
71
+ # Note that the checking of the existence of the admin channel id is delayed
72
+ # until after the subscribed channels are shown.
73
+
74
+ production = production_module.make_production(slack_admin_channel_id, sub_console)
75
+
76
+ console.print("Preparing props...")
77
+ production.prepare_props()
78
+ console.print("Props are up-to-date.")
79
+
80
+ slack_app_token, slack_bot_token = get_slack_auth()
81
+
82
+ socket_mode_client = connect(slack_app_token, slack_bot_token)
83
+
84
+ user_id = bot_user_id(socket_mode_client, console)
85
+ console.print()
86
+
87
+ channels_by_id, channel_name_to_id = channel_maps(socket_mode_client)
88
+ console.print(channel_table(channels_by_id))
89
+ console.print()
90
+
91
+ if slack_admin_channel_id is None:
92
+ raise ValueError(
93
+ "SLACK_ADMIN_CHANNEL_ID environment variable not set. "
94
+ "Please set it to the channel ID of the Proscenium admin channel."
95
+ )
96
+ if slack_admin_channel_id not in channels_by_id:
97
+ raise ValueError(
98
+ f"Admin channel {slack_admin_channel_id} not found in subscribed channels."
99
+ )
100
+
101
+ admin = Admin(slack_admin_channel_id)
102
+ log.info("Admin handler started.")
103
+
104
+ log.info("Places, please!")
105
+ channel_id_to_character = production.places(channel_name_to_id)
106
+ channel_id_to_character[slack_admin_channel_id] = admin
107
+
108
+ console.print(places_table(channel_id_to_character, channels_by_id))
109
+ console.print()
110
+
111
+ slack_listener = make_slack_listener(
112
+ user_id,
113
+ slack_admin_channel_id,
114
+ channels_by_id,
115
+ channel_id_to_character,
116
+ console,
117
+ )
118
+
119
+ send_curtain_up(socket_mode_client, production, slack_admin_channel_id)
120
+
121
+ console.print("Starting the show. Listening for events...")
122
+ listen(
123
+ socket_mode_client,
124
+ slack_listener,
125
+ user_id,
126
+ console,
127
+ )
128
+
129
+ send_curtain_down(socket_mode_client, slack_admin_channel_id)
130
+
131
+ shutdown(
132
+ socket_mode_client,
133
+ slack_listener,
134
+ user_id,
135
+ production,
136
+ console,
137
+ )
138
+
139
+
140
+ if __name__ == "__main__":
141
+
142
+ app()
@@ -0,0 +1,152 @@
1
+ from typing import Generator
2
+ from typing import Optional
3
+ import logging
4
+ from rich.console import Console
5
+
6
+ logging.getLogger(__name__).addHandler(logging.NullHandler())
7
+
8
+ log = logging.getLogger(__name__)
9
+
10
+
11
+ class Prop:
12
+ """
13
+ A `Prop` is a resource available to the `Character`s in a `Scene`.
14
+ """
15
+
16
+ def __init__(
17
+ self,
18
+ console: Optional[Console] = None,
19
+ ):
20
+ self.console = console
21
+
22
+ def name(self) -> str:
23
+ return self.__class__.__name__
24
+
25
+ def description(self) -> str:
26
+ return self.__doc__ or ""
27
+
28
+ def curtain_up_message(self) -> str:
29
+ return f"- {self.name()}, {self.description().strip()}"
30
+
31
+ def already_built(self) -> bool:
32
+ return False
33
+
34
+ def build(self) -> None:
35
+ pass
36
+
37
+
38
+ class Character:
39
+ """
40
+ A `Character` is a participant in a `Scene` that `handle`s utterances from the
41
+ scene by producing its own utterances."""
42
+
43
+ def __init__(self, admin_channel_id: str):
44
+ self.admin_channel_id = admin_channel_id
45
+
46
+ def name(self) -> str:
47
+ return self.__class__.__name__
48
+
49
+ def description(self) -> str:
50
+ return self.__doc__ or ""
51
+
52
+ def curtain_up_message(self) -> str:
53
+ return f"- {self.name()}, {self.description().strip()}"
54
+
55
+ def handle(
56
+ channel_id: str, speaker_id: str, utterance: str
57
+ ) -> Generator[tuple[str, str], None, None]:
58
+ pass
59
+
60
+
61
+ class Scene:
62
+ """
63
+ A `Scene` is a setting in which `Character`s interact with each other and
64
+ with `Prop`s. It is a container for `Character`s and `Prop`s.
65
+ """
66
+
67
+ def __init__(self):
68
+ pass
69
+
70
+ def name(self) -> str:
71
+ return self.__class__.__name__
72
+
73
+ def description(self) -> str:
74
+ return self.__doc__ or ""
75
+
76
+ def curtain_up_message(self) -> str:
77
+
78
+ characters_msg = "\n".join(
79
+ [character.curtain_up_message() for character in self.characters()]
80
+ )
81
+
82
+ props_msg = "\n".join([prop.curtain_up_message() for prop in self.props()])
83
+
84
+ return f"""
85
+ Scene: {self.name()}, {self.description().strip()}
86
+
87
+ Characters:
88
+ {characters_msg}
89
+
90
+ Props:
91
+ {props_msg}
92
+ """
93
+
94
+ def props(self) -> list[Prop]:
95
+ return []
96
+
97
+ def prepare_props(self, force_rebuild: bool = False) -> None:
98
+ for prop in self.props():
99
+ if force_rebuild:
100
+ prop.build()
101
+ elif not prop.already_built():
102
+ log.info("Prop %s not built. Building it now.", prop.name())
103
+ prop.build()
104
+
105
+ def characters(self) -> list[Character]:
106
+ return []
107
+
108
+ def places(self) -> dict[str, Character]:
109
+ pass
110
+
111
+ def curtain(self) -> None:
112
+ pass
113
+
114
+
115
+ class Production:
116
+ """
117
+ A `Production` is a collection of `Scene`s."""
118
+
119
+ def __init__(self):
120
+ pass
121
+
122
+ def name(self) -> str:
123
+ return self.__class__.__name__
124
+
125
+ def description(self) -> str:
126
+ return self.__doc__ or ""
127
+
128
+ def prepare_props(self, force_rebuild: bool = False) -> None:
129
+ if force_rebuild:
130
+ log.info("Forcing rebuild of all props.")
131
+ else:
132
+ log.info("Building any missing props...")
133
+
134
+ for scene in self.scenes():
135
+ scene.prepare_props(force_rebuild=force_rebuild)
136
+
137
+ def curtain_up_message(self) -> str:
138
+
139
+ scenes_msg = "\n\n".join(
140
+ [scene.curtain_up_message() for scene in self.scenes()]
141
+ )
142
+
143
+ return f"""Production: {self.name()}, {self.description().strip()}
144
+
145
+ {scenes_msg}"""
146
+
147
+ def scenes(self) -> list[Scene]:
148
+ return []
149
+
150
+ def curtain(self) -> None:
151
+ for scene in self.scenes():
152
+ scene.curtain()
@@ -0,0 +1,3 @@
1
+ import logging
2
+
3
+ logging.getLogger(__name__).addHandler(logging.NullHandler())
@@ -0,0 +1,265 @@
1
+ from typing import Callable
2
+
3
+ from typing import Generator
4
+ import time
5
+ import logging
6
+ import os
7
+ from rich.console import Console
8
+ from rich.table import Table
9
+
10
+ from slack_sdk.web import WebClient
11
+ from slack_sdk.socket_mode import SocketModeClient
12
+ from slack_sdk.socket_mode.request import SocketModeRequest
13
+ from slack_sdk.socket_mode.response import SocketModeResponse
14
+ from slack_sdk.socket_mode.listeners import SocketModeRequestListener
15
+
16
+ from proscenium.core import Production
17
+ from proscenium.core import Character
18
+
19
+ log = logging.getLogger(__name__)
20
+
21
+
22
+ def get_slack_auth() -> tuple[str, str]:
23
+
24
+ slack_app_token = os.environ.get("SLACK_APP_TOKEN")
25
+ if slack_app_token is None:
26
+ raise ValueError(
27
+ "SLACK_APP_TOKEN environment variable not set. "
28
+ "Please set it to the app token of the Proscenium Slack app."
29
+ )
30
+ slack_bot_token = os.environ.get("SLACK_BOT_TOKEN")
31
+ if slack_bot_token is None:
32
+ raise ValueError(
33
+ "SLACK_BOT_TOKEN environment variable not set. "
34
+ "Please set it to the bot token of the Proscenium Slack app."
35
+ )
36
+
37
+ return slack_app_token, slack_bot_token
38
+
39
+
40
+ def connect(app_token: str, bot_token: str) -> SocketModeClient:
41
+
42
+ web_client = WebClient(token=bot_token)
43
+ socket_mode_client = SocketModeClient(app_token=app_token, web_client=web_client)
44
+
45
+ socket_mode_client.connect()
46
+ log.info("Connected to Slack.")
47
+
48
+ return socket_mode_client
49
+
50
+
51
+ def make_slack_listener(
52
+ proscenium_user_id: str,
53
+ admin_channel_id: str,
54
+ channels_by_id: dict,
55
+ channel_id_to_handler: dict[
56
+ str, Callable[[str, str, str], Generator[tuple[str, str], None, None]]
57
+ ],
58
+ console: Console,
59
+ ):
60
+
61
+ def process(client: SocketModeClient, req: SocketModeRequest):
62
+
63
+ if req.type == "events_api":
64
+
65
+ event = req.payload["event"]
66
+
67
+ response = SocketModeResponse(envelope_id=req.envelope_id)
68
+ client.send_socket_mode_response(response)
69
+
70
+ if event.get("type") in [
71
+ "message",
72
+ "app_mention",
73
+ ]:
74
+ speaker_id = event.get("user")
75
+ if speaker_id == proscenium_user_id:
76
+ return
77
+
78
+ text = event.get("text")
79
+ channel_id = event.get("channel")
80
+ console.print(f"{speaker_id} in {channel_id} said something")
81
+
82
+ channel = channels_by_id.get(channel_id, None)
83
+
84
+ if channel is None:
85
+
86
+ # TODO: channels_by_id will get stale
87
+ log.info("No handler for channel id %s", channel_id)
88
+
89
+ else:
90
+
91
+ character = channel_id_to_handler[channel_id]
92
+ log.info("Handler defined for channel id %s", channel_id)
93
+
94
+ # TODO determine whether the handler has a good chance of being useful
95
+
96
+ for receiving_channel_id, response in character.handle(
97
+ channel_id, speaker_id, text
98
+ ):
99
+ response_response = client.web_client.chat_postMessage(
100
+ channel=receiving_channel_id, text=response
101
+ )
102
+ log.info(
103
+ "Response sent to channel %s",
104
+ receiving_channel_id,
105
+ )
106
+ if receiving_channel_id == admin_channel_id:
107
+ continue
108
+
109
+ permalink = client.web_client.chat_getPermalink(
110
+ channel=receiving_channel_id,
111
+ message_ts=response_response["ts"],
112
+ )["permalink"]
113
+ log.info(
114
+ "Response sent to channel %s link %s",
115
+ receiving_channel_id,
116
+ permalink,
117
+ )
118
+ client.web_client.chat_postMessage(
119
+ channel=admin_channel_id,
120
+ text=permalink,
121
+ )
122
+
123
+ elif req.type == "interactive":
124
+ pass
125
+ elif req.type == "slash_commands":
126
+ pass
127
+ elif req.type == "app_home_opened":
128
+ pass
129
+ elif req.type == "block_actions":
130
+ pass
131
+ elif req.type == "message_actions":
132
+ pass
133
+
134
+ return process
135
+
136
+
137
+ def channel_maps(
138
+ socket_mode_client: SocketModeClient,
139
+ ) -> tuple[dict[str, dict], dict[str, str]]:
140
+
141
+ subscribed_channels = socket_mode_client.web_client.users_conversations(
142
+ types="public_channel,private_channel,mpim,im",
143
+ limit=100,
144
+ )
145
+ log.info(
146
+ "Subscribed channels count: %s",
147
+ len(subscribed_channels["channels"]),
148
+ )
149
+
150
+ channels_by_id = {
151
+ channel["id"]: channel for channel in subscribed_channels["channels"]
152
+ }
153
+
154
+ channel_name_to_id = {
155
+ channel["name"]: channel["id"]
156
+ for channel in channels_by_id.values()
157
+ if channel.get("name")
158
+ }
159
+
160
+ return channels_by_id, channel_name_to_id
161
+
162
+
163
+ def channel_table(channels_by_id) -> Table:
164
+ channel_table = Table(title="Subscribed channels")
165
+ channel_table.add_column("Channel ID", justify="left")
166
+ channel_table.add_column("Name", justify="left")
167
+ for channel_id, channel in channels_by_id.items():
168
+ channel_table.add_row(
169
+ channel_id,
170
+ channel.get("name", "-"),
171
+ )
172
+ return channel_table
173
+
174
+
175
+ def bot_user_id(socket_mode_client: SocketModeClient, console: Console):
176
+
177
+ auth_response = socket_mode_client.web_client.auth_test()
178
+
179
+ console.print(auth_response["url"])
180
+ console.print()
181
+ console.print(f"Team '{auth_response["team"]}' ({auth_response["team_id"]})")
182
+ console.print(f"User '{auth_response["user"]}' ({auth_response["user_id"]})")
183
+
184
+ user_id = auth_response["user_id"]
185
+ console.print("Bot id", auth_response["bot_id"])
186
+
187
+ return user_id
188
+
189
+
190
+ def places_table(
191
+ channel_id_to_character: dict[str, Character], channels_by_id: dict[str, dict]
192
+ ) -> Table:
193
+
194
+ table = Table(title="Characters in place")
195
+ table.add_column("Channel ID", justify="left")
196
+ table.add_column("Channel Name", justify="left")
197
+ table.add_column("Character", justify="left")
198
+ for channel_id, character in channel_id_to_character.items():
199
+ channel = channels_by_id[channel_id]
200
+ table.add_row(channel_id, channel["name"], character.name())
201
+
202
+ return table
203
+
204
+
205
+ def send_curtain_up(
206
+ socket_mode_client: SocketModeClient,
207
+ production: Production,
208
+ slack_admin_channel_id: str,
209
+ ) -> None:
210
+
211
+ curtain_up_message = f"""
212
+ Proscenium 🎭 https://the-ai-alliance.github.io/proscenium/
213
+
214
+ ```
215
+ {production.curtain_up_message()}
216
+ ```
217
+
218
+ Curtain up.
219
+ """
220
+
221
+ socket_mode_client.web_client.chat_postMessage(
222
+ channel=slack_admin_channel_id,
223
+ text=curtain_up_message,
224
+ )
225
+
226
+
227
+ def listen(
228
+ socket_mode_client: SocketModeClient,
229
+ slack_listener: SocketModeRequestListener,
230
+ user_id: str,
231
+ console: Console,
232
+ ):
233
+ socket_mode_client.socket_mode_request_listeners.append(slack_listener)
234
+
235
+ try:
236
+ while True:
237
+ time.sleep(1)
238
+ except KeyboardInterrupt:
239
+ console.print("Exiting...")
240
+
241
+
242
+ def send_curtain_down(
243
+ socket_mode_client: SocketModeClient, slack_admin_channel_id: str
244
+ ) -> None:
245
+ socket_mode_client.web_client.chat_postMessage(
246
+ channel=slack_admin_channel_id,
247
+ text="""Curtain down. We hope you enjoyed the show!""",
248
+ )
249
+
250
+
251
+ def shutdown(
252
+ socket_mode_client: SocketModeClient,
253
+ slack_listener: SocketModeRequestListener,
254
+ user_id: str,
255
+ production: Production,
256
+ console: Console,
257
+ ):
258
+
259
+ socket_mode_client.socket_mode_request_listeners.remove(slack_listener)
260
+ socket_mode_client.disconnect()
261
+ console.print("Disconnected from Slack.")
262
+
263
+ production.curtain()
264
+
265
+ console.print("Handlers stopped.")
@@ -0,0 +1,3 @@
1
+ import logging
2
+
3
+ logging.getLogger(__name__).addHandler(logging.NullHandler())
@@ -0,0 +1,51 @@
1
+ from typing import Optional
2
+ import logging
3
+ from rich.console import Console
4
+ from pymilvus import model
5
+
6
+ from proscenium.verbs.read import load_file
7
+ from proscenium.verbs.chunk import documents_to_chunks_by_characters
8
+ from proscenium.verbs.display.milvus import collection_panel
9
+ from proscenium.verbs.vector_database import vector_db
10
+ from proscenium.verbs.vector_database import create_collection
11
+ from proscenium.verbs.vector_database import add_chunks_to_vector_db
12
+
13
+ log = logging.getLogger(__name__)
14
+
15
+
16
+ def load_chunks_from_files(
17
+ data_files: list[str],
18
+ milvus_uri: str,
19
+ embedding_fn: model.dense.SentenceTransformerEmbeddingFunction,
20
+ collection_name: str,
21
+ console: Optional[Console] = None,
22
+ ) -> None:
23
+
24
+ vector_db_client = vector_db(milvus_uri)
25
+ log.info("Vector db stored at %s", milvus_uri)
26
+
27
+ for data_file in data_files:
28
+
29
+ log.info(
30
+ "Loading data file %s into vector db %s collection %s",
31
+ data_file,
32
+ milvus_uri,
33
+ collection_name,
34
+ )
35
+ create_collection(vector_db_client, embedding_fn, collection_name)
36
+
37
+ documents = load_file(data_file)
38
+ chunks = documents_to_chunks_by_characters(documents)
39
+ log.info("Data file %s has %s chunks", data_file, len(chunks))
40
+
41
+ info = add_chunks_to_vector_db(
42
+ vector_db_client,
43
+ embedding_fn,
44
+ chunks,
45
+ collection_name,
46
+ )
47
+ log.info("%s chunks inserted ", info["insert_count"])
48
+ if console is not None:
49
+ console.print(collection_panel(vector_db_client, collection_name))
50
+
51
+ vector_db_client.close()