meshagent-agents 0.5.15__py3-none-any.whl → 0.6.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.
Potentially problematic release.
This version of meshagent-agents might be problematic. Click here for more details.
- meshagent/agents/__init__.py +0 -2
- meshagent/agents/adapter.py +22 -5
- meshagent/agents/agent.py +28 -29
- meshagent/agents/chat.py +519 -175
- meshagent/agents/context.py +16 -1
- meshagent/agents/development.py +3 -1
- meshagent/agents/indexer.py +2 -2
- meshagent/agents/llmrunner.py +169 -0
- meshagent/agents/mail.py +15 -17
- meshagent/agents/planning.py +3 -3
- meshagent/agents/pydantic.py +1 -1
- meshagent/agents/schemas/transcript.py +77 -0
- meshagent/agents/thread_schema.py +58 -7
- meshagent/agents/utils.py +0 -2
- meshagent/agents/version.py +1 -1
- meshagent/agents/worker.py +8 -4
- meshagent/agents/writer.py +1 -1
- {meshagent_agents-0.5.15.dist-info → meshagent_agents-0.6.0.dist-info}/METADATA +8 -8
- meshagent_agents-0.6.0.dist-info/RECORD +31 -0
- meshagent/agents/hosting.py +0 -182
- meshagent_agents-0.5.15.dist-info/RECORD +0 -30
- {meshagent_agents-0.5.15.dist-info → meshagent_agents-0.6.0.dist-info}/WHEEL +0 -0
- {meshagent_agents-0.5.15.dist-info → meshagent_agents-0.6.0.dist-info}/licenses/LICENSE +0 -0
- {meshagent_agents-0.5.15.dist-info → meshagent_agents-0.6.0.dist-info}/top_level.txt +0 -0
meshagent/agents/chat.py
CHANGED
|
@@ -4,21 +4,28 @@ from meshagent.api import (
|
|
|
4
4
|
RoomMessage,
|
|
5
5
|
RoomClient,
|
|
6
6
|
RemoteParticipant,
|
|
7
|
+
Participant,
|
|
7
8
|
RequiredSchema,
|
|
8
9
|
Requirement,
|
|
9
10
|
Element,
|
|
10
11
|
MeshDocument,
|
|
11
12
|
)
|
|
12
|
-
from meshagent.tools import Toolkit, ToolContext
|
|
13
|
+
from meshagent.tools import Toolkit, ToolContext, make_tools, ToolkitBuilder
|
|
13
14
|
from meshagent.agents.adapter import LLMAdapter, ToolResponseAdapter
|
|
14
|
-
from meshagent.openai.tools.responses_adapter import
|
|
15
|
+
from meshagent.openai.tools.responses_adapter import (
|
|
16
|
+
ImageGenerationConfig,
|
|
17
|
+
ImageGenerationTool,
|
|
18
|
+
LocalShellConfig,
|
|
19
|
+
LocalShellTool,
|
|
20
|
+
# WebSearchConfig,
|
|
21
|
+
# WebSearchTool,
|
|
22
|
+
ReasoningTool,
|
|
23
|
+
)
|
|
15
24
|
import asyncio
|
|
16
25
|
from typing import Optional
|
|
17
26
|
import logging
|
|
18
|
-
from meshagent.tools import MultiToolkit
|
|
19
27
|
import uuid
|
|
20
28
|
import datetime
|
|
21
|
-
from typing import Literal
|
|
22
29
|
import base64
|
|
23
30
|
from openai.types.responses import ResponseStreamEvent
|
|
24
31
|
from asyncio import CancelledError
|
|
@@ -32,10 +39,108 @@ tracer = trace.get_tracer("meshagent.chatbot")
|
|
|
32
39
|
logger = logging.getLogger("chat")
|
|
33
40
|
|
|
34
41
|
|
|
35
|
-
class
|
|
36
|
-
def __init__(self, *, thread_context: "ChatThreadContext"):
|
|
42
|
+
class ChatBotReasoningTool(ReasoningTool):
|
|
43
|
+
def __init__(self, *, room: RoomClient, thread_context: "ChatThreadContext"):
|
|
37
44
|
super().__init__()
|
|
38
45
|
self.thread_context = thread_context
|
|
46
|
+
self.room = room
|
|
47
|
+
|
|
48
|
+
self._reasoning_element = None
|
|
49
|
+
self._reasoning_item = None
|
|
50
|
+
|
|
51
|
+
def _get_messages_element(self):
|
|
52
|
+
messages = self.thread_context.thread.root.get_children_by_tag_name("messages")
|
|
53
|
+
if len(messages) > 0:
|
|
54
|
+
return messages[0]
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
async def on_reasoning_summary_part_added(
|
|
58
|
+
self,
|
|
59
|
+
context: ToolContext,
|
|
60
|
+
*,
|
|
61
|
+
item_id: str,
|
|
62
|
+
output_index: int,
|
|
63
|
+
part: dict,
|
|
64
|
+
sequence_number: int,
|
|
65
|
+
summary_index: int,
|
|
66
|
+
type: str,
|
|
67
|
+
**extra,
|
|
68
|
+
):
|
|
69
|
+
el = self._get_messages_element()
|
|
70
|
+
if el is None:
|
|
71
|
+
logger.warning("missing messages element, cannot log reasoning")
|
|
72
|
+
else:
|
|
73
|
+
self._reasoning_element = el.append_child("reasoning", {"summary": ""})
|
|
74
|
+
|
|
75
|
+
async def on_reasoning_summary_part_done(
|
|
76
|
+
self,
|
|
77
|
+
context: ToolContext,
|
|
78
|
+
*,
|
|
79
|
+
item_id: str,
|
|
80
|
+
output_index: int,
|
|
81
|
+
part: dict,
|
|
82
|
+
sequence_number: int,
|
|
83
|
+
summary_index: int,
|
|
84
|
+
type: str,
|
|
85
|
+
**extra,
|
|
86
|
+
):
|
|
87
|
+
self._reasoning_element = None
|
|
88
|
+
|
|
89
|
+
async def on_reasoning_summary_text_delta(
|
|
90
|
+
self,
|
|
91
|
+
context: ToolContext,
|
|
92
|
+
*,
|
|
93
|
+
delta: str,
|
|
94
|
+
output_index: int,
|
|
95
|
+
sequence_number: int,
|
|
96
|
+
summary_index: int,
|
|
97
|
+
type: str,
|
|
98
|
+
**extra,
|
|
99
|
+
):
|
|
100
|
+
el = self._reasoning_element
|
|
101
|
+
el.set_attribute("summary", el.get_attribute("summary") + delta)
|
|
102
|
+
|
|
103
|
+
async def on_reasoning_summary_text_done(
|
|
104
|
+
self,
|
|
105
|
+
context: ToolContext,
|
|
106
|
+
*,
|
|
107
|
+
item_id: str,
|
|
108
|
+
output_index: int,
|
|
109
|
+
sequence_number: int,
|
|
110
|
+
summary_index: int,
|
|
111
|
+
type: str,
|
|
112
|
+
**extra,
|
|
113
|
+
):
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class ChatBotThreadLocalShellToolkitBuilder(ToolkitBuilder):
|
|
118
|
+
def __init__(self, *, thread_context: "ChatThreadContext"):
|
|
119
|
+
super().__init__(name="local_shell", type=ImageGenerationConfig)
|
|
120
|
+
self.thread_context = thread_context
|
|
121
|
+
|
|
122
|
+
def make(
|
|
123
|
+
self,
|
|
124
|
+
*,
|
|
125
|
+
model: str,
|
|
126
|
+
config: LocalShellConfig,
|
|
127
|
+
):
|
|
128
|
+
return Toolkit(
|
|
129
|
+
name="local_shell",
|
|
130
|
+
tools=[
|
|
131
|
+
ChatBotThreadLocalShellTool(
|
|
132
|
+
config=config, thread_context=self.thread_context
|
|
133
|
+
)
|
|
134
|
+
],
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class ChatBotThreadLocalShellTool(LocalShellTool):
|
|
139
|
+
def __init__(
|
|
140
|
+
self, *, thread_context: "ChatThreadContext", config: LocalShellConfig
|
|
141
|
+
):
|
|
142
|
+
super().__init__(config=config)
|
|
143
|
+
self.thread_context = thread_context
|
|
39
144
|
|
|
40
145
|
async def execute_shell_command(
|
|
41
146
|
self,
|
|
@@ -75,32 +180,35 @@ class ChatBotThreadLocalShellTool(LocalShellTool):
|
|
|
75
180
|
return result
|
|
76
181
|
|
|
77
182
|
|
|
183
|
+
class ChatBotThreadOpenAIImageGenerationToolkitBuilder(ToolkitBuilder):
|
|
184
|
+
def __init__(self, *, thread_context: "ChatThreadContext"):
|
|
185
|
+
super().__init__(name="image_generation", type=ImageGenerationConfig)
|
|
186
|
+
self.thread_context = thread_context
|
|
187
|
+
|
|
188
|
+
def make(
|
|
189
|
+
self,
|
|
190
|
+
*,
|
|
191
|
+
model: str,
|
|
192
|
+
config: ImageGenerationConfig,
|
|
193
|
+
):
|
|
194
|
+
return Toolkit(
|
|
195
|
+
name="image_generation",
|
|
196
|
+
tools=[
|
|
197
|
+
ChatBotThreadOpenAIImageGenerationTool(
|
|
198
|
+
config=config, thread_context=self.thread_context
|
|
199
|
+
)
|
|
200
|
+
],
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
|
|
78
204
|
class ChatBotThreadOpenAIImageGenerationTool(ImageGenerationTool):
|
|
79
205
|
def __init__(
|
|
80
206
|
self,
|
|
81
207
|
*,
|
|
82
|
-
|
|
83
|
-
input_image_mask_url: Optional[str] = None,
|
|
84
|
-
model: Optional[str] = None,
|
|
85
|
-
moderation: Optional[str] = None,
|
|
86
|
-
output_compression: Optional[int] = None,
|
|
87
|
-
output_format: Optional[Literal["png", "webp", "jpeg"]] = None,
|
|
88
|
-
partial_images: Optional[int] = None,
|
|
89
|
-
quality: Optional[Literal["auto", "low", "medium", "high"]] = None,
|
|
90
|
-
size: Optional[Literal["1024x1024", "1024x1536", "1536x1024", "auto"]] = None,
|
|
208
|
+
config: ImageGenerationConfig,
|
|
91
209
|
thread_context: "ChatThreadContext",
|
|
92
210
|
):
|
|
93
|
-
super().__init__(
|
|
94
|
-
background=background,
|
|
95
|
-
input_image_mask_url=input_image_mask_url,
|
|
96
|
-
model=model,
|
|
97
|
-
moderation=moderation,
|
|
98
|
-
output_compression=output_compression,
|
|
99
|
-
output_format=output_format,
|
|
100
|
-
partial_images=partial_images,
|
|
101
|
-
quality=quality,
|
|
102
|
-
size=size,
|
|
103
|
-
)
|
|
211
|
+
super().__init__(config=config)
|
|
104
212
|
|
|
105
213
|
self.thread_context = thread_context
|
|
106
214
|
|
|
@@ -214,8 +322,11 @@ class ChatBotThreadOpenAIImageGenerationTool(ImageGenerationTool):
|
|
|
214
322
|
)
|
|
215
323
|
|
|
216
324
|
|
|
217
|
-
def
|
|
218
|
-
*,
|
|
325
|
+
def get_online_participants(
|
|
326
|
+
*,
|
|
327
|
+
room: RoomClient,
|
|
328
|
+
thread: MeshDocument,
|
|
329
|
+
exclude: Optional[list[Participant]] = None,
|
|
219
330
|
) -> list[RemoteParticipant]:
|
|
220
331
|
results = list[RemoteParticipant]()
|
|
221
332
|
|
|
@@ -224,7 +335,8 @@ def get_thread_participants(
|
|
|
224
335
|
for member in prop.get_children():
|
|
225
336
|
for online in room.messaging.get_participants():
|
|
226
337
|
if online.get_attribute("name") == member.get_attribute("name"):
|
|
227
|
-
|
|
338
|
+
if exclude is None or online not in exclude:
|
|
339
|
+
results.append(online)
|
|
228
340
|
|
|
229
341
|
return results
|
|
230
342
|
|
|
@@ -247,7 +359,6 @@ class ChatThreadContext:
|
|
|
247
359
|
self.path = path
|
|
248
360
|
|
|
249
361
|
|
|
250
|
-
# todo: thread should stop when participant stops?
|
|
251
362
|
class ChatBot(SingleRoomAgent):
|
|
252
363
|
def __init__(
|
|
253
364
|
self,
|
|
@@ -262,7 +373,8 @@ class ChatBot(SingleRoomAgent):
|
|
|
262
373
|
rules: Optional[list[str]] = None,
|
|
263
374
|
auto_greet_message: Optional[str] = None,
|
|
264
375
|
empty_state_title: Optional[str] = None,
|
|
265
|
-
labels: Optional[str] = None,
|
|
376
|
+
labels: Optional[list[str]] = None,
|
|
377
|
+
decision_model: Optional[str] = None,
|
|
266
378
|
):
|
|
267
379
|
super().__init__(
|
|
268
380
|
name=name,
|
|
@@ -275,6 +387,10 @@ class ChatBot(SingleRoomAgent):
|
|
|
275
387
|
if toolkits is None:
|
|
276
388
|
toolkits = []
|
|
277
389
|
|
|
390
|
+
self._decision_model = (
|
|
391
|
+
"gpt-4.1-mini" if decision_model is None else decision_model
|
|
392
|
+
)
|
|
393
|
+
|
|
278
394
|
self._llm_adapter = llm_adapter
|
|
279
395
|
self._tool_adapter = tool_adapter
|
|
280
396
|
|
|
@@ -295,6 +411,7 @@ class ChatBot(SingleRoomAgent):
|
|
|
295
411
|
self._empty_state_title = empty_state_title
|
|
296
412
|
|
|
297
413
|
self._thread_tasks = dict[str, asyncio.Task]()
|
|
414
|
+
self._open_threads = {}
|
|
298
415
|
|
|
299
416
|
def init_requirements(self, requires: list[Requirement]):
|
|
300
417
|
if requires is None:
|
|
@@ -373,8 +490,15 @@ class ChatBot(SingleRoomAgent):
|
|
|
373
490
|
thread_attributes=thread_attributes,
|
|
374
491
|
)
|
|
375
492
|
|
|
376
|
-
async def
|
|
377
|
-
|
|
493
|
+
async def get_online_participants(
|
|
494
|
+
self, *, thread: MeshDocument, exclude: Optional[list[Participant]] = None
|
|
495
|
+
):
|
|
496
|
+
return get_online_participants(room=self._room, thread=thread, exclude=exclude)
|
|
497
|
+
|
|
498
|
+
async def get_thread_toolkit_builders(
|
|
499
|
+
self, *, thread_context: ChatThreadContext, participant: RemoteParticipant
|
|
500
|
+
) -> list[ToolkitBuilder]:
|
|
501
|
+
return []
|
|
378
502
|
|
|
379
503
|
async def get_thread_toolkits(
|
|
380
504
|
self, *, thread_context: ChatThreadContext, participant: RemoteParticipant
|
|
@@ -382,27 +506,23 @@ class ChatBot(SingleRoomAgent):
|
|
|
382
506
|
toolkits = await self.get_required_toolkits(
|
|
383
507
|
context=ToolContext(
|
|
384
508
|
room=self.room,
|
|
385
|
-
caller=
|
|
509
|
+
caller=self.room.local_participant,
|
|
510
|
+
on_behalf_of=participant,
|
|
386
511
|
caller_context={"chat": thread_context.chat.to_json()},
|
|
387
512
|
)
|
|
388
513
|
)
|
|
389
|
-
toaster = None
|
|
390
|
-
|
|
391
|
-
for toolkit in toolkits:
|
|
392
|
-
if toolkit.name == "ui":
|
|
393
|
-
for tool in toolkit.tools:
|
|
394
|
-
if tool.name == "show_toast":
|
|
395
|
-
toaster = tool
|
|
396
|
-
|
|
397
|
-
if toaster is not None:
|
|
398
514
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
515
|
+
toolkits.append(
|
|
516
|
+
Toolkit(
|
|
517
|
+
name="reasoning",
|
|
518
|
+
tools=[
|
|
519
|
+
ChatBotReasoningTool(
|
|
520
|
+
room=self._room,
|
|
521
|
+
thread_context=thread_context,
|
|
522
|
+
)
|
|
523
|
+
],
|
|
524
|
+
)
|
|
525
|
+
)
|
|
406
526
|
|
|
407
527
|
return [*self._toolkits, *toolkits]
|
|
408
528
|
|
|
@@ -412,9 +532,19 @@ class ChatBot(SingleRoomAgent):
|
|
|
412
532
|
return context
|
|
413
533
|
|
|
414
534
|
async def open_thread(self, *, path: str):
|
|
415
|
-
|
|
535
|
+
logger.info(f"opening thread {path}")
|
|
536
|
+
if path not in self._open_threads:
|
|
537
|
+
fut = asyncio.ensure_future(self.room.sync.open(path=path))
|
|
538
|
+
self._open_threads[path] = fut
|
|
539
|
+
|
|
540
|
+
return await self._open_threads[path]
|
|
416
541
|
|
|
417
542
|
async def close_thread(self, *, path: str):
|
|
543
|
+
logger.info(f"closing thread {path}")
|
|
544
|
+
|
|
545
|
+
if path in self._open_threads:
|
|
546
|
+
del self._open_threads[path]
|
|
547
|
+
|
|
418
548
|
return await self.room.sync.close(path=path)
|
|
419
549
|
|
|
420
550
|
async def load_thread_context(self, *, thread_context: ChatThreadContext):
|
|
@@ -435,7 +565,9 @@ class ChatBot(SingleRoomAgent):
|
|
|
435
565
|
] == self.room.local_participant.get_attribute("name"):
|
|
436
566
|
chat_context.append_assistant_message(msg)
|
|
437
567
|
else:
|
|
438
|
-
chat_context.append_user_message(
|
|
568
|
+
chat_context.append_user_message(
|
|
569
|
+
element["author_name"] + " said: " + msg
|
|
570
|
+
)
|
|
439
571
|
|
|
440
572
|
for child in element.get_children():
|
|
441
573
|
if child.tag_name == "file":
|
|
@@ -448,7 +580,7 @@ class ChatBot(SingleRoomAgent):
|
|
|
448
580
|
if doc_messages is None:
|
|
449
581
|
raise Exception("thread was not properly initialized")
|
|
450
582
|
|
|
451
|
-
async def prepare_llm_context(self, *,
|
|
583
|
+
async def prepare_llm_context(self, *, thread_context: ChatThreadContext):
|
|
452
584
|
"""
|
|
453
585
|
called prior to sending the request to the LLM in case the agent needs to modify the context prior to sending
|
|
454
586
|
"""
|
|
@@ -552,7 +684,156 @@ class ChatBot(SingleRoomAgent):
|
|
|
552
684
|
pass
|
|
553
685
|
finally:
|
|
554
686
|
updates.shutdown()
|
|
555
|
-
|
|
687
|
+
|
|
688
|
+
await update_thread_task
|
|
689
|
+
|
|
690
|
+
def get_thread_members(self, *, thread: MeshDocument) -> list[str]:
|
|
691
|
+
results = []
|
|
692
|
+
|
|
693
|
+
for prop in thread.root.get_children():
|
|
694
|
+
if prop.tag_name == "members":
|
|
695
|
+
for member in prop.get_children():
|
|
696
|
+
results.append(member.get_attribute("name"))
|
|
697
|
+
|
|
698
|
+
return results
|
|
699
|
+
|
|
700
|
+
async def should_reply(
|
|
701
|
+
self,
|
|
702
|
+
*,
|
|
703
|
+
context: ChatThreadContext,
|
|
704
|
+
has_more_than_one_other_user: bool,
|
|
705
|
+
toolkits: list[Toolkit],
|
|
706
|
+
from_user: RemoteParticipant,
|
|
707
|
+
online: list[Participant],
|
|
708
|
+
):
|
|
709
|
+
if not has_more_than_one_other_user:
|
|
710
|
+
return True
|
|
711
|
+
|
|
712
|
+
online_set = {}
|
|
713
|
+
|
|
714
|
+
all_members = []
|
|
715
|
+
online_members = []
|
|
716
|
+
|
|
717
|
+
for m in self.get_thread_members(thread=context.thread):
|
|
718
|
+
all_members.append(m)
|
|
719
|
+
|
|
720
|
+
for o in online:
|
|
721
|
+
if o.get_attribute("name") not in online_set:
|
|
722
|
+
online_set[o.get_attribute("name")] = True
|
|
723
|
+
online_members.append(o.get_attribute("name"))
|
|
724
|
+
|
|
725
|
+
logger.info(
|
|
726
|
+
"multiple participants detected, checking whether agent should reply to conversation"
|
|
727
|
+
)
|
|
728
|
+
|
|
729
|
+
cloned_context = context.chat.copy()
|
|
730
|
+
cloned_context.replace_rules(
|
|
731
|
+
rules=[
|
|
732
|
+
"examine the conversation so far and return whether the user is expecting a reply from you or another user as the next message in the conversation",
|
|
733
|
+
f'your name (the assistant) is "{self.room.local_participant.get_attribute("name")}"',
|
|
734
|
+
"if the user mentions a person with another name, they aren't talking to you unless they also mention you",
|
|
735
|
+
"if the user poses a question to everyone, they are talking to you",
|
|
736
|
+
f"members of thread are currently {all_members}",
|
|
737
|
+
f"users online currently are {online_members}",
|
|
738
|
+
]
|
|
739
|
+
)
|
|
740
|
+
response = await self._llm_adapter.next(
|
|
741
|
+
context=cloned_context,
|
|
742
|
+
room=self._room,
|
|
743
|
+
model=self._decision_model or self._llm_adapter.default_model(),
|
|
744
|
+
on_behalf_of=from_user,
|
|
745
|
+
toolkits=[],
|
|
746
|
+
output_schema={
|
|
747
|
+
"type": "object",
|
|
748
|
+
"required": ["reasoning", "expecting_assistant_reply", "next_user"],
|
|
749
|
+
"additionalProperties": False,
|
|
750
|
+
"properties": {
|
|
751
|
+
"reasoning": {
|
|
752
|
+
"type": "string",
|
|
753
|
+
"description": "explain why you think the user was or was not expecting you to reply",
|
|
754
|
+
},
|
|
755
|
+
"next_user": {
|
|
756
|
+
"type": "string",
|
|
757
|
+
"description": "who would be expectd to send the next message in the conversation",
|
|
758
|
+
},
|
|
759
|
+
"expecting_assistant_reply": {"type": "boolean"},
|
|
760
|
+
},
|
|
761
|
+
},
|
|
762
|
+
)
|
|
763
|
+
|
|
764
|
+
logger.info(f"should reply check returned {response}")
|
|
765
|
+
|
|
766
|
+
return response["expecting_assistant_reply"]
|
|
767
|
+
|
|
768
|
+
async def handle_user_message(
|
|
769
|
+
self,
|
|
770
|
+
*,
|
|
771
|
+
context: ChatThreadContext,
|
|
772
|
+
toolkits: list[Toolkit],
|
|
773
|
+
model: str,
|
|
774
|
+
from_user: RemoteParticipant,
|
|
775
|
+
event_handler,
|
|
776
|
+
):
|
|
777
|
+
online = await self.get_online_participants(
|
|
778
|
+
thread=context.thread, exclude=[self.room.local_participant]
|
|
779
|
+
)
|
|
780
|
+
|
|
781
|
+
for participant in get_online_participants(
|
|
782
|
+
room=self._room, thread=context.thread
|
|
783
|
+
):
|
|
784
|
+
self._room.messaging.send_message_nowait(
|
|
785
|
+
to=participant,
|
|
786
|
+
type="listening",
|
|
787
|
+
message={"listening": True, "path": context.path},
|
|
788
|
+
)
|
|
789
|
+
|
|
790
|
+
has_more_than_one_other_user = False
|
|
791
|
+
|
|
792
|
+
for member_name in self.get_thread_members(thread=context.thread):
|
|
793
|
+
if member_name != self._room.local_participant.get_attribute(
|
|
794
|
+
"name"
|
|
795
|
+
) and member_name != from_user.get_attribute("name"):
|
|
796
|
+
has_more_than_one_other_user = True
|
|
797
|
+
break
|
|
798
|
+
|
|
799
|
+
reply = await self.should_reply(
|
|
800
|
+
has_more_than_one_other_user=has_more_than_one_other_user,
|
|
801
|
+
online=online,
|
|
802
|
+
context=context,
|
|
803
|
+
toolkits=toolkits,
|
|
804
|
+
from_user=from_user,
|
|
805
|
+
)
|
|
806
|
+
|
|
807
|
+
for participant in get_online_participants(
|
|
808
|
+
room=self._room, thread=context.thread
|
|
809
|
+
):
|
|
810
|
+
self._room.messaging.send_message_nowait(
|
|
811
|
+
to=participant,
|
|
812
|
+
type="listening",
|
|
813
|
+
message={"listening": False, "path": context.path},
|
|
814
|
+
)
|
|
815
|
+
|
|
816
|
+
if not reply:
|
|
817
|
+
return
|
|
818
|
+
|
|
819
|
+
for participant in get_online_participants(
|
|
820
|
+
room=self._room, thread=context.thread
|
|
821
|
+
):
|
|
822
|
+
self._room.messaging.send_message_nowait(
|
|
823
|
+
to=participant,
|
|
824
|
+
type="thinking",
|
|
825
|
+
message={"thinking": True, "path": context.path},
|
|
826
|
+
)
|
|
827
|
+
|
|
828
|
+
await self._llm_adapter.next(
|
|
829
|
+
context=context.chat,
|
|
830
|
+
room=self._room,
|
|
831
|
+
toolkits=toolkits,
|
|
832
|
+
tool_adapter=self._tool_adapter,
|
|
833
|
+
event_handler=event_handler,
|
|
834
|
+
model=model,
|
|
835
|
+
on_behalf_of=from_user,
|
|
836
|
+
)
|
|
556
837
|
|
|
557
838
|
async def _spawn_thread(self, path: str, messages: Chan[RoomMessage]):
|
|
558
839
|
logger.debug("chatbot is starting a thread", extra={"path": path})
|
|
@@ -570,120 +851,108 @@ class ChatBot(SingleRoomAgent):
|
|
|
570
851
|
received = None
|
|
571
852
|
|
|
572
853
|
while True:
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
logger.debug(f"received message on thread {path}: {received.type}")
|
|
577
|
-
|
|
578
|
-
chat_with_participant = None
|
|
579
|
-
for participant in self._room.messaging.get_participants():
|
|
580
|
-
if participant.id == received.from_participant_id:
|
|
581
|
-
chat_with_participant = participant
|
|
582
|
-
break
|
|
583
|
-
|
|
584
|
-
if chat_with_participant is None:
|
|
585
|
-
logger.warning(
|
|
586
|
-
"participant does not have messaging enabled, skipping message"
|
|
587
|
-
)
|
|
588
|
-
continue
|
|
589
|
-
|
|
590
|
-
thread_attributes = {
|
|
591
|
-
"agent_name": self.name,
|
|
592
|
-
"agent_participant_id": self.room.local_participant.id,
|
|
593
|
-
"agent_participant_name": self.room.local_participant.get_attribute(
|
|
594
|
-
"name"
|
|
595
|
-
),
|
|
596
|
-
"remote_participant_id": chat_with_participant.id,
|
|
597
|
-
"remote_participant_name": chat_with_participant.get_attribute(
|
|
598
|
-
"name"
|
|
599
|
-
),
|
|
600
|
-
"path": path,
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
if current_file != chat_with_participant.get_attribute(
|
|
604
|
-
"current_file"
|
|
605
|
-
):
|
|
606
|
-
logger.info(
|
|
607
|
-
f"participant is now looking at {chat_with_participant.get_attribute('current_file')}"
|
|
608
|
-
)
|
|
609
|
-
current_file = chat_with_participant.get_attribute(
|
|
610
|
-
"current_file"
|
|
611
|
-
)
|
|
854
|
+
logger.debug(f"waiting for message on thread {path}")
|
|
855
|
+
received = await messages.recv()
|
|
856
|
+
logger.debug(f"received message on thread {path}: {received.type}")
|
|
612
857
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
858
|
+
chat_with_participant = None
|
|
859
|
+
for participant in self._room.messaging.get_participants():
|
|
860
|
+
if participant.id == received.from_participant_id:
|
|
861
|
+
chat_with_participant = participant
|
|
862
|
+
break
|
|
617
863
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
864
|
+
if chat_with_participant is None:
|
|
865
|
+
logger.warning(
|
|
866
|
+
"participant does not have messaging enabled, skipping message"
|
|
867
|
+
)
|
|
868
|
+
continue
|
|
869
|
+
|
|
870
|
+
thread_attributes = {
|
|
871
|
+
"agent_name": self.name,
|
|
872
|
+
"agent_participant_id": self.room.local_participant.id,
|
|
873
|
+
"agent_participant_name": self.room.local_participant.get_attribute(
|
|
874
|
+
"name"
|
|
875
|
+
),
|
|
876
|
+
"remote_participant_id": chat_with_participant.id,
|
|
877
|
+
"remote_participant_name": chat_with_participant.get_attribute(
|
|
878
|
+
"name"
|
|
879
|
+
),
|
|
880
|
+
"path": path,
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
if current_file != chat_with_participant.get_attribute("current_file"):
|
|
884
|
+
logger.info(
|
|
885
|
+
f"participant is now looking at {chat_with_participant.get_attribute('current_file')}"
|
|
886
|
+
)
|
|
887
|
+
current_file = chat_with_participant.get_attribute("current_file")
|
|
622
888
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
span.set_attributes(thread_attributes)
|
|
628
|
-
|
|
629
|
-
thread = await self.open_thread(path=path)
|
|
630
|
-
|
|
631
|
-
thread_context = ChatThreadContext(
|
|
632
|
-
path=path,
|
|
633
|
-
chat=chat_context,
|
|
634
|
-
thread=thread,
|
|
635
|
-
participants=get_thread_participants(
|
|
636
|
-
room=self.room, thread=thread
|
|
637
|
-
),
|
|
638
|
-
)
|
|
889
|
+
if current_file is not None:
|
|
890
|
+
chat_context.append_assistant_message(
|
|
891
|
+
message=f"the user is currently viewing the file at the path: {current_file}"
|
|
892
|
+
)
|
|
639
893
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
894
|
+
elif current_file is not None:
|
|
895
|
+
chat_context.append_assistant_message(
|
|
896
|
+
message="the user is not current viewing any files"
|
|
897
|
+
)
|
|
643
898
|
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
899
|
+
if thread is None:
|
|
900
|
+
with tracer.start_as_current_span("chatbot.thread.open") as span:
|
|
901
|
+
span.set_attributes(thread_attributes)
|
|
647
902
|
|
|
648
|
-
|
|
649
|
-
path=path,
|
|
650
|
-
chat_context=chat_context,
|
|
651
|
-
participant=chat_with_participant,
|
|
652
|
-
thread=thread,
|
|
653
|
-
thread_attributes=thread_attributes,
|
|
654
|
-
)
|
|
903
|
+
thread = await self.open_thread(path=path)
|
|
655
904
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
extra={
|
|
664
|
-
"context": chat_context.id,
|
|
665
|
-
"participant_id": self.room.local_participant.id,
|
|
666
|
-
"participant_name": self.room.local_participant.get_attribute(
|
|
667
|
-
"name"
|
|
668
|
-
),
|
|
669
|
-
"text": received.message["text"],
|
|
670
|
-
},
|
|
905
|
+
thread_context = ChatThreadContext(
|
|
906
|
+
path=path,
|
|
907
|
+
chat=chat_context,
|
|
908
|
+
thread=thread,
|
|
909
|
+
participants=get_online_participants(
|
|
910
|
+
room=self.room, thread=thread
|
|
911
|
+
),
|
|
671
912
|
)
|
|
672
913
|
|
|
673
|
-
|
|
674
|
-
text = received.message["text"]
|
|
914
|
+
await self.load_thread_context(thread_context=thread_context)
|
|
675
915
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
916
|
+
if received.type == "opened":
|
|
917
|
+
if not opened:
|
|
918
|
+
opened = True
|
|
919
|
+
|
|
920
|
+
await self._greet(
|
|
921
|
+
path=path,
|
|
922
|
+
chat_context=chat_context,
|
|
923
|
+
participant=chat_with_participant,
|
|
924
|
+
thread=thread,
|
|
925
|
+
thread_attributes=thread_attributes,
|
|
926
|
+
)
|
|
927
|
+
|
|
928
|
+
if received.type == "chat":
|
|
929
|
+
if thread is None:
|
|
930
|
+
logger.info("thread is not open", extra={"path": path})
|
|
931
|
+
break
|
|
932
|
+
|
|
933
|
+
logger.debug(
|
|
934
|
+
"chatbot received a chat",
|
|
935
|
+
extra={
|
|
936
|
+
"context": chat_context.id,
|
|
937
|
+
"participant_id": self.room.local_participant.id,
|
|
938
|
+
"participant_name": self.room.local_participant.get_attribute(
|
|
939
|
+
"name"
|
|
940
|
+
),
|
|
941
|
+
"text": received.message["text"],
|
|
942
|
+
},
|
|
943
|
+
)
|
|
680
944
|
|
|
681
|
-
|
|
945
|
+
attachments = received.message.get("attachments", [])
|
|
946
|
+
text = received.message["text"]
|
|
947
|
+
|
|
948
|
+
for attachment in attachments:
|
|
949
|
+
chat_context.append_assistant_message(
|
|
950
|
+
message=f"the user attached a file at the path '{attachment['path']}'"
|
|
951
|
+
)
|
|
682
952
|
|
|
683
|
-
|
|
684
|
-
break
|
|
953
|
+
chat_context.append_user_message(message=text)
|
|
685
954
|
|
|
686
|
-
if received is not None:
|
|
955
|
+
if received is not None and received.type == "chat":
|
|
687
956
|
with tracer.start_as_current_span("chatbot.thread.message") as span:
|
|
688
957
|
span.set_attributes(thread_attributes)
|
|
689
958
|
span.set_attribute("role", "user")
|
|
@@ -699,27 +968,17 @@ class ChatBot(SingleRoomAgent):
|
|
|
699
968
|
span.set_attributes({"text": text})
|
|
700
969
|
|
|
701
970
|
try:
|
|
702
|
-
for participant in get_thread_participants(
|
|
703
|
-
room=self._room, thread=thread
|
|
704
|
-
):
|
|
705
|
-
# TODO: async gather
|
|
706
|
-
self._room.messaging.send_message_nowait(
|
|
707
|
-
to=participant,
|
|
708
|
-
type="thinking",
|
|
709
|
-
message={"thinking": True, "path": path},
|
|
710
|
-
)
|
|
711
|
-
|
|
712
971
|
if thread_context is None:
|
|
713
972
|
thread_context = ChatThreadContext(
|
|
714
973
|
path=path,
|
|
715
974
|
chat=chat_context,
|
|
716
975
|
thread=thread,
|
|
717
|
-
participants=
|
|
976
|
+
participants=get_online_participants(
|
|
718
977
|
room=self.room, thread=thread
|
|
719
978
|
),
|
|
720
979
|
)
|
|
721
980
|
else:
|
|
722
|
-
thread_context.participants =
|
|
981
|
+
thread_context.participants = get_online_participants(
|
|
723
982
|
room=self.room, thread=thread
|
|
724
983
|
)
|
|
725
984
|
|
|
@@ -731,12 +990,22 @@ class ChatBot(SingleRoomAgent):
|
|
|
731
990
|
thread_toolkits = (
|
|
732
991
|
await self.get_thread_toolkits(
|
|
733
992
|
thread_context=thread_context,
|
|
734
|
-
participant=
|
|
993
|
+
participant=chat_with_participant,
|
|
994
|
+
)
|
|
995
|
+
)
|
|
996
|
+
|
|
997
|
+
with tracer.start_as_current_span(
|
|
998
|
+
"get_thread_toolkit_builders"
|
|
999
|
+
) as span:
|
|
1000
|
+
thread_tool_providers = (
|
|
1001
|
+
await self.get_thread_toolkit_builders(
|
|
1002
|
+
thread_context=thread_context,
|
|
1003
|
+
participant=chat_with_participant,
|
|
735
1004
|
)
|
|
736
1005
|
)
|
|
737
1006
|
|
|
738
1007
|
await self.prepare_llm_context(
|
|
739
|
-
|
|
1008
|
+
thread_context=thread_context
|
|
740
1009
|
)
|
|
741
1010
|
|
|
742
1011
|
llm_messages = asyncio.Queue[ResponseStreamEvent]()
|
|
@@ -752,12 +1021,32 @@ class ChatBot(SingleRoomAgent):
|
|
|
752
1021
|
)
|
|
753
1022
|
)
|
|
754
1023
|
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
1024
|
+
message_toolkits = [*thread_toolkits]
|
|
1025
|
+
|
|
1026
|
+
model = received.message.get(
|
|
1027
|
+
"model", self._llm_adapter.default_model()
|
|
1028
|
+
)
|
|
1029
|
+
|
|
1030
|
+
message_tools = received.message.get("tools")
|
|
1031
|
+
|
|
1032
|
+
if (
|
|
1033
|
+
message_tools is not None
|
|
1034
|
+
and len(message_tools) > 0
|
|
1035
|
+
):
|
|
1036
|
+
message_toolkits.extend(
|
|
1037
|
+
make_tools(
|
|
1038
|
+
model=model,
|
|
1039
|
+
providers=thread_tool_providers,
|
|
1040
|
+
tools=message_tools,
|
|
1041
|
+
)
|
|
1042
|
+
)
|
|
1043
|
+
|
|
1044
|
+
await self.handle_user_message(
|
|
1045
|
+
context=thread_context,
|
|
1046
|
+
toolkits=message_toolkits,
|
|
760
1047
|
event_handler=handle_event,
|
|
1048
|
+
model=model,
|
|
1049
|
+
from_user=chat_with_participant,
|
|
761
1050
|
)
|
|
762
1051
|
|
|
763
1052
|
llm_messages.shutdown()
|
|
@@ -777,7 +1066,7 @@ class ChatBot(SingleRoomAgent):
|
|
|
777
1066
|
finally:
|
|
778
1067
|
|
|
779
1068
|
async def cleanup():
|
|
780
|
-
for participant in
|
|
1069
|
+
for participant in get_online_participants(
|
|
781
1070
|
room=self._room, thread=thread
|
|
782
1071
|
):
|
|
783
1072
|
self._room.messaging.send_message_nowait(
|
|
@@ -817,6 +1106,48 @@ class ChatBot(SingleRoomAgent):
|
|
|
817
1106
|
|
|
818
1107
|
self._thread_tasks.clear()
|
|
819
1108
|
|
|
1109
|
+
async def _on_get_thread_toolkits_message(self, *, message: RoomMessage):
|
|
1110
|
+
path = message.message["path"]
|
|
1111
|
+
|
|
1112
|
+
thread_context = None
|
|
1113
|
+
if path in self._open_threads:
|
|
1114
|
+
thread = await self._open_threads[path]
|
|
1115
|
+
|
|
1116
|
+
thread_context = ChatThreadContext(
|
|
1117
|
+
path=path,
|
|
1118
|
+
chat=AgentChatContext(),
|
|
1119
|
+
thread=thread,
|
|
1120
|
+
participants=get_online_participants(room=self.room, thread=thread),
|
|
1121
|
+
)
|
|
1122
|
+
|
|
1123
|
+
if thread_context is None:
|
|
1124
|
+
logger.warning("thread toolkits requested for a thread that is not open")
|
|
1125
|
+
return
|
|
1126
|
+
|
|
1127
|
+
chat_with_participant = None
|
|
1128
|
+
for participant in self._room.messaging.get_participants():
|
|
1129
|
+
if participant.id == message.from_participant_id:
|
|
1130
|
+
chat_with_participant = participant
|
|
1131
|
+
break
|
|
1132
|
+
|
|
1133
|
+
if chat_with_participant is None:
|
|
1134
|
+
logger.warning(
|
|
1135
|
+
"participant does not have messaging enabled, skipping message"
|
|
1136
|
+
)
|
|
1137
|
+
return
|
|
1138
|
+
|
|
1139
|
+
tool_providers = await self.get_thread_toolkit_builders(
|
|
1140
|
+
thread_context=thread_context, participant=chat_with_participant
|
|
1141
|
+
)
|
|
1142
|
+
self._room.messaging.send_message_nowait(
|
|
1143
|
+
to=chat_with_participant,
|
|
1144
|
+
type="set_thread_tool_providers",
|
|
1145
|
+
message={
|
|
1146
|
+
"path": path,
|
|
1147
|
+
"tool_providers": [{"name": t.name} for t in tool_providers],
|
|
1148
|
+
},
|
|
1149
|
+
)
|
|
1150
|
+
|
|
820
1151
|
async def start(self, *, room):
|
|
821
1152
|
await super().start(room=room)
|
|
822
1153
|
|
|
@@ -827,6 +1158,19 @@ class ChatBot(SingleRoomAgent):
|
|
|
827
1158
|
)
|
|
828
1159
|
|
|
829
1160
|
def on_message(message: RoomMessage):
|
|
1161
|
+
if message.type == "get_thread_toolkit_builders":
|
|
1162
|
+
task = asyncio.create_task(
|
|
1163
|
+
self._on_get_thread_toolkits_message(message=message)
|
|
1164
|
+
)
|
|
1165
|
+
|
|
1166
|
+
def on_done(task: asyncio.Task):
|
|
1167
|
+
try:
|
|
1168
|
+
task.result()
|
|
1169
|
+
except Exception as ex:
|
|
1170
|
+
logger.error(f"unable to get tool providers {ex}", exc_info=ex)
|
|
1171
|
+
|
|
1172
|
+
task.add_done_callback(on_done)
|
|
1173
|
+
|
|
830
1174
|
if message.type == "chat" or message.type == "opened":
|
|
831
1175
|
path = message.message["path"]
|
|
832
1176
|
|