meshagent-agents 0.0.37__py3-none-any.whl → 0.0.38__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.
Potentially problematic release.
This version of meshagent-agents might be problematic. Click here for more details.
- meshagent/agents/__init__.py +27 -2
- meshagent/agents/adapter.py +18 -9
- meshagent/agents/agent.py +317 -214
- meshagent/agents/chat.py +390 -267
- meshagent/agents/context.py +58 -30
- meshagent/agents/development.py +11 -13
- meshagent/agents/hosting.py +109 -46
- meshagent/agents/indexer.py +241 -224
- meshagent/agents/listener.py +55 -52
- meshagent/agents/mail.py +145 -109
- meshagent/agents/planning.py +294 -199
- meshagent/agents/prompt.py +14 -12
- meshagent/agents/pydantic.py +98 -61
- meshagent/agents/schemas/__init__.py +11 -0
- meshagent/agents/schemas/document.py +32 -21
- meshagent/agents/schemas/gallery.py +23 -14
- meshagent/agents/schemas/presentation.py +33 -17
- meshagent/agents/schemas/schema.py +99 -45
- meshagent/agents/schemas/super_editor_document.py +52 -46
- meshagent/agents/single_shot_writer.py +37 -31
- meshagent/agents/thread_schema.py +74 -32
- meshagent/agents/utils.py +20 -12
- meshagent/agents/version.py +1 -1
- meshagent/agents/worker.py +48 -28
- meshagent/agents/writer.py +36 -23
- meshagent_agents-0.0.38.dist-info/METADATA +64 -0
- meshagent_agents-0.0.38.dist-info/RECORD +30 -0
- meshagent_agents-0.0.37.dist-info/METADATA +0 -36
- meshagent_agents-0.0.37.dist-info/RECORD +0 -30
- {meshagent_agents-0.0.37.dist-info → meshagent_agents-0.0.38.dist-info}/WHEEL +0 -0
- {meshagent_agents-0.0.37.dist-info → meshagent_agents-0.0.38.dist-info}/licenses/LICENSE +0 -0
- {meshagent_agents-0.0.37.dist-info → meshagent_agents-0.0.38.dist-info}/top_level.txt +0 -0
meshagent/agents/chat.py
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
from .agent import SingleRoomAgent, AgentChatContext
|
|
1
|
+
from .agent import SingleRoomAgent, AgentChatContext
|
|
2
2
|
from meshagent.api.chan import Chan
|
|
3
|
-
from meshagent.api import
|
|
3
|
+
from meshagent.api import (
|
|
4
|
+
RoomMessage,
|
|
5
|
+
RoomClient,
|
|
6
|
+
RemoteParticipant,
|
|
7
|
+
RequiredSchema,
|
|
8
|
+
Requirement,
|
|
9
|
+
Element,
|
|
10
|
+
MeshDocument,
|
|
11
|
+
)
|
|
4
12
|
from meshagent.tools import Toolkit, ToolContext
|
|
5
13
|
from .adapter import LLMAdapter, ToolResponseAdapter
|
|
6
14
|
from meshagent.openai.tools.responses_adapter import ImageGenerationTool
|
|
@@ -8,11 +16,9 @@ import asyncio
|
|
|
8
16
|
from typing import Optional
|
|
9
17
|
import logging
|
|
10
18
|
from meshagent.tools import MultiToolkit
|
|
11
|
-
import urllib
|
|
12
19
|
import uuid
|
|
13
20
|
import datetime
|
|
14
|
-
import
|
|
15
|
-
from typing import Literal, Optional
|
|
21
|
+
from typing import Literal
|
|
16
22
|
import base64
|
|
17
23
|
from openai.types.responses import ResponseStreamEvent
|
|
18
24
|
from asyncio import CancelledError
|
|
@@ -24,20 +30,20 @@ tracer = trace.get_tracer("meshagent.chatbot")
|
|
|
24
30
|
logger = logging.getLogger("chat")
|
|
25
31
|
|
|
26
32
|
|
|
27
|
-
|
|
28
33
|
class ChatBotThreadOpenAIImageGenerationTool(ImageGenerationTool):
|
|
29
|
-
def __init__(
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
30
36
|
*,
|
|
31
|
-
background: Literal["transparent","opaque","auto"] = None,
|
|
37
|
+
background: Literal["transparent", "opaque", "auto"] = None,
|
|
32
38
|
input_image_mask_url: Optional[str] = None,
|
|
33
39
|
model: Optional[str] = None,
|
|
34
40
|
moderation: Optional[str] = None,
|
|
35
41
|
output_compression: Optional[int] = None,
|
|
36
|
-
output_format: Optional[Literal["png","webp","jpeg"]] = None,
|
|
42
|
+
output_format: Optional[Literal["png", "webp", "jpeg"]] = None,
|
|
37
43
|
partial_images: Optional[int] = None,
|
|
38
44
|
quality: Optional[Literal["auto", "low", "medium", "high"]] = None,
|
|
39
|
-
size: Optional[Literal["1024x1024","1024x1536","1536x1024","auto"]] = None,
|
|
40
|
-
thread_context:
|
|
45
|
+
size: Optional[Literal["1024x1024", "1024x1536", "1536x1024", "auto"]] = None,
|
|
46
|
+
thread_context: "ChatThreadContext",
|
|
41
47
|
):
|
|
42
48
|
super().__init__(
|
|
43
49
|
background=background,
|
|
@@ -48,57 +54,80 @@ class ChatBotThreadOpenAIImageGenerationTool(ImageGenerationTool):
|
|
|
48
54
|
output_format=output_format,
|
|
49
55
|
partial_images=partial_images,
|
|
50
56
|
quality=quality,
|
|
51
|
-
size=size
|
|
57
|
+
size=size,
|
|
52
58
|
)
|
|
53
59
|
|
|
54
60
|
self.thread_context = thread_context
|
|
55
61
|
|
|
56
|
-
async def on_image_generation_partial(
|
|
57
|
-
|
|
62
|
+
async def on_image_generation_partial(
|
|
63
|
+
self,
|
|
64
|
+
context,
|
|
65
|
+
*,
|
|
66
|
+
item_id,
|
|
67
|
+
output_index,
|
|
68
|
+
sequence_number,
|
|
69
|
+
type,
|
|
70
|
+
partial_image_b64,
|
|
71
|
+
partial_image_index,
|
|
72
|
+
size,
|
|
73
|
+
quality,
|
|
74
|
+
background,
|
|
75
|
+
output_format,
|
|
76
|
+
**extra,
|
|
77
|
+
):
|
|
58
78
|
output_format = self.output_format
|
|
59
|
-
if output_format
|
|
79
|
+
if output_format is None:
|
|
60
80
|
output_format = "png"
|
|
61
81
|
|
|
62
82
|
image_name = f"{str(uuid.uuid4())}.{output_format}"
|
|
63
83
|
|
|
64
84
|
handle = await context.room.storage.open(path=image_name)
|
|
65
|
-
await context.room.storage.write(
|
|
85
|
+
await context.room.storage.write(
|
|
86
|
+
handle=handle, data=base64.b64decode(partial_image_b64)
|
|
87
|
+
)
|
|
66
88
|
await context.room.storage.close(handle=handle)
|
|
67
89
|
|
|
68
|
-
|
|
69
90
|
messages = None
|
|
70
91
|
|
|
71
92
|
for prop in self.thread_context.thread.root.get_children():
|
|
72
|
-
|
|
73
93
|
if prop.tag_name == "messages":
|
|
74
|
-
|
|
75
94
|
messages = prop
|
|
76
95
|
|
|
77
96
|
for child in messages.get_children():
|
|
78
|
-
|
|
79
97
|
if child.get_attribute("id") == item_id:
|
|
80
|
-
|
|
81
98
|
for file in child.get_children():
|
|
82
99
|
file.set_attribute("path", image_name)
|
|
83
100
|
|
|
84
101
|
return
|
|
85
102
|
|
|
86
103
|
message_element = messages.append_child(
|
|
87
|
-
tag_name="message",
|
|
104
|
+
tag_name="message",
|
|
88
105
|
attributes={
|
|
89
|
-
"id"
|
|
90
|
-
"text"
|
|
91
|
-
"created_at"
|
|
92
|
-
|
|
93
|
-
|
|
106
|
+
"id": item_id,
|
|
107
|
+
"text": "",
|
|
108
|
+
"created_at": datetime.datetime.now(datetime.timezone.utc)
|
|
109
|
+
.isoformat()
|
|
110
|
+
.replace("+00:00", "Z"),
|
|
111
|
+
"author_name": context.room.local_participant.get_attribute("name"),
|
|
112
|
+
},
|
|
94
113
|
)
|
|
95
|
-
message_element.append_child(tag_name="file", attributes={
|
|
114
|
+
message_element.append_child(tag_name="file", attributes={"path": image_name})
|
|
96
115
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
116
|
+
async def on_image_generated(
|
|
117
|
+
self,
|
|
118
|
+
context: ToolContext,
|
|
119
|
+
*,
|
|
120
|
+
item_id: str,
|
|
121
|
+
data: bytes,
|
|
122
|
+
status: str,
|
|
123
|
+
size: str,
|
|
124
|
+
quality: str,
|
|
125
|
+
background: str,
|
|
126
|
+
output_format: str,
|
|
127
|
+
**extra,
|
|
128
|
+
):
|
|
100
129
|
output_format = self.output_format
|
|
101
|
-
if output_format
|
|
130
|
+
if output_format is None:
|
|
102
131
|
output_format = "png"
|
|
103
132
|
|
|
104
133
|
image_name = f"{str(uuid.uuid4())}.{output_format}"
|
|
@@ -107,80 +136,94 @@ class ChatBotThreadOpenAIImageGenerationTool(ImageGenerationTool):
|
|
|
107
136
|
await context.room.storage.write(handle=handle, data=data)
|
|
108
137
|
await context.room.storage.close(handle=handle)
|
|
109
138
|
|
|
110
|
-
|
|
111
139
|
messages = None
|
|
112
140
|
|
|
113
141
|
for prop in self.thread_context.thread.root.get_children():
|
|
114
|
-
|
|
115
142
|
if prop.tag_name == "messages":
|
|
116
|
-
|
|
117
143
|
messages = prop
|
|
118
144
|
|
|
119
145
|
for child in messages.get_children():
|
|
120
|
-
|
|
121
146
|
if child.get_attribute("id") == item_id:
|
|
122
|
-
|
|
123
147
|
for file in child.get_children():
|
|
124
148
|
file.set_attribute("path", image_name)
|
|
125
149
|
|
|
126
150
|
return
|
|
127
|
-
|
|
151
|
+
|
|
128
152
|
message_element = messages.append_child(
|
|
129
|
-
tag_name="message",
|
|
153
|
+
tag_name="message",
|
|
130
154
|
attributes={
|
|
131
|
-
"id"
|
|
132
|
-
"text"
|
|
133
|
-
"created_at"
|
|
134
|
-
|
|
135
|
-
|
|
155
|
+
"id": item_id,
|
|
156
|
+
"text": "",
|
|
157
|
+
"created_at": datetime.datetime.now(datetime.timezone.utc)
|
|
158
|
+
.isoformat()
|
|
159
|
+
.replace("+00:00", "Z"),
|
|
160
|
+
"author_name": context.room.local_participant.get_attribute("name"),
|
|
161
|
+
},
|
|
136
162
|
)
|
|
137
|
-
message_element.append_child(tag_name="file", attributes={
|
|
138
|
-
|
|
139
|
-
self.thread_context.chat.append_assistant_message(f"An image was saved at the path {image_name} and displayed to the user")
|
|
140
|
-
|
|
163
|
+
message_element.append_child(tag_name="file", attributes={"path": image_name})
|
|
141
164
|
|
|
165
|
+
self.thread_context.chat.append_assistant_message(
|
|
166
|
+
f"An image was saved at the path {image_name} and displayed to the user"
|
|
167
|
+
)
|
|
142
168
|
|
|
143
|
-
def get_thread_participants(*, room: RoomClient, thread: MeshDocument) -> list[RemoteParticipant]:
|
|
144
169
|
|
|
170
|
+
def get_thread_participants(
|
|
171
|
+
*, room: RoomClient, thread: MeshDocument
|
|
172
|
+
) -> list[RemoteParticipant]:
|
|
145
173
|
results = list[RemoteParticipant]()
|
|
146
174
|
|
|
147
175
|
for prop in thread.root.get_children():
|
|
148
|
-
|
|
149
176
|
if prop.tag_name == "members":
|
|
150
|
-
|
|
151
177
|
for member in prop.get_children():
|
|
152
|
-
|
|
153
178
|
for online in room.messaging.get_participants():
|
|
154
|
-
|
|
155
179
|
if online.get_attribute("name") == member.get_attribute("name"):
|
|
156
|
-
|
|
157
180
|
results.append(online)
|
|
158
181
|
|
|
159
182
|
return results
|
|
160
183
|
|
|
161
184
|
|
|
162
185
|
class ChatThreadContext:
|
|
163
|
-
def __init__(
|
|
186
|
+
def __init__(
|
|
187
|
+
self,
|
|
188
|
+
*,
|
|
189
|
+
chat: AgentChatContext,
|
|
190
|
+
thread: MeshDocument,
|
|
191
|
+
participants: Optional[list[RemoteParticipant]] = None,
|
|
192
|
+
):
|
|
164
193
|
self.thread = thread
|
|
165
|
-
if participants
|
|
194
|
+
if participants is None:
|
|
166
195
|
participants = []
|
|
167
196
|
|
|
168
197
|
self.participants = participants
|
|
169
198
|
self.chat = chat
|
|
170
199
|
|
|
200
|
+
|
|
171
201
|
# todo: thread should stop when participant stops?
|
|
172
202
|
class ChatBot(SingleRoomAgent):
|
|
173
|
-
def __init__(
|
|
174
|
-
|
|
203
|
+
def __init__(
|
|
204
|
+
self,
|
|
205
|
+
*,
|
|
206
|
+
name,
|
|
207
|
+
title=None,
|
|
208
|
+
description=None,
|
|
209
|
+
requires: Optional[list[Requirement]] = None,
|
|
210
|
+
llm_adapter: LLMAdapter,
|
|
211
|
+
tool_adapter: Optional[ToolResponseAdapter] = None,
|
|
212
|
+
toolkits: Optional[list[Toolkit]] = None,
|
|
213
|
+
rules: Optional[list[str]] = None,
|
|
214
|
+
auto_greet_message: Optional[str] = None,
|
|
215
|
+
empty_state_title: Optional[str] = None,
|
|
216
|
+
labels: Optional[str] = None,
|
|
217
|
+
):
|
|
175
218
|
super().__init__(
|
|
176
219
|
name=name,
|
|
177
220
|
title=title,
|
|
178
221
|
description=description,
|
|
179
222
|
requires=requires,
|
|
180
|
-
labels=labels
|
|
223
|
+
labels=labels,
|
|
181
224
|
)
|
|
182
225
|
|
|
183
|
-
if toolkits
|
|
226
|
+
if toolkits is None:
|
|
184
227
|
toolkits = []
|
|
185
228
|
|
|
186
229
|
self._llm_adapter = llm_adapter
|
|
@@ -188,180 +231,202 @@ class ChatBot(SingleRoomAgent):
|
|
|
188
231
|
|
|
189
232
|
self._message_channels = dict[str, Chan[RoomMessage]]()
|
|
190
233
|
|
|
191
|
-
self._room
|
|
234
|
+
self._room: RoomClient | None = None
|
|
192
235
|
self._toolkits = toolkits
|
|
193
236
|
|
|
194
|
-
if rules
|
|
237
|
+
if rules is None:
|
|
195
238
|
rules = []
|
|
196
239
|
|
|
197
240
|
self._rules = rules
|
|
198
|
-
self._is_typing = dict[str,asyncio.Task]()
|
|
241
|
+
self._is_typing = dict[str, asyncio.Task]()
|
|
199
242
|
self._auto_greet_message = auto_greet_message
|
|
200
|
-
|
|
201
|
-
if empty_state_title
|
|
243
|
+
|
|
244
|
+
if empty_state_title is None:
|
|
202
245
|
empty_state_title = "How can I help you?"
|
|
203
246
|
self._empty_state_title = empty_state_title
|
|
204
247
|
|
|
205
|
-
self._thread_tasks = dict[str,asyncio.Task]()
|
|
248
|
+
self._thread_tasks = dict[str, asyncio.Task]()
|
|
206
249
|
|
|
207
250
|
def init_requirements(self, requires: list[Requirement]):
|
|
208
|
-
if requires
|
|
209
|
-
|
|
210
|
-
requires = [
|
|
211
|
-
RequiredSchema(
|
|
212
|
-
name="thread"
|
|
213
|
-
)
|
|
214
|
-
]
|
|
251
|
+
if requires is None:
|
|
252
|
+
requires = [RequiredSchema(name="thread")]
|
|
215
253
|
|
|
216
254
|
else:
|
|
217
|
-
|
|
218
|
-
|
|
255
|
+
thread_schema = list(
|
|
256
|
+
n
|
|
257
|
+
for n in requires
|
|
258
|
+
if (isinstance(n, RequiredSchema) and n.name == "thread")
|
|
259
|
+
)
|
|
219
260
|
if len(thread_schema) == 0:
|
|
220
|
-
requires.append(
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
261
|
+
requires.append(RequiredSchema(name="thread"))
|
|
262
|
+
|
|
263
|
+
async def _send_and_save_chat(
|
|
264
|
+
self,
|
|
265
|
+
messages: Element,
|
|
266
|
+
path: str,
|
|
267
|
+
to: RemoteParticipant,
|
|
268
|
+
id: str,
|
|
269
|
+
text: str,
|
|
270
|
+
thread_attributes: dict,
|
|
271
|
+
):
|
|
228
272
|
with tracer.start_as_current_span("chatbot.thread.message") as span:
|
|
229
|
-
|
|
230
273
|
span.set_attributes(thread_attributes)
|
|
231
274
|
span.set_attribute("role", "assistant")
|
|
232
|
-
span.set_attribute(
|
|
233
|
-
|
|
234
|
-
"
|
|
235
|
-
|
|
236
|
-
})
|
|
237
|
-
|
|
238
|
-
await self.room.messaging.send_message(
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
"
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
275
|
+
span.set_attribute(
|
|
276
|
+
"from_participant_name",
|
|
277
|
+
self.room.local_participant.get_attribute("name"),
|
|
278
|
+
)
|
|
279
|
+
span.set_attributes({"id": id, "text": text})
|
|
280
|
+
|
|
281
|
+
await self.room.messaging.send_message(
|
|
282
|
+
to=to, type="chat", message={"path": path, "text": text}
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
messages.append_child(
|
|
286
|
+
tag_name="message",
|
|
287
|
+
attributes={
|
|
288
|
+
"id": id,
|
|
289
|
+
"text": text,
|
|
290
|
+
"created_at": datetime.datetime.now(datetime.timezone.utc)
|
|
291
|
+
.isoformat()
|
|
292
|
+
.replace("+00:00", "Z"),
|
|
293
|
+
"author_name": self.room.local_participant.get_attribute("name"),
|
|
294
|
+
},
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
async def greet(
|
|
298
|
+
self,
|
|
299
|
+
*,
|
|
300
|
+
messages: Element,
|
|
301
|
+
path: str,
|
|
302
|
+
chat_context: AgentChatContext,
|
|
303
|
+
participant: RemoteParticipant,
|
|
304
|
+
thread_attributes: dict,
|
|
305
|
+
):
|
|
306
|
+
if self._auto_greet_message is not None:
|
|
251
307
|
chat_context.append_user_message(self._auto_greet_message)
|
|
252
|
-
await self._send_and_save_chat(
|
|
253
|
-
|
|
308
|
+
await self._send_and_save_chat(
|
|
309
|
+
id=str(uuid.uuid4()),
|
|
310
|
+
to=RemoteParticipant(id=participant.id),
|
|
311
|
+
messages=messages,
|
|
312
|
+
path=path,
|
|
313
|
+
text=self._auto_greet_message,
|
|
314
|
+
thread_attributes=thread_attributes,
|
|
315
|
+
)
|
|
254
316
|
|
|
255
317
|
async def get_thread_participants(self, *, thread: MeshDocument):
|
|
256
318
|
return get_thread_participants(room=self._room, thread=thread)
|
|
257
|
-
|
|
258
|
-
async def get_thread_toolkits(self, *, thread_context: ChatThreadContext, participant: RemoteParticipant) -> list[Toolkit]:
|
|
259
319
|
|
|
260
|
-
|
|
320
|
+
async def get_thread_toolkits(
|
|
321
|
+
self, *, thread_context: ChatThreadContext, participant: RemoteParticipant
|
|
322
|
+
) -> list[Toolkit]:
|
|
323
|
+
toolkits = await self.get_required_toolkits(
|
|
324
|
+
context=ToolContext(
|
|
325
|
+
room=self.room,
|
|
326
|
+
caller=participant,
|
|
327
|
+
caller_context={"chat": thread_context.chat.to_json()},
|
|
328
|
+
)
|
|
329
|
+
)
|
|
261
330
|
toaster = None
|
|
262
|
-
|
|
263
|
-
for toolkit in toolkits:
|
|
264
331
|
|
|
332
|
+
for toolkit in toolkits:
|
|
265
333
|
if toolkit.name == "ui":
|
|
266
|
-
|
|
267
334
|
for tool in toolkit.tools:
|
|
268
|
-
|
|
269
335
|
if tool.name == "show_toast":
|
|
270
|
-
|
|
271
336
|
toaster = tool
|
|
272
337
|
|
|
273
|
-
if toaster
|
|
338
|
+
if toaster is not None:
|
|
274
339
|
|
|
275
340
|
def multi_tool(toolkit: Toolkit):
|
|
276
341
|
if toaster in toolkit.tools:
|
|
277
342
|
return toolkit
|
|
278
|
-
|
|
279
|
-
return MultiToolkit(required=[
|
|
343
|
+
|
|
344
|
+
return MultiToolkit(required=[toaster], base_toolkit=toolkit)
|
|
280
345
|
|
|
281
346
|
toolkits = list(map(multi_tool, toolkits))
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
*self._toolkits,
|
|
286
|
-
*toolkits
|
|
287
|
-
]
|
|
288
|
-
|
|
347
|
+
|
|
348
|
+
return [*self._toolkits, *toolkits]
|
|
349
|
+
|
|
289
350
|
async def init_chat_context(self) -> AgentChatContext:
|
|
290
|
-
context =
|
|
351
|
+
context = self._llm_adapter.create_chat_context()
|
|
291
352
|
context.append_rules(self._rules)
|
|
292
353
|
return context
|
|
293
354
|
|
|
294
355
|
async def open_thread(self, *, path: str):
|
|
295
356
|
return await self.room.sync.open(path=path)
|
|
296
|
-
|
|
357
|
+
|
|
297
358
|
async def close_thread(self, *, path: str):
|
|
298
359
|
return await self.room.sync.close(path=path)
|
|
299
360
|
|
|
300
|
-
|
|
301
361
|
async def _spawn_thread(self, path: str, messages: Chan[RoomMessage]):
|
|
302
|
-
|
|
303
|
-
|
|
362
|
+
self.room.developer.log_nowait(
|
|
363
|
+
type="chatbot.thread.started", data={"path": path}
|
|
364
|
+
)
|
|
304
365
|
chat_context = await self.init_chat_context()
|
|
305
366
|
opened = False
|
|
306
|
-
|
|
367
|
+
|
|
307
368
|
doc_messages = None
|
|
308
369
|
current_file = None
|
|
309
370
|
llm_messages = Chan[ResponseStreamEvent]()
|
|
310
371
|
thread_context = None
|
|
311
372
|
|
|
312
373
|
thread_attributes = None
|
|
313
|
-
|
|
314
374
|
|
|
315
375
|
def done_processing_llm_events(task: asyncio.Task):
|
|
316
376
|
try:
|
|
317
377
|
task.result()
|
|
318
|
-
except CancelledError
|
|
378
|
+
except CancelledError:
|
|
319
379
|
pass
|
|
320
380
|
except Exception as e:
|
|
321
381
|
logger.error("error sending delta", exc_info=e)
|
|
322
382
|
|
|
323
383
|
async def process_llm_events():
|
|
324
|
-
|
|
325
384
|
partial = ""
|
|
326
385
|
content_element = None
|
|
327
386
|
context_message = None
|
|
328
387
|
|
|
329
388
|
async for evt in llm_messages:
|
|
330
|
-
|
|
331
389
|
for participant in self._room.messaging.get_participants():
|
|
332
|
-
logger.debug(
|
|
390
|
+
logger.debug(
|
|
391
|
+
f"sending event {evt.type} to {participant.get_attribute('name')}"
|
|
392
|
+
)
|
|
333
393
|
|
|
334
394
|
# self.room.messaging.send_message_nowait(to=participant, type="llm.event", message=json.loads(evt.to_json()))
|
|
335
395
|
|
|
336
396
|
if evt.type == "response.content_part.added":
|
|
337
397
|
partial = ""
|
|
338
|
-
content_element = doc_messages.append_child(
|
|
339
|
-
"
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
398
|
+
content_element = doc_messages.append_child(
|
|
399
|
+
tag_name="message",
|
|
400
|
+
attributes={
|
|
401
|
+
"text": "",
|
|
402
|
+
"created_at": datetime.datetime.now(datetime.timezone.utc)
|
|
403
|
+
.isoformat()
|
|
404
|
+
.replace("+00:00", "Z"),
|
|
405
|
+
"author_name": self.room.local_participant.get_attribute(
|
|
406
|
+
"name"
|
|
407
|
+
),
|
|
408
|
+
},
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
context_message = {"role": "assistant", "content": ""}
|
|
348
412
|
chat_context.messages.append(context_message)
|
|
349
|
-
|
|
413
|
+
|
|
350
414
|
elif evt.type == "response.output_text.delta":
|
|
351
415
|
partial += evt.delta
|
|
352
416
|
content_element["text"] = partial
|
|
353
417
|
context_message["content"] = partial
|
|
354
|
-
|
|
418
|
+
|
|
355
419
|
elif evt.type == "response.output_text.done":
|
|
356
420
|
content_element = None
|
|
357
|
-
|
|
421
|
+
|
|
358
422
|
with tracer.start_as_current_span("chatbot.thread.message") as span:
|
|
359
|
-
span.set_attribute(
|
|
423
|
+
span.set_attribute(
|
|
424
|
+
"from_participant_name",
|
|
425
|
+
self.room.local_participant.get_attribute("name"),
|
|
426
|
+
)
|
|
360
427
|
span.set_attribute("role", "assistant")
|
|
361
428
|
span.set_attributes(thread_attributes)
|
|
362
|
-
span.set_attributes({
|
|
363
|
-
"text" : evt.text
|
|
364
|
-
})
|
|
429
|
+
span.set_attributes({"text": evt.text})
|
|
365
430
|
|
|
366
431
|
llm_task = asyncio.create_task(process_llm_events())
|
|
367
432
|
llm_task.add_done_callback(done_processing_llm_events)
|
|
@@ -369,13 +434,10 @@ class ChatBot(SingleRoomAgent):
|
|
|
369
434
|
thread = None
|
|
370
435
|
|
|
371
436
|
try:
|
|
372
|
-
|
|
373
437
|
received = None
|
|
374
438
|
|
|
375
439
|
while True:
|
|
376
|
-
|
|
377
440
|
while True:
|
|
378
|
-
|
|
379
441
|
logger.info(f"waiting for message on thread {path}")
|
|
380
442
|
received = await messages.recv()
|
|
381
443
|
logger.info(f"received message on thread {path}: {received.type}")
|
|
@@ -385,169 +447,228 @@ class ChatBot(SingleRoomAgent):
|
|
|
385
447
|
if participant.id == received.from_participant_id:
|
|
386
448
|
chat_with_participant = participant
|
|
387
449
|
break
|
|
388
|
-
|
|
389
|
-
if chat_with_participant
|
|
390
|
-
logger.warning(
|
|
450
|
+
|
|
451
|
+
if chat_with_participant is None:
|
|
452
|
+
logger.warning(
|
|
453
|
+
"participant does not have messaging enabled, skipping message"
|
|
454
|
+
)
|
|
391
455
|
continue
|
|
392
456
|
|
|
393
457
|
thread_attributes = {
|
|
394
|
-
"agent_name"
|
|
395
|
-
"agent_participant_id"
|
|
396
|
-
"agent_participant_name"
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
"
|
|
458
|
+
"agent_name": self.name,
|
|
459
|
+
"agent_participant_id": self.room.local_participant.id,
|
|
460
|
+
"agent_participant_name": self.room.local_participant.get_attribute(
|
|
461
|
+
"name"
|
|
462
|
+
),
|
|
463
|
+
"remote_participant_id": chat_with_participant.id,
|
|
464
|
+
"remote_participant_name": chat_with_participant.get_attribute(
|
|
465
|
+
"name"
|
|
466
|
+
),
|
|
467
|
+
"path": path,
|
|
400
468
|
}
|
|
401
|
-
|
|
402
|
-
if current_file != chat_with_participant.get_attribute("current_file"):
|
|
403
|
-
logger.info(f"participant is now looking at {chat_with_participant.get_attribute("current_file")}")
|
|
404
|
-
current_file = chat_with_participant.get_attribute("current_file")
|
|
405
|
-
|
|
406
|
-
if current_file != None:
|
|
407
|
-
chat_context.append_assistant_message(message=f"the user is currently viewing the file at the path: {current_file}")
|
|
408
|
-
|
|
409
|
-
elif current_file != None:
|
|
410
|
-
chat_context.append_assistant_message(message=f"the user is not current viewing any files")
|
|
411
|
-
|
|
412
|
-
if thread == None:
|
|
413
|
-
|
|
414
|
-
with tracer.start_as_current_span("chatbot.thread.open") as span:
|
|
415
469
|
|
|
470
|
+
if current_file != chat_with_participant.get_attribute(
|
|
471
|
+
"current_file"
|
|
472
|
+
):
|
|
473
|
+
logger.info(
|
|
474
|
+
f"participant is now looking at {chat_with_participant.get_attribute('current_file')}"
|
|
475
|
+
)
|
|
476
|
+
current_file = chat_with_participant.get_attribute(
|
|
477
|
+
"current_file"
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
if current_file is not None:
|
|
481
|
+
chat_context.append_assistant_message(
|
|
482
|
+
message=f"the user is currently viewing the file at the path: {current_file}"
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
elif current_file is not None:
|
|
486
|
+
chat_context.append_assistant_message(
|
|
487
|
+
message="the user is not current viewing any files"
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
if thread is None:
|
|
491
|
+
with tracer.start_as_current_span(
|
|
492
|
+
"chatbot.thread.open"
|
|
493
|
+
) as span:
|
|
416
494
|
span.set_attributes(thread_attributes)
|
|
417
495
|
|
|
418
496
|
thread = await self.open_thread(path=path)
|
|
419
|
-
|
|
497
|
+
|
|
420
498
|
for prop in thread.root.get_children():
|
|
421
|
-
|
|
422
499
|
if prop.tag_name == "messages":
|
|
423
|
-
|
|
424
500
|
doc_messages = prop
|
|
425
|
-
|
|
426
|
-
for element in doc_messages.get_children():
|
|
427
501
|
|
|
502
|
+
for element in doc_messages.get_children():
|
|
428
503
|
if isinstance(element, Element):
|
|
429
|
-
|
|
430
504
|
msg = element["text"]
|
|
431
|
-
if
|
|
432
|
-
|
|
505
|
+
if (
|
|
506
|
+
element["author_name"]
|
|
507
|
+
== self.room.local_participant.get_attribute(
|
|
508
|
+
"name"
|
|
509
|
+
)
|
|
510
|
+
):
|
|
511
|
+
chat_context.append_assistant_message(
|
|
512
|
+
msg
|
|
513
|
+
)
|
|
433
514
|
else:
|
|
434
515
|
chat_context.append_user_message(msg)
|
|
435
516
|
|
|
436
517
|
for child in element.get_children():
|
|
437
518
|
if child.tag_name == "file":
|
|
438
|
-
chat_context.append_assistant_message(
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
raise Exception("thread was not properly initialized")
|
|
519
|
+
chat_context.append_assistant_message(
|
|
520
|
+
f"the user attached a file with the path '{child.get_attribute('path')}'"
|
|
521
|
+
)
|
|
442
522
|
|
|
523
|
+
if doc_messages is None:
|
|
524
|
+
raise Exception("thread was not properly initialized")
|
|
443
525
|
|
|
444
526
|
if received.type == "opened":
|
|
445
|
-
|
|
446
|
-
if opened == False:
|
|
447
|
-
|
|
527
|
+
if not opened:
|
|
448
528
|
opened = True
|
|
449
|
-
|
|
450
|
-
await self.greet(path=path, chat_context=chat_context, participant=chat_with_participant, messages=doc_messages, thread_attributes=thread_attributes)
|
|
451
529
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
530
|
+
await self.greet(
|
|
531
|
+
path=path,
|
|
532
|
+
chat_context=chat_context,
|
|
533
|
+
participant=chat_with_participant,
|
|
534
|
+
messages=doc_messages,
|
|
535
|
+
thread_attributes=thread_attributes,
|
|
536
|
+
)
|
|
456
537
|
|
|
457
|
-
|
|
538
|
+
if received.type == "chat":
|
|
539
|
+
if thread is None:
|
|
540
|
+
self.room.developer.log_nowait(
|
|
541
|
+
type="thread is not open", data={}
|
|
542
|
+
)
|
|
458
543
|
break
|
|
459
544
|
|
|
460
|
-
|
|
461
|
-
|
|
545
|
+
for participant in get_thread_participants(
|
|
546
|
+
room=self._room, thread=thread
|
|
547
|
+
):
|
|
462
548
|
# TODO: async gather
|
|
463
|
-
self._room.messaging.send_message_nowait(
|
|
549
|
+
self._room.messaging.send_message_nowait(
|
|
550
|
+
to=participant,
|
|
551
|
+
type="thinking",
|
|
552
|
+
message={"thinking": True, "path": path},
|
|
553
|
+
)
|
|
464
554
|
|
|
465
555
|
if chat_with_participant.id == received.from_participant_id:
|
|
466
|
-
self.room.developer.log_nowait(
|
|
556
|
+
self.room.developer.log_nowait(
|
|
557
|
+
type="llm.message",
|
|
558
|
+
data={
|
|
559
|
+
"context": chat_context.id,
|
|
560
|
+
"participant_id": self.room.local_participant.id,
|
|
561
|
+
"participant_name": self.room.local_participant.get_attribute(
|
|
562
|
+
"name"
|
|
563
|
+
),
|
|
564
|
+
"message": {
|
|
565
|
+
"content": {
|
|
566
|
+
"role": "user",
|
|
567
|
+
"text": received.message["text"],
|
|
568
|
+
}
|
|
569
|
+
},
|
|
570
|
+
},
|
|
571
|
+
)
|
|
467
572
|
|
|
468
573
|
attachments = received.message.get("attachments", [])
|
|
469
574
|
text = received.message["text"]
|
|
470
575
|
|
|
471
576
|
for attachment in attachments:
|
|
472
|
-
|
|
473
|
-
|
|
577
|
+
chat_context.append_assistant_message(
|
|
578
|
+
message=f"the user attached a file at the path '{attachment['path']}'"
|
|
579
|
+
)
|
|
474
580
|
|
|
475
581
|
chat_context.append_user_message(message=text)
|
|
476
|
-
|
|
477
582
|
|
|
478
583
|
# if user is typing, wait for typing to stop
|
|
479
584
|
while True:
|
|
480
|
-
|
|
481
585
|
if chat_with_participant.id not in self._is_typing:
|
|
482
586
|
break
|
|
483
|
-
|
|
484
|
-
await asyncio.sleep(.5)
|
|
485
587
|
|
|
486
|
-
|
|
588
|
+
await asyncio.sleep(0.5)
|
|
589
|
+
|
|
590
|
+
if messages.empty():
|
|
487
591
|
break
|
|
488
|
-
|
|
489
|
-
if received != None:
|
|
490
592
|
|
|
593
|
+
if received is not None:
|
|
491
594
|
with tracer.start_as_current_span("chatbot.thread.message") as span:
|
|
492
|
-
|
|
493
595
|
span.set_attributes(thread_attributes)
|
|
494
596
|
span.set_attribute("role", "user")
|
|
495
|
-
span.set_attribute(
|
|
597
|
+
span.set_attribute(
|
|
598
|
+
"from_participant_name",
|
|
599
|
+
chat_with_participant.get_attribute("name"),
|
|
600
|
+
)
|
|
496
601
|
|
|
497
602
|
attachments = received.message.get("attachments", [])
|
|
498
603
|
span.set_attribute("attachments", attachments)
|
|
499
604
|
|
|
500
605
|
text = received.message["text"]
|
|
501
|
-
span.set_attributes({
|
|
502
|
-
"text" : text
|
|
503
|
-
})
|
|
606
|
+
span.set_attributes({"text": text})
|
|
504
607
|
|
|
505
608
|
try:
|
|
506
|
-
|
|
507
|
-
if thread_context == None:
|
|
508
|
-
|
|
609
|
+
if thread_context is None:
|
|
509
610
|
thread_context = ChatThreadContext(
|
|
510
611
|
chat=chat_context,
|
|
511
612
|
thread=thread,
|
|
512
|
-
participants=get_thread_participants(
|
|
613
|
+
participants=get_thread_participants(
|
|
614
|
+
room=self.room, thread=thread
|
|
615
|
+
),
|
|
513
616
|
)
|
|
514
617
|
|
|
515
618
|
def handle_event(evt):
|
|
516
619
|
llm_messages.send_nowait(evt)
|
|
517
620
|
|
|
518
|
-
with tracer.start_as_current_span("chatbot.llm") as span:
|
|
519
|
-
|
|
621
|
+
with tracer.start_as_current_span("chatbot.llm") as span:
|
|
520
622
|
try:
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
thread_toolkits =
|
|
525
|
-
|
|
526
|
-
|
|
623
|
+
with tracer.start_as_current_span(
|
|
624
|
+
"get_thread_toolkits"
|
|
625
|
+
) as span:
|
|
626
|
+
thread_toolkits = (
|
|
627
|
+
await self.get_thread_toolkits(
|
|
628
|
+
thread_context=thread_context,
|
|
629
|
+
participant=participant,
|
|
630
|
+
)
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
await self._llm_adapter.next(
|
|
527
634
|
context=chat_context,
|
|
528
635
|
room=self._room,
|
|
529
636
|
toolkits=thread_toolkits,
|
|
530
637
|
tool_adapter=self._tool_adapter,
|
|
531
|
-
event_handler=handle_event
|
|
638
|
+
event_handler=handle_event,
|
|
532
639
|
)
|
|
533
640
|
except Exception as e:
|
|
534
641
|
logger.error("An error was encountered", exc_info=e)
|
|
535
|
-
await self._send_and_save_chat(
|
|
642
|
+
await self._send_and_save_chat(
|
|
643
|
+
messages=doc_messages,
|
|
644
|
+
to=chat_with_participant,
|
|
645
|
+
path=path,
|
|
646
|
+
id=str(uuid.uuid4()),
|
|
647
|
+
text="There was an error while communicating with the LLM. Please try again later.",
|
|
648
|
+
thread_attributes=thread_attributes,
|
|
649
|
+
)
|
|
536
650
|
|
|
537
651
|
finally:
|
|
538
|
-
for participant in get_thread_participants(
|
|
652
|
+
for participant in get_thread_participants(
|
|
653
|
+
room=self._room, thread=thread
|
|
654
|
+
):
|
|
539
655
|
# TODO: async gather
|
|
540
|
-
self._room.messaging.send_message_nowait(
|
|
656
|
+
self._room.messaging.send_message_nowait(
|
|
657
|
+
to=participant,
|
|
658
|
+
type="thinking",
|
|
659
|
+
message={"thinking": False, "path": path},
|
|
660
|
+
)
|
|
541
661
|
|
|
542
662
|
finally:
|
|
543
|
-
|
|
544
663
|
llm_messages.close()
|
|
545
664
|
|
|
546
|
-
if self.room
|
|
665
|
+
if self.room is not None:
|
|
547
666
|
logger.info("thread was ended {path}")
|
|
548
|
-
self.room.developer.log_nowait(
|
|
549
|
-
|
|
550
|
-
|
|
667
|
+
self.room.developer.log_nowait(
|
|
668
|
+
type="chatbot.thread.ended", data={"path": path}
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
if thread is not None:
|
|
551
672
|
await self.close_thread(path=path)
|
|
552
673
|
|
|
553
674
|
def _get_message_channel(self, key: str) -> Chan[RoomMessage]:
|
|
@@ -556,64 +677,67 @@ class ChatBot(SingleRoomAgent):
|
|
|
556
677
|
self._message_channels[key] = chan
|
|
557
678
|
|
|
558
679
|
chan = self._message_channels[key]
|
|
559
|
-
|
|
680
|
+
|
|
560
681
|
return chan
|
|
561
|
-
|
|
682
|
+
|
|
562
683
|
async def stop(self):
|
|
563
684
|
await super().stop()
|
|
564
685
|
|
|
565
686
|
for thread in self._thread_tasks.values():
|
|
566
687
|
thread.cancel()
|
|
567
|
-
|
|
688
|
+
|
|
568
689
|
self._thread_tasks.clear()
|
|
569
690
|
|
|
570
691
|
async def start(self, *, room):
|
|
571
|
-
|
|
572
692
|
await super().start(room=room)
|
|
573
693
|
|
|
574
694
|
logger.debug("Starting chatbot")
|
|
575
|
-
|
|
576
|
-
await self.room.local_participant.set_attribute("empty_state_title", self._empty_state_title)
|
|
577
695
|
|
|
578
|
-
|
|
696
|
+
await self.room.local_participant.set_attribute(
|
|
697
|
+
"empty_state_title", self._empty_state_title
|
|
698
|
+
)
|
|
579
699
|
|
|
580
|
-
|
|
700
|
+
def on_message(message: RoomMessage):
|
|
581
701
|
if message.type == "chat" or message.type == "opened":
|
|
582
|
-
|
|
583
702
|
path = message.message["path"]
|
|
584
|
-
|
|
703
|
+
|
|
585
704
|
messages = self._get_message_channel(path)
|
|
586
705
|
|
|
587
|
-
logger.info(
|
|
588
|
-
|
|
706
|
+
logger.info(
|
|
707
|
+
f"queued incoming message for thread {path}: {message.type}"
|
|
708
|
+
)
|
|
709
|
+
|
|
589
710
|
messages.send_nowait(message)
|
|
590
711
|
|
|
591
712
|
if path not in self._thread_tasks or self._thread_tasks[path].done():
|
|
592
|
-
|
|
593
|
-
def thread_done(task: asyncio.Task):
|
|
594
713
|
|
|
714
|
+
def thread_done(task: asyncio.Task):
|
|
595
715
|
self._message_channels.pop(path)
|
|
596
716
|
try:
|
|
597
717
|
task.result()
|
|
598
|
-
except CancelledError
|
|
718
|
+
except CancelledError:
|
|
599
719
|
pass
|
|
600
720
|
except Exception as e:
|
|
601
|
-
logger.error(
|
|
602
|
-
|
|
603
|
-
|
|
721
|
+
logger.error(
|
|
722
|
+
f"The chat thread ended with an error {e}", exc_info=e
|
|
723
|
+
)
|
|
724
|
+
|
|
604
725
|
logger.info(f"spawning chat thread for {path}")
|
|
605
|
-
task = asyncio.create_task(
|
|
726
|
+
task = asyncio.create_task(
|
|
727
|
+
self._spawn_thread(messages=messages, path=path)
|
|
728
|
+
)
|
|
606
729
|
task.add_done_callback(thread_done)
|
|
607
730
|
|
|
608
731
|
self._thread_tasks[path] = task
|
|
609
732
|
|
|
610
733
|
elif message.type == "typing":
|
|
734
|
+
|
|
611
735
|
def callback(task: asyncio.Task):
|
|
612
736
|
try:
|
|
613
737
|
task.result()
|
|
614
|
-
except:
|
|
738
|
+
except Exception:
|
|
615
739
|
pass
|
|
616
|
-
|
|
740
|
+
|
|
617
741
|
async def remove_timeout(id: str):
|
|
618
742
|
await asyncio.sleep(1)
|
|
619
743
|
self._is_typing.pop(id)
|
|
@@ -621,23 +745,22 @@ class ChatBot(SingleRoomAgent):
|
|
|
621
745
|
if message.from_participant_id in self._is_typing:
|
|
622
746
|
self._is_typing[message.from_participant_id].cancel()
|
|
623
747
|
|
|
624
|
-
timeout = asyncio.create_task(
|
|
748
|
+
timeout = asyncio.create_task(
|
|
749
|
+
remove_timeout(id=message.from_participant_id)
|
|
750
|
+
)
|
|
625
751
|
timeout.add_done_callback(callback)
|
|
626
752
|
|
|
627
753
|
self._is_typing[message.from_participant_id] = timeout
|
|
628
754
|
|
|
629
755
|
room.messaging.on("message", on_message)
|
|
630
|
-
|
|
631
|
-
if self._auto_greet_message
|
|
632
|
-
|
|
633
|
-
|
|
756
|
+
|
|
757
|
+
if self._auto_greet_message is not None:
|
|
758
|
+
|
|
759
|
+
def on_participant_added(participant: RemoteParticipant):
|
|
634
760
|
# will spawn the initial thread
|
|
635
761
|
self._get_message_channel(participant_id=participant.id)
|
|
636
|
-
|
|
637
762
|
|
|
638
763
|
room.messaging.on("participant_added", on_participant_added)
|
|
639
764
|
|
|
640
|
-
|
|
641
765
|
logger.debug("Enabling chatbot messaging")
|
|
642
766
|
await room.messaging.enable()
|
|
643
|
-
|