meshagent-agents 0.0.36__py3-none-any.whl → 0.0.38__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of meshagent-agents might be problematic. Click here for more details.

@@ -1,22 +1,19 @@
1
-
2
1
  import logging
3
2
  import asyncio
4
3
  from typing import Optional
5
4
 
6
5
  from meshagent.agents import TaskRunner
7
- from meshagent.api.schema_document import Element,Text
6
+ from meshagent.api.schema_document import Element, Text
8
7
  from meshagent.api.room_server_client import RoomClient, MeshDocument
9
- from meshagent.api.websocket_protocol import WebSocketClientProtocol
10
-
11
- from typing import Optional
8
+ from meshagent.agents.agent import AgentCallContext
12
9
 
13
10
  logger = logging.getLogger(__name__)
14
11
 
15
12
 
16
- from meshagent.agents.agent import TaskRunner, AgentChatContext, AgentCallContext
17
-
18
13
  class ListenerContext:
19
- def __init__(self, document: MeshDocument, room: RoomClient, call_context: AgentCallContext):
14
+ def __init__(
15
+ self, document: MeshDocument, room: RoomClient, call_context: AgentCallContext
16
+ ):
20
17
  self.document = document
21
18
  self.call_context = call_context
22
19
  self.room = room
@@ -24,44 +21,55 @@ class ListenerContext:
24
21
 
25
22
  # Notifies of new nodes or changed nodes in a document, the document must already exist
26
23
  class Listener(TaskRunner):
27
-
28
- def __init__(self, *, name: str, wait_for_synchronize: bool = True, title: Optional[str] = None, description: Optional[str] = None):
24
+ def __init__(
25
+ self,
26
+ *,
27
+ name: str,
28
+ wait_for_synchronize: bool = True,
29
+ title: Optional[str] = None,
30
+ description: Optional[str] = None,
31
+ ):
29
32
  super().__init__(
30
33
  name=name,
31
34
  description=description,
32
35
  title=title,
33
36
  input_schema={
34
- "type" : "object",
35
- "required" : ["path"],
36
- "additionalProperties" : False,
37
- "properties" : {
38
- "path" : {
39
- "type" : "string",
40
- "description" : "the path of the document to listen to"
37
+ "type": "object",
38
+ "required": ["path"],
39
+ "additionalProperties": False,
40
+ "properties": {
41
+ "path": {
42
+ "type": "string",
43
+ "description": "the path of the document to listen to",
41
44
  }
42
- }
45
+ },
43
46
  },
44
47
  output_schema={
45
- "type" : "object",
46
- "additionalProperties" : False,
47
- "required" : [],
48
- "properties" : {
49
- }
50
- }
48
+ "type": "object",
49
+ "additionalProperties": False,
50
+ "required": [],
51
+ "properties": {},
52
+ },
51
53
  )
52
54
  self.wait_for_synchronize = wait_for_synchronize
53
-
55
+
54
56
  async def on_listening_started(self, listener_context: ListenerContext):
55
57
  pass
56
58
 
57
- async def on_element_inserted(self, listener_context: ListenerContext, element: Element) -> bool:
59
+ async def on_element_inserted(
60
+ self, listener_context: ListenerContext, element: Element
61
+ ) -> bool:
58
62
  return False
59
63
 
60
- async def on_attribute_changed(self, listener_context: ListenerContext, element: Element, attribute: Optional[str]) -> bool:
64
+ async def on_attribute_changed(
65
+ self,
66
+ listener_context: ListenerContext,
67
+ element: Element,
68
+ attribute: Optional[str],
69
+ ) -> bool:
61
70
  return False
62
-
63
- async def ask(self, *, context: AgentCallContext, arguments: dict):
64
71
 
72
+ async def ask(self, *, context: AgentCallContext, arguments: dict):
65
73
  output_path = arguments["path"]
66
74
  room = context.room
67
75
  logger.info("Visitor connecting to %s", output_path)
@@ -75,58 +83,56 @@ class Listener(TaskRunner):
75
83
  logger.info("Visitor connected to %s", output_path)
76
84
 
77
85
  change_queue = list[Element | Text]()
78
-
79
- def append_children(node:Element):
86
+
87
+ def append_children(node: Element):
80
88
  for child in node.get_children():
81
89
  if child not in change_queue:
82
90
  change_queue.append([child])
83
91
  if isinstance(child, Element):
84
92
  append_children(child)
85
-
86
- if self.wait_for_synchronize == False:
93
+
94
+ if not self.wait_for_synchronize:
87
95
  change_queue.append([doc.root])
88
96
  append_children(doc.root)
89
97
  else:
90
98
  await doc.synchronized
91
99
 
92
-
93
100
  await self.on_listening_started(listener_context=listener_context)
94
-
101
+
95
102
  wait_for_changes = asyncio.Future()
96
-
103
+
97
104
  @doc.on("inserted")
98
105
  def on_inserted(e: Element):
99
106
  logger.info("element inserted %s", e.tag_name)
100
107
  if e not in change_queue:
101
108
  change_queue.append([e])
102
109
  append_children(e)
103
-
104
- if wait_for_changes.done() == False:
105
- wait_for_changes.set_result(True)
106
110
 
111
+ if not wait_for_changes.done():
112
+ wait_for_changes.set_result(True)
107
113
 
108
114
  @doc.on("updated")
109
115
  def on_updated(e: Element, attribute: str):
110
116
  logger.info("element updated %s", e.tag_name)
111
- if e not in change_queue:
117
+ if e not in change_queue:
112
118
  change_queue.append([e, attribute])
113
119
  #
114
120
  # append_children(e)
115
-
116
- if wait_for_changes.done() == False:
117
- wait_for_changes.set_result(True)
118
121
 
122
+ if not wait_for_changes.done():
123
+ wait_for_changes.set_result(True)
119
124
 
120
125
  waiting_for_end = True
121
126
  while waiting_for_end:
122
127
  await wait_for_changes
123
-
124
- while len(change_queue) > 0:
125
128
 
129
+ while len(change_queue) > 0:
126
130
  change = change_queue.pop(0)
127
131
  content = change[0]
128
132
  if len(change) > 1:
129
- done = await self.on_attribute_changed(listener_context, content, change[1])
133
+ done = await self.on_attribute_changed(
134
+ listener_context, content, change[1]
135
+ )
130
136
  if done:
131
137
  waiting_for_end = False
132
138
 
@@ -134,22 +140,19 @@ class Listener(TaskRunner):
134
140
  done = await self.on_element_inserted(listener_context, content)
135
141
  if done:
136
142
  waiting_for_end = False
137
-
143
+
138
144
  if content in change_queue:
139
145
  change_queue.remove(content)
140
146
 
141
-
142
-
143
147
  wait_for_changes = asyncio.Future()
144
148
  except Exception as e:
145
- logger.error("Failed to visit", exc_info=e)
149
+ logger.error("Failed to visit", exc_info=e)
146
150
  raise
147
151
 
148
152
  finally:
149
-
150
153
  logger.info("vistor done")
151
154
 
152
155
  await asyncio.sleep(5)
153
156
  await room.sync.close(output_path)
154
-
157
+
155
158
  return {}
meshagent/agents/mail.py CHANGED
@@ -1,5 +1,4 @@
1
1
  from meshagent.agents.worker import Worker
2
- from meshagent.tools import Tool, Toolkit
3
2
  from meshagent.api.room_server_client import TextDataType
4
3
  from email import message_from_bytes
5
4
  from email.message import EmailMessage
@@ -27,15 +26,15 @@ logger = logging.getLogger("mail")
27
26
  type MessageRole = Literal["user", "agent"]
28
27
 
29
28
 
30
-
31
- def create_reply_email_message(*, message: dict, from_address: str, body: str) -> EmailMessage:
32
-
33
- subject : str = message.get("subject")
29
+ def create_reply_email_message(
30
+ *, message: dict, from_address: str, body: str
31
+ ) -> EmailMessage:
32
+ subject: str = message.get("subject")
34
33
 
35
34
  if not subject.startswith("RE:"):
36
35
  subject = "RE: " + subject
37
36
 
38
- _, addr = email.utils.parseaddr(from_address)
37
+ _, addr = email.utils.parseaddr(from_address)
39
38
  domain = addr.split("@")[-1].lower()
40
39
  id = f"<{uuid.uuid4()}@{domain}>"
41
40
 
@@ -49,73 +48,86 @@ def create_reply_email_message(*, message: dict, from_address: str, body: str) -
49
48
 
50
49
  return msg
51
50
 
52
- def message_to_json(*, message: EmailMessage, role: MessageRole):
53
51
 
54
- body_part = message.get_body(('plain', 'html')) # returns the “best” part :contentReference[oaicite:0]{index=0}
52
+ def message_to_json(*, message: EmailMessage, role: MessageRole):
53
+ body_part = message.get_body(
54
+ ("plain", "html")
55
+ ) # returns the “best” part :contentReference[oaicite:0]{index=0}
55
56
  if body_part:
56
57
  body = body_part.get_content()
57
- else: # simple, non-MIME message
58
+ else: # simple, non-MIME message
58
59
  body = message.get_content()
59
60
 
60
61
  id = message.get("Message-ID")
61
- if id == None:
62
+ if id is None:
62
63
  mfrom = message.get("From")
63
- _, addr = email.utils.parseaddr(mfrom)
64
+ _, addr = email.utils.parseaddr(mfrom)
64
65
  domain = addr.split("@")[-1].lower()
65
66
  id = f"{uuid.uuid4()}@{domain}"
66
67
 
67
68
  return {
68
- "id" : id,
69
- "in_reply_to" : message.get("In-Reply-To"),
70
- "reply_to" : message.get("Reply-To", message.get("From")),
71
- "references" : message.get("References"),
72
- "from" : message.get("From"),
73
- "to" : message.get_all("To"),
74
- "subject" : message.get("Subject"),
75
- "body" : body,
76
- "attachments" : [],
77
- "role" : role
69
+ "id": id,
70
+ "in_reply_to": message.get("In-Reply-To"),
71
+ "reply_to": message.get("Reply-To", message.get("From")),
72
+ "references": message.get("References"),
73
+ "from": message.get("From"),
74
+ "to": message.get_all("To"),
75
+ "subject": message.get("Subject"),
76
+ "body": body,
77
+ "attachments": [],
78
+ "role": role,
78
79
  }
79
80
 
81
+
80
82
  async def load_message(*, room: RoomClient, message_id: str) -> dict | None:
83
+ messages = await room.database.search(table="emails", where={"id": message_id})
81
84
 
82
- messages = await room.database.search(
83
- table="emails",
84
- where={
85
- "id" : message_id
86
- })
87
-
88
85
  if len(messages) == 0:
89
86
  return None
90
-
87
+
91
88
  return json.loads(messages[0]["json"])
92
89
 
93
- async def save_email_message(*, room: RoomClient, content: bytes, role: MessageRole) -> dict:
94
90
 
91
+ async def save_email_message(
92
+ *, room: RoomClient, content: bytes, role: MessageRole
93
+ ) -> dict:
95
94
  message = message_from_bytes(content, policy=default)
96
95
 
97
96
  now = datetime.now(timezone.utc)
98
-
99
- folder_path = now.strftime('%Y/%m/%d') +"/" + now.strftime('%H/%M/%S') + '/' + secrets.token_hex(3)
97
+
98
+ folder_path = (
99
+ now.strftime("%Y/%m/%d")
100
+ + "/"
101
+ + now.strftime("%H/%M/%S")
102
+ + "/"
103
+ + secrets.token_hex(3)
104
+ )
100
105
 
101
106
  queued_message = message_to_json(message=message, role=role)
102
107
  message_id = queued_message["id"]
103
-
104
108
 
105
109
  queued_message["role"] = role
106
-
110
+
107
111
  queued_message["path"] = f".emails/{message_id}/message.json"
108
-
109
- for part in message.iter_attachments(): # ↔ only the “real” attachments :contentReference[oaicite:0]{index=0}
110
- fname = part.get_filename() or "attachment.bin" # RFC 2183 filename, if any :contentReference[oaicite:1]{index=1}
111
- ctype = part.get_content_type() # e.g. image/png, application/pdf
112
+
113
+ for part in (
114
+ message.iter_attachments()
115
+ ): # only the “real” attachments :contentReference[oaicite:0]{index=0}
116
+ fname = (
117
+ part.get_filename() or "attachment.bin"
118
+ ) # RFC 2183 filename, if any :contentReference[oaicite:1]{index=1}
119
+
112
120
  # get_content() auto-decodes transfer-encodings; returns
113
121
  # *str* for text/*, *bytes* for everything else :contentReference[oaicite:2]{index=2}
114
- data = part.get_content()
122
+ data = part.get_content()
115
123
 
116
124
  # make sure we write binary data
117
- bin_data = data.encode(part.get_content_charset('utf-8')) if isinstance(data, str) else data
118
-
125
+ bin_data = (
126
+ data.encode(part.get_content_charset("utf-8"))
127
+ if isinstance(data, str)
128
+ else data
129
+ )
130
+
119
131
  path = f".emails/{folder_path}/attachments/{fname}"
120
132
  handle = await room.storage.open(path=path)
121
133
  try:
@@ -125,7 +137,7 @@ async def save_email_message(*, room: RoomClient, content: bytes, role: MessageR
125
137
  await room.storage.close(handle=handle)
126
138
 
127
139
  queued_message["attachments"].append(path)
128
-
140
+
129
141
  logger.info(f"received mail, {queued_message}")
130
142
 
131
143
  # write email
@@ -141,7 +153,9 @@ async def save_email_message(*, room: RoomClient, content: bytes, role: MessageR
141
153
  handle = await room.storage.open(path=path)
142
154
  try:
143
155
  logger.info(f"writing source message.json to {path}")
144
- await room.storage.write(handle=handle, data=json.dumps(queued_message, indent=4).encode("utf-8"))
156
+ await room.storage.write(
157
+ handle=handle, data=json.dumps(queued_message, indent=4).encode("utf-8")
158
+ )
145
159
  finally:
146
160
  await room.storage.close(handle=handle)
147
161
 
@@ -149,160 +163,176 @@ async def save_email_message(*, room: RoomClient, content: bytes, role: MessageR
149
163
  tables = await room.database.list_tables()
150
164
 
151
165
  if "emails" not in tables:
152
-
153
166
  await room.database.create_table_with_schema(
154
167
  name="emails",
155
- schema={
156
- "id" : TextDataType(),
157
- "json" : TextDataType()
158
- },
159
- mode="create_if_not_exists"
168
+ schema={"id": TextDataType(), "json": TextDataType()},
169
+ mode="create_if_not_exists",
160
170
  )
161
171
 
162
172
  await room.database.create_scalar_index(table="emails", column="id")
163
173
 
164
-
165
- await room.database.insert(table="emails", records=[ { "id" : message_id, "json" : json.dumps(queued_message) } ])
174
+ await room.database.insert(
175
+ table="emails", records=[{"id": message_id, "json": json.dumps(queued_message)}]
176
+ )
166
177
 
167
178
  return queued_message
168
179
 
169
180
 
170
181
  async def load_thread(*, room: RoomClient, message: dict, thread: list[dict]):
171
-
172
182
  in_reply_to = message.get("in_reply_to", None)
173
- if in_reply_to != None:
174
-
183
+ if in_reply_to is not None:
175
184
  source = await load_message(room=room, message_id=in_reply_to)
176
185
 
177
- if source != None:
178
-
186
+ if source is not None:
179
187
  thread.insert(0, source)
180
188
 
181
189
  await load_thread(room=room, message=source, thread=thread)
182
190
 
183
191
  else:
184
-
185
192
  logger.warning(f"message not found {in_reply_to}")
186
193
 
187
194
 
188
-
189
195
  class SmtpConfiguration:
190
- def __init__(self, username: Optional[str] = None, password: Optional[str] = None, port: Optional[int] = None, hostname: Optional[str] = None):
191
- if username == None:
196
+ def __init__(
197
+ self,
198
+ username: Optional[str] = None,
199
+ password: Optional[str] = None,
200
+ port: Optional[int] = None,
201
+ hostname: Optional[str] = None,
202
+ ):
203
+ if username is None:
192
204
  username = os.getenv("SMTP_USERNAME")
193
205
 
194
- if password == None:
206
+ if password is None:
195
207
  password = os.getenv("SMTP_PASSWORD")
196
208
 
197
- if port == None:
209
+ if port is None:
198
210
  port = int(os.getenv("SMTP_PORT", "587"))
199
211
 
200
- if hostname == None:
212
+ if hostname is None:
201
213
  hostname = os.getenv("SMTP_HOSTNAME")
202
-
214
+
203
215
  self.username = username
204
216
  self.password = password
205
217
  self.port = port
206
218
  self.hostname = hostname
207
219
 
208
- class MailWorker(Worker):
209
220
 
210
- def __init__(self, *,
211
- queue: str = "email",
221
+ class MailWorker(Worker):
222
+ def __init__(
223
+ self,
224
+ *,
225
+ queue: str = "email",
212
226
  name,
213
227
  title=None,
214
228
  description=None,
215
229
  requires=None,
216
230
  llm_adapter,
217
- tool_adapter = None,
218
- toolkits = None,
219
- rules = None,
231
+ tool_adapter=None,
232
+ toolkits=None,
233
+ rules=None,
220
234
  domain: str = os.getenv("MESHAGENT_MAIL_DOMAIN", "mail.meshagent.com"),
221
- smtp: Optional[SmtpConfiguration] = None):
222
-
223
- if smtp == None:
235
+ smtp: Optional[SmtpConfiguration] = None,
236
+ ):
237
+ if smtp is None:
224
238
  smtp = SmtpConfiguration()
225
239
 
226
240
  self._domain = domain
227
241
  self._smtp = smtp
228
- super().__init__(queue=queue, name=name, title=title, description=description, requires=requires, llm_adapter=llm_adapter, tool_adapter=tool_adapter, toolkits=toolkits, rules=rules)
242
+ super().__init__(
243
+ queue=queue,
244
+ name=name,
245
+ title=title,
246
+ description=description,
247
+ requires=requires,
248
+ llm_adapter=llm_adapter,
249
+ tool_adapter=tool_adapter,
250
+ toolkits=toolkits,
251
+ rules=rules,
252
+ )
229
253
 
230
254
  async def start(self, *, room):
231
-
232
255
  await super().start(room=room)
233
256
 
234
257
  token = ParticipantToken.from_jwt(room.protocol.token, validate=False)
235
- self._email_address = room_address(project_id=token.project_id, room_name=room.room_name, domain=self._domain)
258
+ self._email_address = room_address(
259
+ project_id=token.project_id, room_name=room.room_name, domain=self._domain
260
+ )
236
261
 
237
262
  async def append_message_context(self, *, room, message, chat_context):
238
-
239
- thread = [
240
- message
241
- ]
263
+ thread = [message]
242
264
 
243
265
  await load_thread(room=room, message=message, thread=thread)
244
266
 
245
267
  for msg in thread:
246
-
247
268
  if msg["role"] == "agent":
248
-
249
269
  chat_context.append_assistant_message(json.dumps(msg))
250
270
 
251
271
  else:
252
-
253
272
  chat_context.append_user_message(json.dumps(msg))
254
-
255
273
 
256
274
  # TODO: load previous messages
257
- return await super().append_message_context(room=room, message=message, chat_context=chat_context)
258
-
275
+ return await super().append_message_context(
276
+ room=room, message=message, chat_context=chat_context
277
+ )
278
+
259
279
  async def process_message(self, *, chat_context, room, message, toolkits):
260
-
261
280
  logger.info(f"processing message {message}")
262
- body = await super().process_message(chat_context=chat_context, room=room, message=message, toolkits=toolkits)
281
+ body = await super().process_message(
282
+ chat_context=chat_context, room=room, message=message, toolkits=toolkits
283
+ )
263
284
 
264
- msg = create_reply_email_message(message=message, from_address=self._email_address, body=body)
285
+ msg = create_reply_email_message(
286
+ message=message, from_address=self._email_address, body=body
287
+ )
265
288
 
266
-
267
- reply_msg_dict = await save_email_message(room=room, content=msg.as_bytes(), role="agent")
289
+ reply_msg_dict = await save_email_message(
290
+ room=room, content=msg.as_bytes(), role="agent"
291
+ )
268
292
 
269
293
  logger.info(f"replying with message {reply_msg_dict}")
270
-
271
-
294
+
272
295
  username = self._smtp.username
273
- if username == None:
296
+ if username is None:
274
297
  username = self.room.local_participant.get_attribute("name")
275
-
298
+
276
299
  password = self._smtp.password
277
- if password == None:
300
+ if password is None:
278
301
  password = self.room.protocol.token
279
-
280
- await aiosmtplib.send(msg, hostname=self._smtp.hostname, port=self._smtp.port, username=username, password=password)
281
302
 
303
+ await aiosmtplib.send(
304
+ msg,
305
+ hostname=self._smtp.hostname,
306
+ port=self._smtp.port,
307
+ username=username,
308
+ password=password,
309
+ )
282
310
 
283
311
 
284
312
  def base36encode(number: int):
285
313
  if not isinstance(number, int):
286
- raise TypeError('number must be an integer')
314
+ raise TypeError("number must be an integer")
287
315
  if number < 0:
288
- raise ValueError('number must be non-negative')
316
+ raise ValueError("number must be non-negative")
289
317
 
290
- alphabet = '0123456789abcdefghijklmnopqrstuvwxyz'
318
+ alphabet = "0123456789abcdefghijklmnopqrstuvwxyz"
291
319
  if number == 0:
292
- return '0'
293
- base36 = ''
320
+ return "0"
321
+ base36 = ""
294
322
  while number:
295
323
  number, i = divmod(number, 36)
296
324
  base36 = alphabet[i] + base36
297
325
  return base36
298
326
 
327
+
299
328
  def compress_uuid(guid_string: str):
300
- guid_int = int(guid_string.replace('-', ''), 16)
329
+ guid_int = int(guid_string.replace("-", ""), 16)
301
330
  return base36encode(guid_int)
302
331
 
332
+
303
333
  def base36decode(number_str: str) -> int:
304
334
  """Decode a base36-encoded string into an integer."""
305
- alphabet = '0123456789abcdefghijklmnopqrstuvwxyz'
335
+ alphabet = "0123456789abcdefghijklmnopqrstuvwxyz"
306
336
  base = 36
307
337
  number = 0
308
338
  for char in number_str:
@@ -313,6 +343,7 @@ def base36decode(number_str: str) -> int:
313
343
  number = number * base + value
314
344
  return number
315
345
 
346
+
316
347
  def decompress_uuid(compressed_uuid: str) -> str:
317
348
  """
318
349
  Reverse the compressed UUID to its standard 36-character UUID format.
@@ -325,14 +356,19 @@ def decompress_uuid(compressed_uuid: str) -> str:
325
356
  """
326
357
  # Decode the base36 string back to the original integer.
327
358
  guid_int = base36decode(compressed_uuid)
328
-
359
+
329
360
  # Convert the integer into a 32-digit hexadecimal string with leading zeros.
330
- hex_str = f'{guid_int:032x}'
331
-
361
+ hex_str = f"{guid_int:032x}"
362
+
332
363
  # Reinsert dashes to match the standard UUID format: 8-4-4-4-12.
333
- standard_uuid = f'{hex_str[0:8]}-{hex_str[8:12]}-{hex_str[12:16]}-{hex_str[16:20]}-{hex_str[20:32]}'
364
+ standard_uuid = f"{hex_str[0:8]}-{hex_str[8:12]}-{hex_str[12:16]}-{hex_str[16:20]}-{hex_str[20:32]}"
334
365
  return standard_uuid
335
366
 
336
367
 
337
- def room_address(*, project_id: str, room_name: str, domain: str = os.getenv("MESHAGENT_MAIL_DOMAIN", "mail.meshagent.com")):
368
+ def room_address(
369
+ *,
370
+ project_id: str,
371
+ room_name: str,
372
+ domain: str = os.getenv("MESHAGENT_MAIL_DOMAIN", "mail.meshagent.com"),
373
+ ):
338
374
  return f"{compress_uuid(project_id)}+{room_name}@{domain}"