meshagent-agents 0.0.3__tar.gz → 0.0.5__tar.gz

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.

Files changed (32) hide show
  1. {meshagent_agents-0.0.3/meshagent_agents.egg-info → meshagent_agents-0.0.5}/PKG-INFO +4 -4
  2. {meshagent_agents-0.0.3 → meshagent_agents-0.0.5}/meshagent/agents/__init__.py +1 -0
  3. {meshagent_agents-0.0.3 → meshagent_agents-0.0.5}/meshagent/agents/adapter.py +8 -3
  4. {meshagent_agents-0.0.3 → meshagent_agents-0.0.5}/meshagent/agents/agent.py +15 -7
  5. meshagent_agents-0.0.5/meshagent/agents/chat.py +448 -0
  6. {meshagent_agents-0.0.3 → meshagent_agents-0.0.5}/meshagent/agents/context.py +16 -0
  7. {meshagent_agents-0.0.3 → meshagent_agents-0.0.5}/meshagent/agents/indexer.py +0 -3
  8. {meshagent_agents-0.0.3 → meshagent_agents-0.0.5}/meshagent/agents/planning.py +2 -2
  9. {meshagent_agents-0.0.3 → meshagent_agents-0.0.5}/meshagent/agents/prompt.py +9 -3
  10. {meshagent_agents-0.0.3 → meshagent_agents-0.0.5}/meshagent/agents/pydantic.py +32 -2
  11. {meshagent_agents-0.0.3 → meshagent_agents-0.0.5}/meshagent/agents/single_shot_writer.py +1 -1
  12. meshagent_agents-0.0.5/meshagent/agents/thread_schema.py +59 -0
  13. meshagent_agents-0.0.5/meshagent/agents/version.py +1 -0
  14. {meshagent_agents-0.0.3 → meshagent_agents-0.0.5/meshagent_agents.egg-info}/PKG-INFO +4 -4
  15. {meshagent_agents-0.0.3 → meshagent_agents-0.0.5}/meshagent_agents.egg-info/SOURCES.txt +1 -1
  16. {meshagent_agents-0.0.3 → meshagent_agents-0.0.5}/meshagent_agents.egg-info/requires.txt +3 -3
  17. {meshagent_agents-0.0.3 → meshagent_agents-0.0.5}/setup.py +3 -3
  18. meshagent_agents-0.0.3/meshagent/agents/chat.py +0 -328
  19. meshagent_agents-0.0.3/meshagent/agents/schema.py +0 -50
  20. meshagent_agents-0.0.3/meshagent/agents/version.py +0 -1
  21. {meshagent_agents-0.0.3 → meshagent_agents-0.0.5}/LICENSE +0 -0
  22. {meshagent_agents-0.0.3 → meshagent_agents-0.0.5}/MANIFEST.in +0 -0
  23. {meshagent_agents-0.0.3 → meshagent_agents-0.0.5}/README.md +0 -0
  24. {meshagent_agents-0.0.3 → meshagent_agents-0.0.5}/meshagent/agents/development.py +0 -0
  25. {meshagent_agents-0.0.3 → meshagent_agents-0.0.5}/meshagent/agents/hosting.py +0 -0
  26. {meshagent_agents-0.0.3 → meshagent_agents-0.0.5}/meshagent/agents/listener.py +0 -0
  27. {meshagent_agents-0.0.3 → meshagent_agents-0.0.5}/meshagent/agents/worker.py +0 -0
  28. {meshagent_agents-0.0.3 → meshagent_agents-0.0.5}/meshagent/agents/writer.py +0 -0
  29. {meshagent_agents-0.0.3 → meshagent_agents-0.0.5}/meshagent_agents.egg-info/dependency_links.txt +0 -0
  30. {meshagent_agents-0.0.3 → meshagent_agents-0.0.5}/meshagent_agents.egg-info/top_level.txt +0 -0
  31. {meshagent_agents-0.0.3 → meshagent_agents-0.0.5}/pyproject.toml +0 -0
  32. {meshagent_agents-0.0.3 → meshagent_agents-0.0.5}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: meshagent-agents
3
- Version: 0.0.3
3
+ Version: 0.0.5
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.3
17
- Requires-Dist: meshagent-tools>=0.0.3
18
- Requires-Dist: meshagent-openai>=0.0.3
16
+ Requires-Dist: meshagent-api>=0.0.5
17
+ Requires-Dist: meshagent-tools>=0.0.5
18
+ Requires-Dist: meshagent-openai>=0.0.5
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
@@ -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
 
@@ -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:
@@ -0,0 +1,448 @@
1
+ from .agent import SingleRoomAgent, AgentChatContext, AgentCallContext
2
+ from meshagent.api.chan import Chan
3
+ from meshagent.api import RoomMessage, RoomException, RoomClient, RemoteParticipant, RequiredSchema, Requirement, Element, MeshDocument
4
+ from meshagent.tools import Toolkit
5
+ from .adapter import LLMAdapter, ToolResponseAdapter
6
+ import asyncio
7
+ from typing import Optional
8
+ import logging
9
+ from meshagent.tools import MultiToolkit
10
+ import urllib
11
+ import uuid
12
+ import datetime
13
+
14
+ from openai.types.responses import ResponseStreamEvent
15
+
16
+ logging.basicConfig()
17
+ logger = logging.getLogger("chat")
18
+ logger.setLevel(logging.INFO)
19
+
20
+
21
+ # todo: thread should stop when participant stops?
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
+
52
+ class ChatBot(SingleRoomAgent):
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
+
55
+ super().__init__(
56
+ name=name,
57
+ title=title,
58
+ description=description,
59
+ requires=requires,
60
+ labels=labels
61
+ )
62
+
63
+ if toolkits == None:
64
+ toolkits = []
65
+
66
+ self._llm_adapter = llm_adapter
67
+ self._tool_adapter = tool_adapter
68
+
69
+ self._message_channels = dict[str, Chan[RoomMessage]]()
70
+
71
+ self._room : RoomClient | None = None
72
+ self._toolkits = toolkits
73
+
74
+ if rules == None:
75
+ rules = []
76
+
77
+ self._rules = rules
78
+ self._is_typing = dict[str,asyncio.Task]()
79
+ self._auto_greet_message = auto_greet_message
80
+
81
+ if empty_state_title == None:
82
+ empty_state_title = "How can I help you?"
83
+ self._empty_state_title = empty_state_title
84
+
85
+ self._thread_tasks = dict[str,asyncio.Task]()
86
+
87
+ def init_requirements(self, requires: list[Requirement]):
88
+ if requires == None:
89
+
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
+ })
116
+
117
+
118
+ async def greet(self, *, messages: Element, path: str, chat_context: AgentChatContext, participant: RemoteParticipant):
119
+
120
+ if self._auto_greet_message != None:
121
+ chat_context.append_user_message(self._auto_greet_message)
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
+
124
+
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]:
129
+
130
+ toaster = None
131
+
132
+ for toolkit in thread_context.toolkits:
133
+
134
+ if toolkit.name == "meshagent.ui":
135
+
136
+ for tool in toolkit.tools:
137
+
138
+ if tool.name == "show_toast":
139
+
140
+ toaster = tool
141
+
142
+ if toaster != None:
143
+
144
+ def multi_tool(toolkit: Toolkit):
145
+ if toaster in toolkit.tools:
146
+ return toolkit
147
+
148
+ return MultiToolkit(required=[ toaster ], base_toolkit=toolkit )
149
+
150
+ toolkits = list(map(multi_tool, thread_context.toolkits))
151
+
152
+ thread_context.toolkits = toolkits
153
+
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
159
+
160
+ async def open_thread(self, *, path: str):
161
+
162
+
163
+ return await self.room.sync.open(path=path)
164
+
165
+ async def close_thread(self, *, path: str):
166
+
167
+ return await self.room.sync.close(path=path)
168
+
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
177
+ current_file = None
178
+ llm_messages = Chan[ResponseStreamEvent]()
179
+
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)
185
+
186
+ async def process_llm_events():
187
+
188
+ partial = ""
189
+ content_element = None
190
+ context_message = None
191
+
192
+ async for evt in llm_messages:
193
+
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
214
+
215
+ elif evt.type == "response.output_text.done":
216
+ content_element = None
217
+
218
+ llm_task = asyncio.create_task(process_llm_events())
219
+ llm_task.add_done_callback(done_processing_llm_events)
220
+
221
+ try:
222
+ while True:
223
+
224
+ while True:
225
+
226
+ received = await messages.recv()
227
+
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
233
+
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")
241
+
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}")
244
+
245
+ elif current_file != None:
246
+ chat_context.append_assistant_message(message=f"the user is not current viewing any files")
247
+
248
+
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")
270
+
271
+
272
+ if received.type == "opened":
273
+
274
+ if opened == False:
275
+
276
+ opened = True
277
+
278
+ await self.greet(path=path, chat_context=chat_context, participant=chat_with_participant, messages=doc_messages)
279
+
280
+ if received.type == "chat":
281
+
282
+ if thread == None:
283
+
284
+ self.room.developer.log_nowait(type="thread is not open", data={})
285
+
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"] } } })
298
+
299
+
300
+ attachments = received.message.get("attachments", [])
301
+
302
+ for attachment in attachments:
303
+
304
+ chat_context.append_assistant_message(message=f"the user attached a file '{attachment["filename"]}' with the content: '{attachment["content"]}'")
305
+
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
+
324
+ toolkits = [
325
+ *self._toolkits,
326
+ *await self.get_required_tools(participant_id=chat_with_participant.id)
327
+ ]
328
+
329
+ thread_context = ChatThreadContext(
330
+ chat=chat_context,
331
+ thread=thread,
332
+ toolkits=toolkits,
333
+ )
334
+
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
347
+ )
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:
360
+
361
+ self.room.developer.log_nowait(type="chatbot.thread.ended", data={ "path" : path })
362
+
363
+ llm_messages.close()
364
+
365
+ if thread != None:
366
+ await self.close_thread(path=path)
367
+
368
+
369
+ def _get_message_channel(self, participant_id: str) -> Chan[RoomMessage]:
370
+ if participant_id not in self._message_channels:
371
+ chan = Chan[RoomMessage]()
372
+ self._message_channels[participant_id] = chan
373
+
374
+ chan = self._message_channels[participant_id]
375
+
376
+ return chan
377
+
378
+ async def stop(self):
379
+ await super().stop()
380
+
381
+ for thread in self._thread_tasks.values():
382
+ thread.cancel()
383
+
384
+ self._thread_tasks.clear()
385
+
386
+ async def start(self, *, room):
387
+
388
+ await super().start(room=room)
389
+
390
+ await self.room.local_participant.set_attribute("empty_state_title", self._empty_state_title)
391
+
392
+ def on_message(message: RoomMessage):
393
+
394
+ messages = self._get_message_channel(participant_id=message.from_participant_id)
395
+ if message.type == "chat" or message.type == "opened":
396
+ messages.send_nowait(message)
397
+
398
+ path = message.message["path"]
399
+ logger.info(f"received message for thread {path}")
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":
418
+ def callback(task: asyncio.Task):
419
+ try:
420
+ task.result()
421
+ except:
422
+ pass
423
+
424
+ async def remove_timeout(id: str):
425
+ await asyncio.sleep(1)
426
+ self._is_typing.pop(id)
427
+
428
+ if message.from_participant_id in self._is_typing:
429
+ self._is_typing[message.from_participant_id].cancel()
430
+
431
+ timeout = asyncio.create_task(remove_timeout(id=message.from_participant_id))
432
+ timeout.add_done_callback(callback)
433
+
434
+ self._is_typing[message.from_participant_id] = timeout
435
+
436
+ room.messaging.on("message", on_message)
437
+
438
+ if self._auto_greet_message != None:
439
+ def on_participant_added(participant:RemoteParticipant):
440
+
441
+ # will spawn the initial thread
442
+ self._get_message_channel(participant_id=participant.id)
443
+
444
+
445
+ room.messaging.on("participant_added", on_participant_added)
446
+
447
+ await room.messaging.enable()
448
+
@@ -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
+ )
@@ -0,0 +1 @@
1
+ __version__ = "0.0.5"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: meshagent-agents
3
- Version: 0.0.3
3
+ Version: 0.0.5
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.3
17
- Requires-Dist: meshagent-tools>=0.0.3
18
- Requires-Dist: meshagent-openai>=0.0.3
16
+ Requires-Dist: meshagent-api>=0.0.5
17
+ Requires-Dist: meshagent-tools>=0.0.5
18
+ Requires-Dist: meshagent-openai>=0.0.5
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
@@ -15,8 +15,8 @@ meshagent/agents/listener.py
15
15
  meshagent/agents/planning.py
16
16
  meshagent/agents/prompt.py
17
17
  meshagent/agents/pydantic.py
18
- meshagent/agents/schema.py
19
18
  meshagent/agents/single_shot_writer.py
19
+ meshagent/agents/thread_schema.py
20
20
  meshagent/agents/version.py
21
21
  meshagent/agents/worker.py
22
22
  meshagent/agents/writer.py
@@ -1,9 +1,9 @@
1
1
  pyjwt>=2.0.0
2
2
  pytest>=8.3.4
3
3
  pytest-asyncio>=0.24.0
4
- meshagent-api>=0.0.3
5
- meshagent-tools>=0.0.3
6
- meshagent-openai>=0.0.3
4
+ meshagent-api>=0.0.5
5
+ meshagent-tools>=0.0.5
6
+ meshagent-openai>=0.0.5
7
7
  pydantic>=2.10.4
8
8
  pydantic-ai>=0.0.23
9
9
  chonkie>=0.5.1
@@ -28,9 +28,9 @@ setuptools.setup(
28
28
  "pyjwt>=2.0.0",
29
29
  "pytest>=8.3.4",
30
30
  "pytest-asyncio>=0.24.0",
31
- "meshagent-api>=0.0.3",
32
- "meshagent-tools>=0.0.3",
33
- "meshagent-openai>=0.0.3",
31
+ "meshagent-api>=0.0.5",
32
+ "meshagent-tools>=0.0.5",
33
+ "meshagent-openai>=0.0.5",
34
34
  "pydantic>=2.10.4",
35
35
  "pydantic-ai>=0.0.23",
36
36
  "chonkie>=0.5.1",
@@ -1,328 +0,0 @@
1
- from .agent import SingleRoomAgent, AgentChatContext
2
- from meshagent.api.chan import Chan
3
- from meshagent.api import RoomMessage, RoomException, RoomClient, RemoteParticipant
4
- from meshagent.tools import Toolkit
5
- from .adapter import LLMAdapter, ToolResponseAdapter
6
- import asyncio
7
- from typing import Optional
8
- import logging
9
- from meshagent.tools import MultiToolkit
10
- import urllib
11
-
12
- logging.basicConfig()
13
- logger = logging.getLogger("chat")
14
- logger.setLevel(logging.INFO)
15
-
16
-
17
- # todo: thread should stop when participant stops?
18
-
19
- 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):
21
- super().__init__(
22
- name=name,
23
- title=title,
24
- description=description,
25
- requires=requires,
26
- labels=labels
27
- )
28
-
29
- if toolkits == None:
30
- toolkits = []
31
-
32
- self._llm_adapter = llm_adapter
33
- self._tool_adapter = tool_adapter
34
-
35
- self._message_channels = dict[str, Chan[RoomMessage]]()
36
-
37
- self._room : RoomClient | None = None
38
- self._toolkits = toolkits
39
-
40
- if rules == None:
41
- rules = []
42
-
43
- self._rules = rules
44
- self._is_typing = dict[str,asyncio.Task]()
45
- self._auto_greet_prompt = auto_greet_prompt
46
- self._auto_greet_message = auto_greet_message
47
-
48
- if empty_state_title == None:
49
- empty_state_title = "How can I help you?"
50
- self._empty_state_title = empty_state_title
51
-
52
-
53
- async def greet(self, *, chat_context: AgentChatContext, messages: Chan[RoomMessage], participant: RemoteParticipant):
54
-
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 }))
57
-
58
- if self._auto_greet_message != None:
59
- 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
-
63
-
64
- async def finalize_toolkits(self, toolkits) -> list[Toolkit]:
65
-
66
- toaster = None
67
-
68
- for toolkit in toolkits:
69
-
70
- if toolkit.name == "meshagent.ui":
71
-
72
- for tool in toolkit.tools:
73
-
74
- if tool.name == "show_toast":
75
-
76
- toaster = tool
77
-
78
- if toaster != None:
79
-
80
- def multi_tool(toolkit: Toolkit):
81
- if toaster in toolkit.tools:
82
- return toolkit
83
-
84
- return MultiToolkit(required=[ toaster ], base_toolkit=toolkit )
85
-
86
- toolkits = list(map(multi_tool, toolkits))
87
-
88
- return toolkits
89
-
90
- async def _spawn_thread(self, participant_id: str, messages: Chan[RoomMessage]):
91
-
92
-
93
- chat_context = await self.init_chat_context()
94
-
95
-
96
- chat_context.append_rules(
97
- rules=[
98
- *self._rules,
99
- "think step by step",
100
- ]
101
- )
102
-
103
- opened = False
104
- chat_with_participant = None
105
-
106
- for participant in self._room.messaging.get_participants():
107
- if participant.id == participant_id:
108
- chat_with_participant = participant
109
- break
110
-
111
- if chat_with_participant == None:
112
- raise RoomException(f"caller did not have messaging turned on")
113
-
114
- messaging = self._room.messaging
115
-
116
- current_file = None
117
-
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:
140
-
141
-
142
-
143
- received = await messages.recv()
144
-
145
-
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")
150
-
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
-
157
-
158
-
159
- if installed == False:
160
- installed = True
161
- try:
162
- await self.install_requirements(participant_id=participant_id)
163
- except Exception as e:
164
- self.room.developer.log_nowait("error", { "text" : f"unable to install requirements: {e}" })
165
-
166
- error = "I was unable to install the tools I require to operate in the room, I may not function properly."
167
- chat_context.append_user_message(message=error)
168
- await self._room.messaging.send_message(
169
- to=chat_with_participant,
170
- type="chat",
171
- message={
172
- "text": error
173
- }
174
- )
175
-
176
- if received.type == "opened":
177
-
178
-
179
- if opened == False:
180
-
181
- opened = True
182
-
183
- await self.greet(chat_context=chat_context, participant=chat_with_participant, messages=messages)
184
-
185
- if received.type == "chat":
186
-
187
-
188
- await self._room.messaging.send_message(to=chat_with_participant, type="thinking", message={"thinking":True})
189
-
190
- if chat_with_participant.id == received.from_participant_id:
191
- 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"] } } })
192
-
193
- text = received.message["text"]
194
- attachments = received.message.get("attachments", [])
195
-
196
- for attachment in attachments:
197
-
198
- chat_context.append_assistant_message(message=f"the user attached a file '{attachment["filename"]}' with the content: '{attachment["content"]}'")
199
-
200
-
201
- chat_context.append_user_message(message=text)
202
-
203
-
204
- # if user is typing, wait for typing to stop
205
- while True:
206
-
207
- if chat_with_participant.id not in self._is_typing:
208
- break
209
-
210
- await asyncio.sleep(.5)
211
-
212
- if messages.empty() == True:
213
- break
214
-
215
-
216
- try:
217
- while True:
218
-
219
- toolkits = [
220
- *self._toolkits,
221
- *await self.get_required_tools(participant_id=chat_with_participant.id)
222
- ]
223
-
224
- toolkits = await self.finalize_toolkits(toolkits)
225
-
226
- response = await self._llm_adapter.next(
227
- context=chat_context,
228
- room=self._room,
229
- toolkits=toolkits,
230
- tool_adapter=self._tool_adapter,
231
- output_schema=step_schema,
232
- )
233
-
234
- text = response["text"]
235
-
236
-
237
- if response["finished"] or len(toolkits) == 0:
238
- await self._room.messaging.send_message(
239
- to=chat_with_participant,
240
- type="chat",
241
- message={
242
- "text": text
243
- }
244
- )
245
- break
246
- else:
247
- await self._room.messaging.send_message(
248
- to=chat_with_participant,
249
- type="status",
250
- message={
251
- "text": text
252
- }
253
- )
254
- chat_context.append_user_message(message="proceed to the next step if you are ready")
255
-
256
- finally:
257
-
258
- await self._room.messaging.send_message(to=chat_with_participant, type="thinking", message={"thinking":False})
259
-
260
-
261
-
262
- def _get_message_channel(self, participant_id: str) -> Chan[RoomMessage]:
263
- if participant_id not in self._message_channels:
264
- chan = Chan[RoomMessage]()
265
- self._message_channels[participant_id] = chan
266
-
267
- def thread_done(task: asyncio.Task):
268
-
269
- self._message_channels.pop(participant_id)
270
- try:
271
- task.result()
272
- logger.info("ending chat thread")
273
- except Exception as e:
274
- logger.error("chat thread error", exc_info=e)
275
-
276
- task = asyncio.create_task(self._spawn_thread(participant_id=participant_id, messages=chan))
277
- task.add_done_callback(thread_done)
278
-
279
- chan = self._message_channels[participant_id]
280
-
281
- return chan
282
-
283
- async def start(self, *, room):
284
-
285
- await super().start(room=room)
286
-
287
-
288
- await self.room.local_participant.set_attribute("empty_state_title", self._empty_state_title)
289
-
290
- def on_message(message: RoomMessage):
291
- messages = self._get_message_channel(participant_id=message.from_participant_id)
292
- if message.type == "chat" or message.type == "opened":
293
- messages.send_nowait(message)
294
-
295
- elif message.type == "typing":
296
-
297
-
298
- def callback(task: asyncio.Task):
299
- try:
300
- task.result()
301
- except:
302
- pass
303
-
304
- async def remove_timeout(id: str):
305
- await asyncio.sleep(1)
306
- self._is_typing.pop(id)
307
-
308
- if message.from_participant_id in self._is_typing:
309
- self._is_typing[message.from_participant_id].cancel()
310
-
311
- timeout = asyncio.create_task(remove_timeout(id=message.from_participant_id))
312
- timeout.add_done_callback(callback)
313
-
314
- self._is_typing[message.from_participant_id] = timeout
315
-
316
- room.messaging.on("message", on_message)
317
-
318
- if self._auto_greet_prompt != None or self._auto_greet_message != None:
319
- def on_participant_added(participant:RemoteParticipant):
320
-
321
- # will spawn the initial thread
322
- self._get_message_channel(participant_id=participant.id)
323
-
324
-
325
- room.messaging.on("participant_added", on_participant_added)
326
-
327
- await room.messaging.enable()
328
-
@@ -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 +0,0 @@
1
- __version__ = "0.0.3"