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/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 ImageGenerationTool, LocalShellTool
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 ChatBotThreadLocalShellTool(LocalShellTool):
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
- background: Literal["transparent", "opaque", "auto"] = None,
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 get_thread_participants(
218
- *, room: RoomClient, thread: MeshDocument
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
- results.append(online)
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 get_thread_participants(self, *, thread: MeshDocument):
377
- return get_thread_participants(room=self._room, thread=thread)
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=participant,
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
- def multi_tool(toolkit: Toolkit):
400
- if toaster in toolkit.tools:
401
- return toolkit
402
-
403
- return MultiToolkit(required=[toaster], base_toolkit=toolkit)
404
-
405
- toolkits = list(map(multi_tool, toolkits))
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
- return await self.room.sync.open(path=path)
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(msg)
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, *, context: ChatThreadContext):
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
- await update_thread_task
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
- while True:
574
- logger.debug(f"waiting for message on thread {path}")
575
- received = await messages.recv()
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
- if current_file is not None:
614
- chat_context.append_assistant_message(
615
- message=f"the user is currently viewing the file at the path: {current_file}"
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
- elif current_file is not None:
619
- chat_context.append_assistant_message(
620
- message="the user is not current viewing any files"
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
- if thread is None:
624
- with tracer.start_as_current_span(
625
- "chatbot.thread.open"
626
- ) as span:
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
- await self.load_thread_context(
641
- thread_context=thread_context
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
- if received.type == "opened":
645
- if not opened:
646
- opened = True
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
- await self._greet(
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
- if received.type == "chat":
657
- if thread is None:
658
- logger.info("thread is not open", extra={"path": path})
659
- break
660
-
661
- logger.debug(
662
- "chatbot received a chat",
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
- attachments = received.message.get("attachments", [])
674
- text = received.message["text"]
914
+ await self.load_thread_context(thread_context=thread_context)
675
915
 
676
- for attachment in attachments:
677
- chat_context.append_assistant_message(
678
- message=f"the user attached a file at the path '{attachment['path']}'"
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
- chat_context.append_user_message(message=text)
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
- if messages.empty():
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=get_thread_participants(
976
+ participants=get_online_participants(
718
977
  room=self.room, thread=thread
719
978
  ),
720
979
  )
721
980
  else:
722
- thread_context.participants = get_thread_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=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
- context=thread_context
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
- await self._llm_adapter.next(
756
- context=chat_context,
757
- room=self._room,
758
- toolkits=thread_toolkits,
759
- tool_adapter=self._tool_adapter,
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 get_thread_participants(
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