meshagent-agents 0.0.1__py3-none-any.whl → 0.0.4__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.

@@ -3,3 +3,4 @@ from .development import connect_development_agent
3
3
  from .listener import Listener, ListenerContext
4
4
  from .hosting import RemoteTaskRunnerServer
5
5
  from .adapter import ToolResponseAdapter, LLMAdapter
6
+ from .thread_schema import thread_schema
@@ -3,7 +3,7 @@ from .agent import AgentChatContext
3
3
  from jsonschema import validate
4
4
  from meshagent.tools.toolkit import Response, Toolkit
5
5
  from meshagent.api import RoomClient
6
- from typing import Any, Optional
6
+ from typing import Any, Optional, Callable
7
7
 
8
8
  class ToolResponseAdapter(ABC):
9
9
  def __init__(self):
@@ -14,15 +14,19 @@ class ToolResponseAdapter(ABC):
14
14
  pass
15
15
 
16
16
  @abstractmethod
17
- async def append_messages(self, *, context: AgentChatContext, tool_call: Any, room: RoomClient, response: Response):
17
+ async def create_messages(self, *, context: AgentChatContext, tool_call: Any, room: RoomClient, response: Response) -> list:
18
18
  pass
19
19
 
20
20
 
21
- class LLMAdapter(ABC):
21
+ class LLMAdapter[T](ABC):
22
22
 
23
23
  def create_chat_context(self) -> AgentChatContext:
24
24
  return AgentChatContext()
25
25
 
26
+ @abstractmethod
27
+ async def check_for_termination(self, *, context: AgentChatContext, room: RoomClient):
28
+ return True
29
+
26
30
  @abstractmethod
27
31
  async def next(self,
28
32
  *,
@@ -31,6 +35,7 @@ class LLMAdapter(ABC):
31
35
  toolkits: Toolkit,
32
36
  tool_adapter: Optional[ToolResponseAdapter] = None,
33
37
  output_schema: Optional[dict] = None,
38
+ event_handler: Optional[Callable[[T],None]] = None
34
39
  ) -> Any:
35
40
  pass
36
41
 
meshagent/agents/agent.py CHANGED
@@ -8,11 +8,9 @@ from meshagent.api import WebSocketClientProtocol, ToolDescription, ToolkitDescr
8
8
  from meshagent.api.protocol import Protocol
9
9
  from meshagent.tools.toolkit import Toolkit, Tool, ToolContext
10
10
  from meshagent.api.room_server_client import RoomClient, RoomException
11
- from meshagent.api.schema_document import Document
12
11
  from jsonschema import validate
13
- from typing import Callable, Awaitable
14
12
  from .context import AgentCallContext, AgentChatContext
15
- from .schema import no_arguments_schema
13
+ from meshagent.api.schema_util import no_arguments_schema
16
14
  import logging
17
15
  import asyncio
18
16
  from typing import Optional
@@ -45,6 +43,7 @@ class RoomTool(Tool):
45
43
  class Agent:
46
44
 
47
45
  def __init__(self, *, name: str, title: Optional[str] = None, description: Optional[str] = None, requires: Optional[list[Requirement]] = None, labels: Optional[list[str]] = None):
46
+
48
47
  self._name = name
49
48
  if title == None:
50
49
  title = name
@@ -55,6 +54,8 @@ class Agent:
55
54
  self._description = description
56
55
  if requires == None:
57
56
  requires = []
57
+
58
+ self.init_requirements(requires)
58
59
  self._requires = requires
59
60
 
60
61
  if labels == None:
@@ -82,6 +83,8 @@ class Agent:
82
83
  def labels(self):
83
84
  return self._labels
84
85
 
86
+ def init_requirements(self, requires): ...
87
+
85
88
  async def init_chat_context(self) -> AgentChatContext:
86
89
  return AgentChatContext()
87
90
 
@@ -100,6 +103,7 @@ class SingleRoomAgent(Agent):
100
103
  super().__init__(name=name, title=title, description=description, requires=requires, labels=labels)
101
104
  self._room = None
102
105
 
106
+
103
107
  async def start(self, *, room: RoomClient) -> None:
104
108
 
105
109
  if self._room != None:
@@ -107,6 +111,9 @@ class SingleRoomAgent(Agent):
107
111
 
108
112
  self._room = room
109
113
 
114
+ await self.install_requirements()
115
+
116
+
110
117
  async def stop(self) -> None:
111
118
  self._room = None
112
119
  pass
@@ -121,7 +128,6 @@ class SingleRoomAgent(Agent):
121
128
 
122
129
  schemas = await self._room.storage.list(path=".schemas")
123
130
 
124
-
125
131
  for schema in schemas:
126
132
  schemas_by_name[schema.name] = schema
127
133
 
@@ -137,6 +143,11 @@ class SingleRoomAgent(Agent):
137
143
  for requirement in self.requires:
138
144
 
139
145
  if isinstance(requirement, RequiredToolkit):
146
+
147
+ if requirement.name == "meshagent.ui":
148
+ # TODO: maybe requirements can be marked as non installable?
149
+ continue
150
+
140
151
  if requirement.name not in toolkits_by_name:
141
152
 
142
153
  installed = True
@@ -369,9 +380,6 @@ class TaskRunner(SingleRoomAgent):
369
380
  )
370
381
 
371
382
 
372
- await self.install_requirements(participant_id=caller.id)
373
-
374
-
375
383
  context = AgentCallContext(chat=chat_context, room=self.room, caller=caller)
376
384
 
377
385
  for toolkit_json in toolkits_json:
meshagent/agents/chat.py CHANGED
@@ -1,6 +1,6 @@
1
- from .agent import SingleRoomAgent, AgentChatContext
1
+ from .agent import SingleRoomAgent, AgentChatContext, AgentCallContext
2
2
  from meshagent.api.chan import Chan
3
- from meshagent.api import RoomMessage, RoomException, RoomClient, RemoteParticipant
3
+ from meshagent.api import RoomMessage, RoomException, RoomClient, RemoteParticipant, RequiredSchema, Requirement, Element, MeshDocument
4
4
  from meshagent.tools import Toolkit
5
5
  from .adapter import LLMAdapter, ToolResponseAdapter
6
6
  import asyncio
@@ -8,6 +8,10 @@ from typing import Optional
8
8
  import logging
9
9
  from meshagent.tools import MultiToolkit
10
10
  import urllib
11
+ import uuid
12
+ import datetime
13
+
14
+ from openai.types.responses import ResponseStreamEvent
11
15
 
12
16
  logging.basicConfig()
13
17
  logger = logging.getLogger("chat")
@@ -16,8 +20,38 @@ logger.setLevel(logging.INFO)
16
20
 
17
21
  # todo: thread should stop when participant stops?
18
22
 
23
+ def get_thread_participants(*, room: RoomClient, thread: MeshDocument) -> list[RemoteParticipant]:
24
+
25
+ results = list[RemoteParticipant]()
26
+
27
+ for prop in thread.root.get_children():
28
+
29
+ if prop.tag_name == "members":
30
+
31
+ for member in prop.get_children():
32
+
33
+ for online in room.messaging.get_participants():
34
+
35
+ if online.get_attribute("name") == member.get_attribute("name"):
36
+
37
+ results.append(online)
38
+
39
+ return results
40
+
41
+
42
+ class ChatThreadContext:
43
+ def __init__(self, *, chat: AgentChatContext, thread: MeshDocument, toolkits: list[Toolkit], participants: Optional[list[RemoteParticipant]] = None):
44
+ self.thread = thread
45
+ self.toolkits = toolkits
46
+ if participants == None:
47
+ participants = []
48
+
49
+ self.participants = participants
50
+ self.chat = chat
51
+
19
52
  class ChatBot(SingleRoomAgent):
20
- def __init__(self, *, name, title = None, description = None, requires = None, llm_adapter: LLMAdapter, tool_adapter: Optional[ToolResponseAdapter] = None, toolkits: Optional[list[Toolkit]] = None, rules : Optional[list[str]] = None, auto_greet_prompt : Optional[str] = None, auto_greet_message : Optional[str] = None, empty_state_title : Optional[str] = None, labels: Optional[str] = None):
53
+ def __init__(self, *, name, title = None, description = None, requires : Optional[list[Requirement]] = None, llm_adapter: LLMAdapter, tool_adapter: Optional[ToolResponseAdapter] = None, toolkits: Optional[list[Toolkit]] = None, rules : Optional[list[str]] = None, auto_greet_message : Optional[str] = None, empty_state_title : Optional[str] = None, labels: Optional[str] = None):
54
+
21
55
  super().__init__(
22
56
  name=name,
23
57
  title=title,
@@ -42,30 +76,60 @@ class ChatBot(SingleRoomAgent):
42
76
 
43
77
  self._rules = rules
44
78
  self._is_typing = dict[str,asyncio.Task]()
45
- self._auto_greet_prompt = auto_greet_prompt
46
79
  self._auto_greet_message = auto_greet_message
47
80
 
48
81
  if empty_state_title == None:
49
82
  empty_state_title = "How can I help you?"
50
83
  self._empty_state_title = empty_state_title
51
84
 
85
+ self._thread_tasks = dict[str,asyncio.Task]()
52
86
 
53
- async def greet(self, *, chat_context: AgentChatContext, messages: Chan[RoomMessage], participant: RemoteParticipant):
87
+ def init_requirements(self, requires: list[Requirement]):
88
+ if requires == None:
54
89
 
55
- if self._auto_greet_prompt != None:
56
- messages.send_nowait(RoomMessage(from_participant_id=participant.id, type="chat", message={"text": self._auto_greet_prompt }))
90
+ requires = [
91
+ RequiredSchema(
92
+ name="thread"
93
+ )
94
+ ]
95
+
96
+ else:
97
+
98
+ thread_schema = list(n for n in requires if (isinstance(n, RequiredSchema) and n.name == "thread"))
99
+ if len(thread_schema) == 0:
100
+ requires.append(
101
+ RequiredSchema(
102
+ name="thread"
103
+ )
104
+ )
105
+
106
+ async def _send_and_save_chat(self, messages: Element, path: str, to: RemoteParticipant, id: str, text: str):
107
+
108
+ await self.room.messaging.send_message(to=to, type="chat", message={ "path" : path, "text" : text })
109
+
110
+ messages.append_child(tag_name="message", attributes={
111
+ "id" : id,
112
+ "text" : text,
113
+ "created_at" : datetime.datetime.now(datetime.timezone.utc).isoformat().replace("+00:00","Z"),
114
+ "author_name" : self.room.local_participant.get_attribute("name"),
115
+ })
57
116
 
117
+
118
+ async def greet(self, *, messages: Element, path: str, chat_context: AgentChatContext, participant: RemoteParticipant):
119
+
58
120
  if self._auto_greet_message != None:
59
121
  chat_context.append_user_message(self._auto_greet_message)
60
-
61
- await self.room.messaging.send_message(to=RemoteParticipant(id=participant.id), type="chat", message={ "text" : self._auto_greet_message })
62
-
122
+ await self._send_and_save_chat(id=str(uuid.uuid4()), to=RemoteParticipant(id=participant.id), messages=messages, path=path, text= self._auto_greet_message)
123
+
63
124
 
64
- async def finalize_toolkits(self, toolkits) -> list[Toolkit]:
125
+ async def get_thread_participants(self, *, thread: MeshDocument):
126
+ return get_thread_participants(room=self._room, thread=thread)
127
+
128
+ async def init_thread_context(self, *, thread_context: ChatThreadContext) -> list[Toolkit]:
65
129
 
66
130
  toaster = None
67
131
 
68
- for toolkit in toolkits:
132
+ for toolkit in thread_context.toolkits:
69
133
 
70
134
  if toolkit.name == "meshagent.ui":
71
135
 
@@ -83,168 +147,223 @@ class ChatBot(SingleRoomAgent):
83
147
 
84
148
  return MultiToolkit(required=[ toaster ], base_toolkit=toolkit )
85
149
 
86
- toolkits = list(map(multi_tool, toolkits))
150
+ toolkits = list(map(multi_tool, thread_context.toolkits))
87
151
 
88
- return toolkits
152
+ thread_context.toolkits = toolkits
89
153
 
90
- async def _spawn_thread(self, participant_id: str, messages: Chan[RoomMessage]):
154
+
155
+ async def init_chat_context(self) -> AgentChatContext:
156
+ context = self._llm_adapter.create_chat_context()
157
+ context.append_rules(self._rules)
158
+ return context
91
159
 
92
-
93
- chat_context = await self.init_chat_context()
94
-
160
+ async def open_thread(self, *, path: str):
95
161
 
96
- chat_context.append_rules(
97
- rules=[
98
- *self._rules,
99
- "think step by step",
100
- ]
101
- )
102
162
 
103
- opened = False
104
- chat_with_participant = None
163
+ return await self.room.sync.open(path=path)
164
+
165
+ async def close_thread(self, *, path: str):
105
166
 
106
- for participant in self._room.messaging.get_participants():
107
- if participant.id == participant_id:
108
- chat_with_participant = participant
109
- break
167
+ return await self.room.sync.close(path=path)
110
168
 
111
- if chat_with_participant == None:
112
- raise RoomException(f"caller did not have messaging turned on")
113
-
114
- messaging = self._room.messaging
115
169
 
170
+ async def _spawn_thread(self, path: str, messages: Chan[RoomMessage]):
171
+
172
+ self.room.developer.log_nowait(type="chatbot.thread.started", data={ "path" : path })
173
+ chat_context = await self.init_chat_context()
174
+ opened = False
175
+ thread = None
176
+ doc_messages = None
116
177
  current_file = None
178
+ llm_messages = Chan[ResponseStreamEvent]()
117
179
 
118
- step_schema = {
119
- "type" : "object",
120
- "required" : ["text","finished"],
121
- "additionalProperties" : False,
122
- "description" : "execute a step",
123
- "properties" : {
124
- "text" : {
125
- "description" : "a reply to the user or status to display during an intermediate step",
126
- "type" : "string"
127
- },
128
- "finished" : {
129
- "description" : "whether the agent has finished answering the user's last message. you MUST set this to true if there are no more tool calls to be made or you are stuck in a loop.",
130
- "type" : "boolean"
131
- }
132
- }
133
- }
134
-
135
- installed = False
136
-
137
- while True:
138
-
139
- while True:
180
+ def done_processing_llm_events(task: asyncio.Task):
181
+ try:
182
+ task.result()
183
+ except Exception as e:
184
+ logger.error("error sending delta", exc_info=e)
140
185
 
141
-
186
+ async def process_llm_events():
142
187
 
143
- received = await messages.recv()
188
+ partial = ""
189
+ content_element = None
190
+ context_message = None
144
191
 
192
+ async for evt in llm_messages:
145
193
 
146
-
147
- if current_file != chat_with_participant.get_attribute("current_file"):
148
- logger.info(f"participant is now looking at {chat_with_participant.get_attribute("current_file")}")
149
- current_file = chat_with_participant.get_attribute("current_file")
194
+ #await self.room.messaging.send_message(to=chat_with_participant, type="llm.event", message=evt)
195
+
196
+ if evt.type == "response.content_part.added":
197
+ partial = ""
198
+ content_element = doc_messages.append_child(tag_name="message", attributes={
199
+ "text" : "",
200
+ "created_at" : datetime.datetime.now(datetime.timezone.utc).isoformat().replace("+00:00","Z"),
201
+ "author_name" : self.room.local_participant.get_attribute("name"),
202
+ })
203
+
204
+ context_message = {
205
+ "role" : "assistant",
206
+ "content" : ""
207
+ }
208
+ chat_context.messages.append(context_message)
209
+
210
+ elif evt.type == "response.output_text.delta":
211
+ partial += evt.delta
212
+ content_element["text"] = partial
213
+ context_message["content"] = partial
150
214
 
151
- if current_file != None:
152
- chat_context.append_assistant_message(message=f"the user is currently viewing the file at the path: {current_file}")
153
-
154
- elif current_file != None:
155
- chat_context.append_assistant_message(message=f"the user is not current viewing any files")
156
-
215
+ elif evt.type == "response.output_text.done":
216
+ content_element = None
157
217
 
218
+ llm_task = asyncio.create_task(process_llm_events())
219
+ llm_task.add_done_callback(done_processing_llm_events)
158
220
 
159
- if installed == False:
160
- installed = True
161
- await self.install_requirements(participant_id=participant_id)
221
+ try:
222
+ while True:
223
+
224
+ while True:
162
225
 
226
+ received = await messages.recv()
163
227
 
164
- if received.type == "opened":
165
-
166
-
167
- if opened == False:
228
+ chat_with_participant = None
229
+ for participant in self._room.messaging.get_participants():
230
+ if participant.id == received.from_participant_id:
231
+ chat_with_participant = participant
232
+ break
168
233
 
169
- opened = True
234
+ if chat_with_participant == None:
235
+ logger.warning("participant does not have messaging enabled, skipping message")
236
+ continue
237
+
238
+ if current_file != chat_with_participant.get_attribute("current_file"):
239
+ logger.info(f"participant is now looking at {chat_with_participant.get_attribute("current_file")}")
240
+ current_file = chat_with_participant.get_attribute("current_file")
170
241
 
171
- await self.greet(chat_context=chat_context, participant=chat_with_participant, messages=messages)
242
+ if current_file != None:
243
+ chat_context.append_assistant_message(message=f"the user is currently viewing the file at the path: {current_file}")
172
244
 
173
- if received.type == "chat":
245
+ elif current_file != None:
246
+ chat_context.append_assistant_message(message=f"the user is not current viewing any files")
174
247
 
175
-
176
- await self._room.messaging.send_message(to=chat_with_participant, type="thinking", message={"thinking":True})
177
-
178
- if chat_with_participant.id == received.from_participant_id:
179
- self.room.developer.log_nowait(type="llm.message", data={ "context" : chat_context.id, "participant_id" : self.room.local_participant.id, "participant_name" : self.room.local_participant.get_attribute("name"), "message" : { "content" : { "role" : "user", "text" : received.message["text"] } } })
180
248
 
181
- text = received.message["text"]
182
- attachments = received.message.get("attachments", [])
249
+ if thread == None:
250
+ thread = await self.open_thread(path=path)
251
+
252
+ for prop in thread.root.get_children():
253
+
254
+ if prop.tag_name == "messages":
255
+
256
+ doc_messages = prop
257
+
258
+ for element in doc_messages.get_children():
259
+
260
+ if isinstance(element, Element):
261
+
262
+ msg = element["text"]
263
+ if element["author_name"] == self.room.local_participant.get_attribute("name"):
264
+ chat_context.append_assistant_message(msg)
265
+ else:
266
+ chat_context.append_user_message(msg)
267
+
268
+ if doc_messages == None:
269
+ raise Exception("thread was not properly initialized")
183
270
 
184
- for attachment in attachments:
185
271
 
186
- chat_context.append_assistant_message(message=f"the user attached a file '{attachment["filename"]}' with the content: '{attachment["content"]}'")
272
+ if received.type == "opened":
273
+
274
+ if opened == False:
187
275
 
188
-
189
- chat_context.append_user_message(message=text)
276
+ opened = True
190
277
 
278
+ await self.greet(path=path, chat_context=chat_context, participant=chat_with_participant, messages=doc_messages)
191
279
 
192
- # if user is typing, wait for typing to stop
193
- while True:
280
+ if received.type == "chat":
194
281
 
195
- if chat_with_participant.id not in self._is_typing:
282
+ if thread == None:
283
+
284
+ self.room.developer.log_nowait(type="thread is not open", data={})
285
+
196
286
  break
287
+
288
+
289
+ text = received.message["text"]
290
+
291
+
292
+ for participant in get_thread_participants(room=self._room, thread=thread):
293
+ # TODO: async gather
294
+ await self._room.messaging.send_message(to=participant, type="thinking", message={"thinking":True, "path": path})
295
+
296
+ if chat_with_participant.id == received.from_participant_id:
297
+ self.room.developer.log_nowait(type="llm.message", data={ "context" : chat_context.id, "participant_id" : self.room.local_participant.id, "participant_name" : self.room.local_participant.get_attribute("name"), "message" : { "content" : { "role" : "user", "text" : received.message["text"] } } })
197
298
 
198
- await asyncio.sleep(.5)
299
+
300
+ attachments = received.message.get("attachments", [])
199
301
 
200
- if messages.empty() == True:
201
- break
202
-
302
+ for attachment in attachments:
203
303
 
204
- try:
205
- while True:
304
+ chat_context.append_assistant_message(message=f"the user attached a file '{attachment["filename"]}' with the content: '{attachment["content"]}'")
305
+
206
306
 
307
+ chat_context.append_user_message(message=text)
308
+
309
+
310
+ # if user is typing, wait for typing to stop
311
+ while True:
312
+
313
+ if chat_with_participant.id not in self._is_typing:
314
+ break
315
+
316
+ await asyncio.sleep(.5)
317
+
318
+ if messages.empty() == True:
319
+ break
320
+
321
+
322
+ try:
323
+
207
324
  toolkits = [
208
325
  *self._toolkits,
209
326
  *await self.get_required_tools(participant_id=chat_with_participant.id)
210
327
  ]
211
328
 
212
- toolkits = await self.finalize_toolkits(toolkits)
213
-
214
- response = await self._llm_adapter.next(
215
- context=chat_context,
216
- room=self._room,
329
+ thread_context = ChatThreadContext(
330
+ chat=chat_context,
331
+ thread=thread,
217
332
  toolkits=toolkits,
218
- tool_adapter=self._tool_adapter,
219
- output_schema=step_schema,
220
333
  )
221
334
 
222
- text = response["text"]
223
-
224
-
225
- if response["finished"] or len(toolkits) == 0:
226
- await self._room.messaging.send_message(
227
- to=chat_with_participant,
228
- type="chat",
229
- message={
230
- "text": text
231
- }
232
- )
233
- break
234
- else:
235
- await self._room.messaging.send_message(
236
- to=chat_with_participant,
237
- type="status",
238
- message={
239
- "text": text
240
- }
335
+ await self.init_thread_context(thread_context=thread_context)
336
+
337
+ def handle_event(evt):
338
+ llm_messages.send_nowait(evt)
339
+
340
+ try:
341
+ response = await self._llm_adapter.next(
342
+ context=chat_context,
343
+ room=self._room,
344
+ toolkits=toolkits,
345
+ tool_adapter=self._tool_adapter,
346
+ event_handler=handle_event
241
347
  )
242
- chat_context.append_user_message(message="proceed to the next step if you are ready")
243
-
244
- finally:
348
+ except Exception as e:
349
+ logger.error("An error was encountered", exc_info=e)
350
+ await self._send_and_save_chat(messages=doc_messages, to=chat_with_participant, path=path, id=str(uuid.uuid4()), text="There was an error while communicating with the LLM. Please try again later.")
351
+
352
+
353
+ finally:
354
+ for participant in get_thread_participants(room=self._room, thread=thread):
355
+ # TODO: async gather
356
+ await self._room.messaging.send_message(to=participant, type="thinking", message={"thinking":False, "path" : path})
357
+
358
+
359
+ finally:
245
360
 
246
- await self._room.messaging.send_message(to=chat_with_participant, type="thinking", message={"thinking":False})
361
+ self.room.developer.log_nowait(type="chatbot.thread.ended", data={ "path" : path })
362
+
363
+ llm_messages.close()
247
364
 
365
+ if thread != None:
366
+ await self.close_thread(path=path)
248
367
 
249
368
 
250
369
  def _get_message_channel(self, participant_id: str) -> Chan[RoomMessage]:
@@ -252,37 +371,50 @@ class ChatBot(SingleRoomAgent):
252
371
  chan = Chan[RoomMessage]()
253
372
  self._message_channels[participant_id] = chan
254
373
 
255
- def thread_done(task: asyncio.Task):
256
-
257
- self._message_channels.pop(participant_id)
258
- try:
259
- task.result()
260
- logger.info("ending chat thread")
261
- except Exception as e:
262
- logger.error("chat thread error", exc_info=e)
263
-
264
- task = asyncio.create_task(self._spawn_thread(participant_id=participant_id, messages=chan))
265
- task.add_done_callback(thread_done)
266
-
267
374
  chan = self._message_channels[participant_id]
268
375
 
269
376
  return chan
270
377
 
271
- async def start(self, *, room):
378
+ async def stop(self):
379
+ await super().stop()
272
380
 
273
- await super().start(room=room)
381
+ for thread in self._thread_tasks.values():
382
+ thread.cancel()
383
+
384
+ self._thread_tasks.clear()
274
385
 
386
+ async def start(self, *, room):
275
387
 
388
+ await super().start(room=room)
389
+
276
390
  await self.room.local_participant.set_attribute("empty_state_title", self._empty_state_title)
277
391
 
278
392
  def on_message(message: RoomMessage):
393
+
279
394
  messages = self._get_message_channel(participant_id=message.from_participant_id)
280
395
  if message.type == "chat" or message.type == "opened":
281
396
  messages.send_nowait(message)
282
-
283
- elif message.type == "typing":
284
397
 
398
+ path = message.message["path"]
399
+ logger.info(f"received message for thread {path}")
285
400
 
401
+ if path not in self._thread_tasks or self._thread_tasks[path].cancelled:
402
+
403
+ def thread_done(task: asyncio.Task):
404
+
405
+ self._message_channels.pop(message.from_participant_id)
406
+ try:
407
+ task.result()
408
+ except Exception as e:
409
+ logger.error(f"The chat thread ended with an error {e}", exc_info=e)
410
+
411
+
412
+ task = asyncio.create_task(self._spawn_thread(messages=messages, path=path))
413
+ task.add_done_callback(thread_done)
414
+
415
+ self._thread_tasks[path] = task
416
+
417
+ elif message.type == "typing":
286
418
  def callback(task: asyncio.Task):
287
419
  try:
288
420
  task.result()
@@ -303,7 +435,7 @@ class ChatBot(SingleRoomAgent):
303
435
 
304
436
  room.messaging.on("message", on_message)
305
437
 
306
- if self._auto_greet_prompt != None or self._auto_greet_message != None:
438
+ if self._auto_greet_message != None:
307
439
  def on_participant_added(participant:RemoteParticipant):
308
440
 
309
441
  # will spawn the initial thread
@@ -16,6 +16,9 @@ class AgentChatContext:
16
16
  if system_role == None:
17
17
  system_role = "system"
18
18
  self._system_role = system_role
19
+
20
+ self._previous_response_id = None
21
+ self._previous_messages = list[dict]()
19
22
 
20
23
  @property
21
24
  def messages(self):
@@ -24,6 +27,19 @@ class AgentChatContext:
24
27
  @property
25
28
  def system_role(self):
26
29
  return self._system_role
30
+
31
+ @property
32
+ def previous_messages(self):
33
+ return self._previous_messages
34
+
35
+ @property
36
+ def previous_response_id(self):
37
+ return self._previous_response_id
38
+
39
+ def create_response(self, id: str):
40
+ self._previous_response_id = id
41
+ self._previous_messages.extend(self.messages)
42
+ self.messages.clear()
27
43
 
28
44
  def append_rules(self, rules: list[str]):
29
45
 
@@ -283,9 +283,6 @@ class StorageIndexer(SingleRoomAgent):
283
283
 
284
284
  self._index_task = asyncio.create_task(self._indexer())
285
285
  self._index_task.add_done_callback(index_task)
286
-
287
- await self.install_requirements()
288
-
289
286
 
290
287
  async def stop(self):
291
288
  await super().stop()
@@ -7,7 +7,7 @@ from meshagent.api.schema import MeshSchema
7
7
  from meshagent.agents.writer import Writer, WriterContext
8
8
  from meshagent.agents.adapter import LLMAdapter, ToolResponseAdapter
9
9
  from meshagent.api.schema import MeshSchema, ElementType, ChildProperty, ValueProperty
10
- from meshagent.agents.schema import merge
10
+ from meshagent.api.schema_util import merge
11
11
  from meshagent.tools.document_tools import build_tools, DocumentAuthoringToolkit
12
12
  from meshagent.agents import TaskRunner
13
13
  from copy import deepcopy
@@ -120,7 +120,7 @@ def reasoning_schema(*, description: str, elements: Optional[list[ElementType]]
120
120
  *elements
121
121
  ])
122
122
 
123
- from .schema import prompt_schema
123
+ from meshagent.api.schema_util import prompt_schema
124
124
  import logging
125
125
  logging.basicConfig()
126
126
  logger = logging.getLogger("planning_agent")
@@ -1,9 +1,10 @@
1
1
 
2
2
  from .adapter import LLMAdapter, Toolkit, ToolResponseAdapter
3
- from .schema import prompt_schema
3
+ from meshagent.api.schema_util import prompt_schema
4
4
  from .agent import AgentCallContext
5
5
  from typing import Optional
6
6
  from meshagent.agents import TaskRunner
7
+ from meshagent.api import RequiredToolkit
7
8
 
8
9
  # An agent that takes a simple prompt and gets the result
9
10
  class PromptAgent(TaskRunner):
@@ -16,7 +17,10 @@ class PromptAgent(TaskRunner):
16
17
  tools: list[Toolkit] = [],
17
18
  rules: list[str] = [],
18
19
  title: Optional[str] = None,
19
- description: Optional[str] = None
20
+ description: Optional[str] = None,
21
+ requires: Optional[list[RequiredToolkit]] = None,
22
+ supports_tools: Optional[bool] = None,
23
+ labels: Optional[list[str]] = None
20
24
  ):
21
25
  super().__init__(
22
26
  name=name,
@@ -26,7 +30,9 @@ class PromptAgent(TaskRunner):
26
30
  description=description
27
31
  ),
28
32
  output_schema=output_schema,
29
-
33
+ requires=requires,
34
+ supports_tools=supports_tools,
35
+ labels=labels
30
36
  )
31
37
  self.rules = rules
32
38
  self.tools = tools
@@ -6,12 +6,13 @@ from .agent import AgentCallContext
6
6
  from .writer import Writer, WriterContext
7
7
  import logging
8
8
  from typing import Optional
9
+ from meshagent.api import RoomClient
9
10
 
10
11
  from .agent import TaskRunner, RequiredToolkit, Requirement, RequiredSchema
11
12
 
12
13
  from .adapter import ToolResponseAdapter
13
-
14
- from meshagent.tools.pydantic import get_pydantic_ai_tools_from_context
14
+ from meshagent.tools.toolkit import Tool, Response, ToolContext
15
+ from meshagent.tools.pydantic import get_pydantic_ai_tool_definition
15
16
 
16
17
  from typing import Sequence
17
18
  import pydantic_ai
@@ -19,6 +20,35 @@ import pydantic_ai
19
20
  logger = logging.getLogger("pydantic_agent")
20
21
  logger.setLevel(logging.INFO)
21
22
 
23
+
24
+ def get_pydantic_ai_tool(*, room: RoomClient, tool: Tool, response_adapter: ToolResponseAdapter) -> pydantic_ai.tools.Tool:
25
+ async def prepare(ctx: pydantic_ai.RunContext, tool_def: pydantic_ai.tools.ToolDefinition):
26
+ return get_pydantic_ai_tool_definition(tool=tool)
27
+
28
+ async def execute(**kwargs):
29
+ response = await tool.execute(context=ToolContext(room=room, caller=room.local_participant), **kwargs)
30
+ return await response_adapter.to_plain_text(room=room, response=response)
31
+
32
+ return pydantic_ai.Tool(
33
+ name=tool.name,
34
+ takes_ctx=False,
35
+ description=tool.description,
36
+ prepare=prepare,
37
+ function=execute
38
+ )
39
+
40
+ def get_pydantic_ai_tools_from_context(*, context: AgentCallContext, response_adapter: ToolResponseAdapter) -> list[pydantic_ai.tools.Tool]:
41
+
42
+ tools = list[pydantic_ai.tools.Tool]()
43
+
44
+ for toolkit in context.toolkits:
45
+
46
+ for tool in toolkit.tools:
47
+
48
+ tools.append(get_pydantic_ai_tool(room=context.room, tool=tool, response_adapter=response_adapter))
49
+
50
+ return tools
51
+
22
52
  class PydanticAgent[TInput:BaseModel, TOutput:BaseModel](TaskRunner):
23
53
  def __init__(self,
24
54
  *,
@@ -3,7 +3,7 @@ from meshagent.api.schema import ValueProperty, ChildProperty
3
3
  from meshagent.tools import Toolkit
4
4
  from meshagent.agents.writer import Writer, WriterContext
5
5
  from meshagent.agents.adapter import LLMAdapter, ToolResponseAdapter
6
- from .schema import prompt_schema, merge
6
+ from meshagent.api.schema_util import prompt_schema, merge
7
7
  from typing import Optional
8
8
  from meshagent.api import Requirement
9
9
 
@@ -0,0 +1,59 @@
1
+ from meshagent.api.schema import MeshSchema, ElementType, ChildProperty, ValueProperty
2
+
3
+ thread_schema = MeshSchema(
4
+ root_tag_name="thread",
5
+ elements=[
6
+ ElementType(
7
+ tag_name="thread",
8
+ description="a thread of messages",
9
+ properties=[
10
+ ChildProperty(name="properties", description="the messages in the thread", ordered=True, child_tag_names=[
11
+ "members", "messages"
12
+ ]),
13
+ ]
14
+ ),
15
+ ElementType(
16
+ tag_name="members",
17
+ description="the members of this thread",
18
+ properties=[
19
+ ChildProperty(name="items", child_tag_names=["member"], description="the messages in this thread")
20
+ ]
21
+ ),
22
+ ElementType(
23
+ tag_name="messages",
24
+ description="the messages of this thread",
25
+ properties=[
26
+ ChildProperty(name="items", child_tag_names=["message"], description="the messages in this thread")
27
+ ]
28
+ ),
29
+ ElementType(
30
+ tag_name="member",
31
+ description="a member of this thread",
32
+ properties=[
33
+ ValueProperty(name="name", description="the name of the member", type="string"),
34
+ ValueProperty(name="type", description="the type of member", type="string", enum=[
35
+ "user", "agent"
36
+ ]),
37
+ ]
38
+ ),
39
+ ElementType(
40
+ tag_name="file",
41
+ description="a file attachment",
42
+ properties=[
43
+ ValueProperty(name="path", description="the path of the file in the room", type="string"),
44
+ ]
45
+ ),
46
+ ElementType(
47
+ tag_name="message",
48
+ description="a message sent in the conversation",
49
+ properties=[
50
+ ValueProperty(name="id", description="the id of the message", type="string"),
51
+ ValueProperty(name="text", description="the text of the message", type="string"),
52
+ ValueProperty(name="created_at", description="the date that the message was sent in ISO format", type="string"),
53
+ ValueProperty(name="author_name", description="the name of the author of the post", type="string"),
54
+ ValueProperty(name="author_ref", description="a reference to author identity in another system", type="string"),
55
+ ChildProperty(name="attachments", child_tag_names=["file"], description="a list of message attachments")
56
+ ]
57
+ ),
58
+ ]
59
+ )
@@ -1 +1 @@
1
- __version__ = "0.0.1"
1
+ __version__ = "0.0.4"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: meshagent-agents
3
- Version: 0.0.1
3
+ Version: 0.0.4
4
4
  Summary: Agent Building Blocks for Meshagent
5
5
  Home-page:
6
6
  License: Apache License 2.0
@@ -13,9 +13,9 @@ License-File: LICENSE
13
13
  Requires-Dist: pyjwt>=2.0.0
14
14
  Requires-Dist: pytest>=8.3.4
15
15
  Requires-Dist: pytest-asyncio>=0.24.0
16
- Requires-Dist: meshagent-api>=0.0.1
17
- Requires-Dist: meshagent-tools>=0.0.1
18
- Requires-Dist: meshagent-openai>=0.0.1
16
+ Requires-Dist: meshagent-api>=0.0.4
17
+ Requires-Dist: meshagent-tools>=0.0.4
18
+ Requires-Dist: meshagent-openai>=0.0.4
19
19
  Requires-Dist: pydantic>=2.10.4
20
20
  Requires-Dist: pydantic-ai>=0.0.23
21
21
  Requires-Dist: chonkie>=0.5.1
@@ -0,0 +1,22 @@
1
+ meshagent/agents/__init__.py,sha256=uLDRp7W4wG0M2JHmyWBA4oYwYUsqGPD3VVHmcAddGAs,343
2
+ meshagent/agents/adapter.py,sha256=i2ck7a6B8ByXRZgtOH7chc7VT2cpk3uBAdEd93Ts7jI,1278
3
+ meshagent/agents/agent.py,sha256=t9AwoKjF_bgPrJdN9dr73J-CkQy_oAQWyHKB1Q4RPOU,15734
4
+ meshagent/agents/chat.py,sha256=1NsmQECm2WqHhKcegDz40p9FX4dY9W3hjjgztScVc1s,17409
5
+ meshagent/agents/context.py,sha256=1j38JGb0Nc0CZ0UMC_VkKjMraHZdsqBStTGNrP2nanM,3191
6
+ meshagent/agents/development.py,sha256=04VYL1Q_BWUTQeVuiVOpyjcs8bzUIX1eQ4VyTtjc5s0,926
7
+ meshagent/agents/hosting.py,sha256=e2wMGbWFt0WO3CJaTQehwbucftgUNVTgRsrBWiYDgcU,4973
8
+ meshagent/agents/indexer.py,sha256=t7FbUxTSHSS-2pLrYSiCuv7P28y00XA8iJtEsuQjTZA,19399
9
+ meshagent/agents/listener.py,sha256=kKFaWPmvmk1aCAlI55AAU3-QnOWCSZbf621xZz10rbs,5388
10
+ meshagent/agents/planning.py,sha256=DTanAzanhjZ4bU6kWDFTBkw3oVcdtSj3XTKYwntoN0A,23136
11
+ meshagent/agents/prompt.py,sha256=sidiyLLC6Lr9KO3cUTKttd2oROlcizle_4iKoaNrfhA,1864
12
+ meshagent/agents/pydantic.py,sha256=BXALTyj8O9vV-eY5ejiJoYIY4zVl9AKetsqGUxixbiY,6091
13
+ meshagent/agents/single_shot_writer.py,sha256=CJ7KgekR6JUSbYZ8OdYmhBPPjEWDYNtbIjR4g1wkWAA,3405
14
+ meshagent/agents/thread_schema.py,sha256=mmm1p7A6Eux98xsNXwGSNq_DjkMn3hsyaWsplc6akCM,2575
15
+ meshagent/agents/version.py,sha256=2GWffEF1rjyrveUGdfR8y3cl3JUnX0gJjr8Ryy_u4JQ,21
16
+ meshagent/agents/worker.py,sha256=vZ6tnOyjno8N551c5HsXR8Mw3ZPmYRa4o1MZ3i7hEG4,4374
17
+ meshagent/agents/writer.py,sha256=Ff8RVxdpFNKmo6zxaGkuFMz8MIKumdM0FIXX_6lbZ6Q,2738
18
+ meshagent_agents-0.0.4.dist-info/LICENSE,sha256=eTt0SPW-sVNdkZe9PS_S8WfCIyLjRXRl7sUBWdlteFg,10254
19
+ meshagent_agents-0.0.4.dist-info/METADATA,sha256=64GXsnluH0GU9-tYdqzYFBWZuXlU-XzqyLarqvB_cnQ,896
20
+ meshagent_agents-0.0.4.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
21
+ meshagent_agents-0.0.4.dist-info/top_level.txt,sha256=GlcXnHtRP6m7zlG3Df04M35OsHtNXy_DY09oFwWrH74,10
22
+ meshagent_agents-0.0.4.dist-info/RECORD,,
@@ -1,50 +0,0 @@
1
- from copy import deepcopy
2
-
3
- def validation_schema(description: str):
4
- return {
5
- "type" : "object",
6
- "description" : description,
7
- "required" : [ "is_valid", "message" ],
8
- "additionalProperties" : False,
9
- "properties" : {
10
- "is_valid" : {
11
- "type" : "boolean",
12
- },
13
- "message" : {
14
- "type" : "string",
15
- },
16
- }
17
- }
18
-
19
- def prompt_schema(description: str):
20
- return {
21
- "type" : "object",
22
- "description" : description,
23
- "required" : [ "prompt" ],
24
- "additionalProperties" : False,
25
- "properties" : {
26
- "prompt" : {
27
- "description" : "a prompt that will be used by the agent to create a response",
28
- "type" : "string",
29
- }
30
- }
31
- }
32
-
33
- def no_arguments_schema(description: str):
34
- return {
35
- "description" : description,
36
- "type" : "object",
37
- "required" : [],
38
- "additionalProperties" : False,
39
- "properties" : {
40
- }
41
- }
42
-
43
- def merge(*, schema: dict, additional_properties: dict) -> dict:
44
- schema = deepcopy(schema)
45
-
46
- for k,v in additional_properties.items():
47
- schema["required"].append(k)
48
- schema["properties"][k] = v
49
-
50
- return schema
@@ -1,22 +0,0 @@
1
- meshagent/agents/__init__.py,sha256=morriYsu4jkd6oboNO-s531uRTns_br6f4CV_AC7omg,303
2
- meshagent/agents/adapter.py,sha256=fBNmLtHHgZbgWIqGDUPu5PrUV8qM-_E4kWWvHFWmZqg,1066
3
- meshagent/agents/agent.py,sha256=VrjUH_scLe4CuZwsrnGMKVvi4NeUcAahd4K9CGEpwwk,15537
4
- meshagent/agents/chat.py,sha256=JhEscOBipq5liAC_DrPYqyi3v_3td1x25mH5d0GWS5o,11565
5
- meshagent/agents/context.py,sha256=yph6EW5byiKh8znumr6WgN5Us1x6SDPBS3PQo40_krg,2744
6
- meshagent/agents/development.py,sha256=04VYL1Q_BWUTQeVuiVOpyjcs8bzUIX1eQ4VyTtjc5s0,926
7
- meshagent/agents/hosting.py,sha256=e2wMGbWFt0WO3CJaTQehwbucftgUNVTgRsrBWiYDgcU,4973
8
- meshagent/agents/indexer.py,sha256=7JYCnP-A_nqV5l7zhDgyoXhplNldL0_x4ERGhZ0Cx1k,19443
9
- meshagent/agents/listener.py,sha256=kKFaWPmvmk1aCAlI55AAU3-QnOWCSZbf621xZz10rbs,5388
10
- meshagent/agents/planning.py,sha256=CAnfX_ushG4OAvMCUnMs4XNYBoyc7PV_lgvTIMjhjrw,23116
11
- meshagent/agents/prompt.py,sha256=PfdH_7_Re1jEPfDmkwtG74-SwyVA7uRXFk_4_s0us6k,1556
12
- meshagent/agents/pydantic.py,sha256=MUAcc5HWEvnOPPR2DkFVvGykG00_W8BuPwFPrbzher8,4904
13
- meshagent/agents/schema.py,sha256=Efzd-srpHazNq-jZ7bMNZX_TZJOBbfYfDHXbpLu_hhY,1292
14
- meshagent/agents/single_shot_writer.py,sha256=-wMqQTYVYrh_P5Gc9PROOiTeJefdgNGWAYeRFZ2sQVk,3387
15
- meshagent/agents/version.py,sha256=ugyuFliEqtAwQmH4sTlc16YXKYbFWDmfyk87fErB8-8,21
16
- meshagent/agents/worker.py,sha256=vZ6tnOyjno8N551c5HsXR8Mw3ZPmYRa4o1MZ3i7hEG4,4374
17
- meshagent/agents/writer.py,sha256=Ff8RVxdpFNKmo6zxaGkuFMz8MIKumdM0FIXX_6lbZ6Q,2738
18
- meshagent_agents-0.0.1.dist-info/LICENSE,sha256=eTt0SPW-sVNdkZe9PS_S8WfCIyLjRXRl7sUBWdlteFg,10254
19
- meshagent_agents-0.0.1.dist-info/METADATA,sha256=EPKnWFmGl4SIn5Xy17PyisiCqpLWrkpOkvRkqHAvt2g,896
20
- meshagent_agents-0.0.1.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
21
- meshagent_agents-0.0.1.dist-info/top_level.txt,sha256=GlcXnHtRP6m7zlG3Df04M35OsHtNXy_DY09oFwWrH74,10
22
- meshagent_agents-0.0.1.dist-info/RECORD,,