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.
- meshagent/agents/__init__.py +27 -2
- meshagent/agents/adapter.py +18 -9
- meshagent/agents/agent.py +317 -214
- meshagent/agents/chat.py +390 -267
- meshagent/agents/context.py +58 -30
- meshagent/agents/development.py +11 -13
- meshagent/agents/hosting.py +109 -46
- meshagent/agents/indexer.py +241 -224
- meshagent/agents/listener.py +55 -52
- meshagent/agents/mail.py +145 -109
- meshagent/agents/planning.py +294 -199
- meshagent/agents/prompt.py +14 -12
- meshagent/agents/pydantic.py +98 -61
- meshagent/agents/schemas/__init__.py +11 -0
- meshagent/agents/schemas/document.py +32 -21
- meshagent/agents/schemas/gallery.py +23 -14
- meshagent/agents/schemas/presentation.py +33 -17
- meshagent/agents/schemas/schema.py +99 -45
- meshagent/agents/schemas/super_editor_document.py +52 -46
- meshagent/agents/single_shot_writer.py +37 -31
- meshagent/agents/thread_schema.py +74 -32
- meshagent/agents/utils.py +20 -12
- meshagent/agents/version.py +1 -1
- meshagent/agents/worker.py +48 -28
- meshagent/agents/writer.py +36 -23
- meshagent_agents-0.0.38.dist-info/METADATA +64 -0
- meshagent_agents-0.0.38.dist-info/RECORD +30 -0
- meshagent_agents-0.0.36.dist-info/METADATA +0 -36
- meshagent_agents-0.0.36.dist-info/RECORD +0 -30
- {meshagent_agents-0.0.36.dist-info → meshagent_agents-0.0.38.dist-info}/WHEEL +0 -0
- {meshagent_agents-0.0.36.dist-info → meshagent_agents-0.0.38.dist-info}/licenses/LICENSE +0 -0
- {meshagent_agents-0.0.36.dist-info → meshagent_agents-0.0.38.dist-info}/top_level.txt +0 -0
meshagent/agents/listener.py
CHANGED
|
@@ -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.
|
|
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__(
|
|
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
|
-
|
|
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"
|
|
35
|
-
"required"
|
|
36
|
-
"additionalProperties"
|
|
37
|
-
"properties"
|
|
38
|
-
"path"
|
|
39
|
-
"type"
|
|
40
|
-
"description"
|
|
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"
|
|
46
|
-
"additionalProperties"
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
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
|
-
|
|
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:
|
|
58
|
+
else: # simple, non-MIME message
|
|
58
59
|
body = message.get_content()
|
|
59
60
|
|
|
60
61
|
id = message.get("Message-ID")
|
|
61
|
-
if id
|
|
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"
|
|
69
|
-
"in_reply_to"
|
|
70
|
-
"reply_to"
|
|
71
|
-
"references"
|
|
72
|
-
"from"
|
|
73
|
-
"to"
|
|
74
|
-
"subject"
|
|
75
|
-
"body"
|
|
76
|
-
"attachments"
|
|
77
|
-
"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 =
|
|
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
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|
122
|
+
data = part.get_content()
|
|
115
123
|
|
|
116
124
|
# make sure we write binary data
|
|
117
|
-
bin_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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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__(
|
|
191
|
-
|
|
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
|
|
206
|
+
if password is None:
|
|
195
207
|
password = os.getenv("SMTP_PASSWORD")
|
|
196
208
|
|
|
197
|
-
if port
|
|
209
|
+
if port is None:
|
|
198
210
|
port = int(os.getenv("SMTP_PORT", "587"))
|
|
199
211
|
|
|
200
|
-
if hostname
|
|
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
|
-
|
|
211
|
-
|
|
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
|
|
218
|
-
toolkits
|
|
219
|
-
rules
|
|
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
|
|
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__(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
285
|
+
msg = create_reply_email_message(
|
|
286
|
+
message=message, from_address=self._email_address, body=body
|
|
287
|
+
)
|
|
265
288
|
|
|
266
|
-
|
|
267
|
-
|
|
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
|
|
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
|
|
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(
|
|
314
|
+
raise TypeError("number must be an integer")
|
|
287
315
|
if number < 0:
|
|
288
|
-
raise ValueError(
|
|
316
|
+
raise ValueError("number must be non-negative")
|
|
289
317
|
|
|
290
|
-
alphabet =
|
|
318
|
+
alphabet = "0123456789abcdefghijklmnopqrstuvwxyz"
|
|
291
319
|
if number == 0:
|
|
292
|
-
return
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
|
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(
|
|
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}"
|