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.
- {meshagent_agents-0.0.3/meshagent_agents.egg-info → meshagent_agents-0.0.5}/PKG-INFO +4 -4
- {meshagent_agents-0.0.3 → meshagent_agents-0.0.5}/meshagent/agents/__init__.py +1 -0
- {meshagent_agents-0.0.3 → meshagent_agents-0.0.5}/meshagent/agents/adapter.py +8 -3
- {meshagent_agents-0.0.3 → meshagent_agents-0.0.5}/meshagent/agents/agent.py +15 -7
- meshagent_agents-0.0.5/meshagent/agents/chat.py +448 -0
- {meshagent_agents-0.0.3 → meshagent_agents-0.0.5}/meshagent/agents/context.py +16 -0
- {meshagent_agents-0.0.3 → meshagent_agents-0.0.5}/meshagent/agents/indexer.py +0 -3
- {meshagent_agents-0.0.3 → meshagent_agents-0.0.5}/meshagent/agents/planning.py +2 -2
- {meshagent_agents-0.0.3 → meshagent_agents-0.0.5}/meshagent/agents/prompt.py +9 -3
- {meshagent_agents-0.0.3 → meshagent_agents-0.0.5}/meshagent/agents/pydantic.py +32 -2
- {meshagent_agents-0.0.3 → meshagent_agents-0.0.5}/meshagent/agents/single_shot_writer.py +1 -1
- meshagent_agents-0.0.5/meshagent/agents/thread_schema.py +59 -0
- meshagent_agents-0.0.5/meshagent/agents/version.py +1 -0
- {meshagent_agents-0.0.3 → meshagent_agents-0.0.5/meshagent_agents.egg-info}/PKG-INFO +4 -4
- {meshagent_agents-0.0.3 → meshagent_agents-0.0.5}/meshagent_agents.egg-info/SOURCES.txt +1 -1
- {meshagent_agents-0.0.3 → meshagent_agents-0.0.5}/meshagent_agents.egg-info/requires.txt +3 -3
- {meshagent_agents-0.0.3 → meshagent_agents-0.0.5}/setup.py +3 -3
- meshagent_agents-0.0.3/meshagent/agents/chat.py +0 -328
- meshagent_agents-0.0.3/meshagent/agents/schema.py +0 -50
- meshagent_agents-0.0.3/meshagent/agents/version.py +0 -1
- {meshagent_agents-0.0.3 → meshagent_agents-0.0.5}/LICENSE +0 -0
- {meshagent_agents-0.0.3 → meshagent_agents-0.0.5}/MANIFEST.in +0 -0
- {meshagent_agents-0.0.3 → meshagent_agents-0.0.5}/README.md +0 -0
- {meshagent_agents-0.0.3 → meshagent_agents-0.0.5}/meshagent/agents/development.py +0 -0
- {meshagent_agents-0.0.3 → meshagent_agents-0.0.5}/meshagent/agents/hosting.py +0 -0
- {meshagent_agents-0.0.3 → meshagent_agents-0.0.5}/meshagent/agents/listener.py +0 -0
- {meshagent_agents-0.0.3 → meshagent_agents-0.0.5}/meshagent/agents/worker.py +0 -0
- {meshagent_agents-0.0.3 → meshagent_agents-0.0.5}/meshagent/agents/writer.py +0 -0
- {meshagent_agents-0.0.3 → meshagent_agents-0.0.5}/meshagent_agents.egg-info/dependency_links.txt +0 -0
- {meshagent_agents-0.0.3 → meshagent_agents-0.0.5}/meshagent_agents.egg-info/top_level.txt +0 -0
- {meshagent_agents-0.0.3 → meshagent_agents-0.0.5}/pyproject.toml +0 -0
- {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
|
+
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.
|
|
17
|
-
Requires-Dist: meshagent-tools>=0.0.
|
|
18
|
-
Requires-Dist: meshagent-openai>=0.0.
|
|
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,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
|
|
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 .
|
|
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.
|
|
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 .
|
|
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 .
|
|
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
|
|
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 .
|
|
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
|
+
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.
|
|
17
|
-
Requires-Dist: meshagent-tools>=0.0.
|
|
18
|
-
Requires-Dist: meshagent-openai>=0.0.
|
|
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.
|
|
5
|
-
meshagent-tools>=0.0.
|
|
6
|
-
meshagent-openai>=0.0.
|
|
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.
|
|
32
|
-
"meshagent-tools>=0.0.
|
|
33
|
-
"meshagent-openai>=0.0.
|
|
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"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{meshagent_agents-0.0.3 → meshagent_agents-0.0.5}/meshagent_agents.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|