meshagent-api 0.22.2__tar.gz → 0.23.0__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.
- {meshagent_api-0.22.2/meshagent_api.egg-info → meshagent_api-0.23.0}/PKG-INFO +1 -1
- {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/client.py +45 -5
- meshagent_api-0.23.0/meshagent/api/service_template_test.py +184 -0
- {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/specs/service.py +89 -28
- meshagent_api-0.23.0/meshagent/api/version.py +1 -0
- {meshagent_api-0.22.2 → meshagent_api-0.23.0/meshagent_api.egg-info}/PKG-INFO +1 -1
- meshagent_api-0.22.2/meshagent/api/service_template_test.py +0 -181
- meshagent_api-0.22.2/meshagent/api/version.py +0 -1
- {meshagent_api-0.22.2 → meshagent_api-0.23.0}/LICENSE +0 -0
- {meshagent_api-0.22.2 → meshagent_api-0.23.0}/MANIFEST.in +0 -0
- {meshagent_api-0.22.2 → meshagent_api-0.23.0}/README.md +0 -0
- {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/__init__.py +0 -0
- {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/chan.py +0 -0
- {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/crdt.py +0 -0
- {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/entrypoint.js +0 -0
- {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/helpers.py +0 -0
- {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/keys.py +0 -0
- {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/messaging.py +0 -0
- {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/oauth.py +0 -0
- {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/participant.py +0 -0
- {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/participant_token.py +0 -0
- {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/participant_token_test.py +0 -0
- {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/port_forward.py +0 -0
- {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/protocol.py +0 -0
- {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/protocol_test.py +0 -0
- {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/py.typed +0 -0
- {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/reasoning_schema.py +0 -0
- {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/room_server_client.py +0 -0
- {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/runtime.py +0 -0
- {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/runtime_test.py +0 -0
- {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/schema.py +0 -0
- {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/schema_document.py +0 -0
- {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/schema_document_test.py +0 -0
- {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/schema_registry.py +0 -0
- {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/schema_test.py +0 -0
- {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/schema_util.py +0 -0
- {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/services.py +0 -0
- {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/token_test.py +0 -0
- {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/urls.py +0 -0
- {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/webhooks.py +0 -0
- {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/websocket_protocol.py +0 -0
- {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent_api.egg-info/SOURCES.txt +0 -0
- {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent_api.egg-info/dependency_links.txt +0 -0
- {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent_api.egg-info/requires.txt +0 -0
- {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent_api.egg-info/top_level.txt +0 -0
- {meshagent_api-0.22.2 → meshagent_api-0.23.0}/pyproject.toml +0 -0
- {meshagent_api-0.22.2 → meshagent_api-0.23.0}/setup.cfg +0 -0
|
@@ -94,6 +94,19 @@ class ProjectRoomGrant(BaseModel):
|
|
|
94
94
|
permissions: ApiScope
|
|
95
95
|
|
|
96
96
|
|
|
97
|
+
class User(BaseModel):
|
|
98
|
+
id: str
|
|
99
|
+
first_name: Optional[str] = None
|
|
100
|
+
last_name: Optional[str] = None
|
|
101
|
+
email: str
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class UserRoomGrant(BaseModel):
|
|
105
|
+
room: Room
|
|
106
|
+
user: User
|
|
107
|
+
permissions: ApiScope
|
|
108
|
+
|
|
109
|
+
|
|
97
110
|
class ProjectRoomGrantCount(BaseModel):
|
|
98
111
|
room: Room
|
|
99
112
|
count: int
|
|
@@ -197,7 +210,7 @@ def _parse_secret(raw: dict) -> SecretLike:
|
|
|
197
210
|
)
|
|
198
211
|
|
|
199
212
|
|
|
200
|
-
ProjectRole = Literal["member", "admin", "developer"]
|
|
213
|
+
ProjectRole = Literal["member", "admin", "developer", "none"]
|
|
201
214
|
|
|
202
215
|
|
|
203
216
|
class _CreateMailboxRequest(BaseModel):
|
|
@@ -1131,6 +1144,33 @@ class Meshagent:
|
|
|
1131
1144
|
async with self._session.delete(url, headers=self._get_headers()) as resp:
|
|
1132
1145
|
await self._raise_for_status(resp)
|
|
1133
1146
|
|
|
1147
|
+
async def render_template(
|
|
1148
|
+
self,
|
|
1149
|
+
*,
|
|
1150
|
+
template: str,
|
|
1151
|
+
values: dict[str, str],
|
|
1152
|
+
) -> ServiceTemplateSpec:
|
|
1153
|
+
"""
|
|
1154
|
+
POST /templates/render
|
|
1155
|
+
Body: full service spec, e.g.
|
|
1156
|
+
{
|
|
1157
|
+
"template" : ""
|
|
1158
|
+
"values": {...}
|
|
1159
|
+
}
|
|
1160
|
+
Returns: {}
|
|
1161
|
+
"""
|
|
1162
|
+
url = f"{self.base_url}/templates/render"
|
|
1163
|
+
async with self._session.post(
|
|
1164
|
+
url,
|
|
1165
|
+
headers=self._get_headers(),
|
|
1166
|
+
json={
|
|
1167
|
+
"template": template,
|
|
1168
|
+
"values": values,
|
|
1169
|
+
},
|
|
1170
|
+
) as resp:
|
|
1171
|
+
await self._raise_for_status(resp)
|
|
1172
|
+
return ServiceTemplateSpec.model_validate_json(await resp.json())
|
|
1173
|
+
|
|
1134
1174
|
async def create_service(
|
|
1135
1175
|
self,
|
|
1136
1176
|
*,
|
|
@@ -1185,7 +1225,7 @@ class Meshagent:
|
|
|
1185
1225
|
return ServiceSpec.model_validate_json(await resp.json())
|
|
1186
1226
|
|
|
1187
1227
|
async def create_service_from_template(
|
|
1188
|
-
self, *, project_id: str, template:
|
|
1228
|
+
self, *, project_id: str, template: str, values: Dict[str, str]
|
|
1189
1229
|
) -> str:
|
|
1190
1230
|
"""
|
|
1191
1231
|
POST /accounts/projects/{project_id}/services
|
|
@@ -1207,7 +1247,7 @@ class Meshagent:
|
|
|
1207
1247
|
url,
|
|
1208
1248
|
headers=self._get_headers(),
|
|
1209
1249
|
json={
|
|
1210
|
-
"template": template
|
|
1250
|
+
"template": template,
|
|
1211
1251
|
"values": values,
|
|
1212
1252
|
},
|
|
1213
1253
|
) as resp:
|
|
@@ -1219,7 +1259,7 @@ class Meshagent:
|
|
|
1219
1259
|
*,
|
|
1220
1260
|
project_id: str,
|
|
1221
1261
|
service_id: str,
|
|
1222
|
-
template:
|
|
1262
|
+
template: str,
|
|
1223
1263
|
values: Dict[str, str],
|
|
1224
1264
|
) -> None:
|
|
1225
1265
|
"""
|
|
@@ -1233,7 +1273,7 @@ class Meshagent:
|
|
|
1233
1273
|
url,
|
|
1234
1274
|
headers=self._get_headers(),
|
|
1235
1275
|
json={
|
|
1236
|
-
"template": template
|
|
1276
|
+
"template": template,
|
|
1237
1277
|
"values": values,
|
|
1238
1278
|
},
|
|
1239
1279
|
) as resp:
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from meshagent.api.specs.service import (
|
|
4
|
+
ServiceTemplateSpec,
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_service_template_spec_renders_jinja_values():
|
|
9
|
+
yml = """
|
|
10
|
+
version: v1
|
|
11
|
+
kind: ServiceTemplate
|
|
12
|
+
metadata:
|
|
13
|
+
name: "{{service_name}}"
|
|
14
|
+
description: "Hello {{user}}"
|
|
15
|
+
repo: "https://example.com/{{service_name}}"
|
|
16
|
+
annotations:
|
|
17
|
+
"greeting": "hi {{user}}"
|
|
18
|
+
agents:
|
|
19
|
+
- name: "agent-{{service_name}}"
|
|
20
|
+
description: "handles {{role}}"
|
|
21
|
+
annotations:
|
|
22
|
+
role: "{{role}}"
|
|
23
|
+
external:
|
|
24
|
+
url: "https://{{host}}/api"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
values = {
|
|
30
|
+
"service_name": "Concierge",
|
|
31
|
+
"user": "Rina",
|
|
32
|
+
"role": "support",
|
|
33
|
+
"host": "meshagent.dev",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
service = ServiceTemplateSpec.from_yaml(yaml=yml, values=values).to_service_spec()
|
|
37
|
+
|
|
38
|
+
assert service.metadata.annotations is not None
|
|
39
|
+
assert service.agents is not None
|
|
40
|
+
assert service.external is not None
|
|
41
|
+
assert service.metadata.name == "Concierge"
|
|
42
|
+
assert service.metadata.description == "Hello Rina"
|
|
43
|
+
assert service.metadata.repo == "https://example.com/Concierge"
|
|
44
|
+
assert service.metadata.annotations["greeting"] == "hi Rina"
|
|
45
|
+
assert service.external.url == "https://meshagent.dev/api"
|
|
46
|
+
assert service.agents[0].name == "agent-Concierge"
|
|
47
|
+
assert service.agents[0].description == "handles support"
|
|
48
|
+
assert service.agents[0].annotations is not None
|
|
49
|
+
assert service.agents[0].annotations["role"] == "support"
|
|
50
|
+
|
|
51
|
+
source = service.metadata.annotations["meshagent.service.template.yaml"]
|
|
52
|
+
values_json = service.metadata.annotations["meshagent.service.template.values"]
|
|
53
|
+
assert json.loads(values_json) == values
|
|
54
|
+
assert "ServiceTemplate" in source
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_service_template_spec_from_yaml():
|
|
58
|
+
yaml_spec = """
|
|
59
|
+
version: v1
|
|
60
|
+
kind: ServiceTemplate
|
|
61
|
+
metadata:
|
|
62
|
+
name: "{{service_name}}"
|
|
63
|
+
description: "Hello {{user}}"
|
|
64
|
+
repo: null
|
|
65
|
+
annotations:
|
|
66
|
+
greeting: "hi {{user}}"
|
|
67
|
+
agents:
|
|
68
|
+
- name: "agent-{{service_name}}"
|
|
69
|
+
description: "handles {{role}}"
|
|
70
|
+
external:
|
|
71
|
+
url: "https://{{host}}/api"
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
values = {
|
|
75
|
+
"service_name": "Concierge",
|
|
76
|
+
"user": "Rina",
|
|
77
|
+
"role": "support",
|
|
78
|
+
"host": "meshagent.dev",
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
service = ServiceTemplateSpec.from_yaml(
|
|
82
|
+
yaml=yaml_spec, values=values
|
|
83
|
+
).to_service_spec()
|
|
84
|
+
|
|
85
|
+
assert service.metadata.annotations is not None
|
|
86
|
+
assert service.agents is not None
|
|
87
|
+
assert service.external is not None
|
|
88
|
+
assert service.metadata.name == "Concierge"
|
|
89
|
+
assert service.metadata.description == "Hello Rina"
|
|
90
|
+
assert service.metadata.annotations["greeting"] == "hi Rina"
|
|
91
|
+
assert service.external.url == "https://meshagent.dev/api"
|
|
92
|
+
assert service.agents[0].name == "agent-Concierge"
|
|
93
|
+
assert service.agents[0].description == "handles support"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_service_template_spec_replaces_email_in_command():
|
|
97
|
+
yaml_spec = """
|
|
98
|
+
version: v1
|
|
99
|
+
kind: ServiceTemplate
|
|
100
|
+
metadata:
|
|
101
|
+
name: PropertyAssistant
|
|
102
|
+
description: Email template
|
|
103
|
+
repo: null
|
|
104
|
+
annotations: null
|
|
105
|
+
|
|
106
|
+
container:
|
|
107
|
+
image: us-central1-docker.pkg.dev/meshagent-public/images/cli:{SERVER_VERSION}-esgz
|
|
108
|
+
command: >-
|
|
109
|
+
meshagent multi service -c "chatbot --require-uuid
|
|
110
|
+
--agent-name=PropertyAssistant --image-generation=gpt-image-1
|
|
111
|
+
--require-storage --require-toolkit=propertyemail
|
|
112
|
+
--require-table-write=propertyinsurance
|
|
113
|
+
--require-table-write=propertyexpenses --mcp --web-search
|
|
114
|
+
-rr='agents/PropertyAssistant/assistantrules.txt'
|
|
115
|
+
--rule='you have access to the email tool, and you can send out emails.';
|
|
116
|
+
mailbot --reply-all --enable-attachments
|
|
117
|
+
--room-rules='/agents/PropertyAssistant/emailrules.txt'
|
|
118
|
+
--rule='never respnod in JSON or HTML, only in text.'
|
|
119
|
+
--agent-name=PropertyAssistant
|
|
120
|
+
--require-table-write=propertyinsurance
|
|
121
|
+
--require-table-write=propertyexpenses
|
|
122
|
+
--queue={{email}} --require-uuid --reply-all --require-storage
|
|
123
|
+
--email-address={{email}} --require-web-search
|
|
124
|
+
--toolkit-name=propertyemail;
|
|
125
|
+
worker --require-storage
|
|
126
|
+
--room-rules='/agents/PropertyAssistant/workerrules.txt'
|
|
127
|
+
--agent-name=PropertyAssistant
|
|
128
|
+
--require-toolkit=propertyemail --queue=sendupdate
|
|
129
|
+
--require-table-read=propertyinsurance
|
|
130
|
+
--require-table-read=propertyexpenses
|
|
131
|
+
--rule='Use the read_file tool to read PDFs.'"
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
service = ServiceTemplateSpec.from_yaml(
|
|
135
|
+
yaml=yaml_spec, values={"email": "owner@example.com"}
|
|
136
|
+
).to_service_spec()
|
|
137
|
+
|
|
138
|
+
assert service.container is not None
|
|
139
|
+
assert service.container.command is not None
|
|
140
|
+
assert "{{email}}" not in service.container.command
|
|
141
|
+
assert "--queue=owner@example.com" in service.container.command
|
|
142
|
+
assert "--email-address=owner@example.com" in service.container.command
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_service_template_spec_handles_none_values():
|
|
146
|
+
yaml_spec = """
|
|
147
|
+
version: v1
|
|
148
|
+
kind: ServiceTemplate
|
|
149
|
+
|
|
150
|
+
metadata:
|
|
151
|
+
name: Plain Service
|
|
152
|
+
description: null
|
|
153
|
+
repo: null
|
|
154
|
+
icon: null
|
|
155
|
+
annotations: null
|
|
156
|
+
|
|
157
|
+
agents:
|
|
158
|
+
- name: Support
|
|
159
|
+
description: null
|
|
160
|
+
annotations: null
|
|
161
|
+
|
|
162
|
+
container:
|
|
163
|
+
image: meshagent/example
|
|
164
|
+
command: null
|
|
165
|
+
environment:
|
|
166
|
+
- name: EMPTY
|
|
167
|
+
value: null
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
service = ServiceTemplateSpec.from_yaml(yaml=yaml_spec, values={}).to_service_spec()
|
|
171
|
+
|
|
172
|
+
assert service.metadata.annotations is not None
|
|
173
|
+
assert service.agents is not None
|
|
174
|
+
assert service.container is not None
|
|
175
|
+
assert service.container.environment is not None
|
|
176
|
+
assert service.metadata.description is None
|
|
177
|
+
assert service.metadata.repo is None
|
|
178
|
+
assert service.metadata.icon is None
|
|
179
|
+
assert service.metadata.annotations["meshagent.service.template.yaml"]
|
|
180
|
+
assert service.metadata.annotations["meshagent.service.template.values"] == "{}"
|
|
181
|
+
assert len(service.metadata.annotations) == 2
|
|
182
|
+
assert service.agents[0].description is None
|
|
183
|
+
assert service.agents[0].annotations is None
|
|
184
|
+
assert service.container.environment[0].value is None
|
|
@@ -4,10 +4,14 @@ from meshagent.api.participant_token import ApiScope
|
|
|
4
4
|
from meshagent.api.oauth import OAuthClientConfig
|
|
5
5
|
import json
|
|
6
6
|
|
|
7
|
+
import yaml as YAML
|
|
8
|
+
from yaml.loader import SafeLoader
|
|
9
|
+
|
|
7
10
|
|
|
8
11
|
class TokenValue(BaseModel):
|
|
9
12
|
identity: str
|
|
10
13
|
api: Optional[ApiScope] = None
|
|
14
|
+
role: Optional[str] = None
|
|
11
15
|
|
|
12
16
|
|
|
13
17
|
class EnvironmentVariable(BaseModel):
|
|
@@ -39,11 +43,19 @@ class ImageStorageMountSpec(BaseModel):
|
|
|
39
43
|
read_only: bool = True
|
|
40
44
|
|
|
41
45
|
|
|
46
|
+
class FileStorageMountSpec(BaseModel):
|
|
47
|
+
model_config = ConfigDict(extra="forbid")
|
|
48
|
+
path: str
|
|
49
|
+
text: str
|
|
50
|
+
read_only: bool = True
|
|
51
|
+
|
|
52
|
+
|
|
42
53
|
class ContainerMountSpec(BaseModel):
|
|
43
54
|
model_config = ConfigDict(extra="forbid")
|
|
44
55
|
room: Optional[list[RoomStorageMountSpec]] = None
|
|
45
56
|
project: Optional[list[ProjectStorageMountSpec]] = None
|
|
46
57
|
images: Optional[list[ImageStorageMountSpec]] = None
|
|
58
|
+
files: Optional[list[FileStorageMountSpec]] = None
|
|
47
59
|
|
|
48
60
|
|
|
49
61
|
class ServiceApiKeySpec(BaseModel):
|
|
@@ -133,6 +145,10 @@ class ServiceSpec(BaseModel):
|
|
|
133
145
|
raise ValueError("Either 'external' or 'container' must be set")
|
|
134
146
|
return m
|
|
135
147
|
|
|
148
|
+
@staticmethod
|
|
149
|
+
def from_yaml(yaml: str) -> "ServiceSpec":
|
|
150
|
+
return ServiceSpec.model_validate(YAML.safe_load(yaml))
|
|
151
|
+
|
|
136
152
|
|
|
137
153
|
class MeshagentEndpointSpec(BaseModel):
|
|
138
154
|
model_config = ConfigDict(extra="forbid")
|
|
@@ -190,6 +206,7 @@ class ServiceTemplateContainerMountSpec(BaseModel):
|
|
|
190
206
|
room: Optional[list[RoomStorageMountSpec]] = None
|
|
191
207
|
project: Optional[list[ProjectStorageMountSpec]] = None
|
|
192
208
|
images: Optional[list[ImageStorageMountSpec]] = None
|
|
209
|
+
files: Optional[list[FileStorageMountSpec]] = None
|
|
193
210
|
|
|
194
211
|
|
|
195
212
|
class ServiceTemplateMetadata(BaseModel):
|
|
@@ -201,9 +218,22 @@ class ServiceTemplateMetadata(BaseModel):
|
|
|
201
218
|
annotations: Optional[dict[str, str]] = None
|
|
202
219
|
|
|
203
220
|
|
|
221
|
+
class TemplateEnvironmentVariable(BaseModel):
|
|
222
|
+
model_config = ConfigDict(extra="forbid")
|
|
223
|
+
name: str
|
|
224
|
+
value: Optional[str] = None
|
|
225
|
+
token: Optional[TokenValue] = None
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class AgentTemplateSpec(BaseModel):
|
|
229
|
+
name: str
|
|
230
|
+
description: Optional[str] = None
|
|
231
|
+
annotations: Optional[dict[str, str]] = None
|
|
232
|
+
|
|
233
|
+
|
|
204
234
|
class ContainerTemplateSpec(BaseModel):
|
|
205
235
|
model_config = ConfigDict(extra="forbid")
|
|
206
|
-
environment: Optional[list[
|
|
236
|
+
environment: Optional[list[TemplateEnvironmentVariable]] = None
|
|
207
237
|
image: Optional[str] = None
|
|
208
238
|
command: Optional[str] = None
|
|
209
239
|
storage: Optional[ServiceTemplateContainerMountSpec] = None
|
|
@@ -246,20 +276,22 @@ class ServiceTemplateSpec(BaseModel):
|
|
|
246
276
|
version: Literal["v1"]
|
|
247
277
|
kind: Literal["ServiceTemplate"]
|
|
248
278
|
metadata: ServiceTemplateMetadata
|
|
249
|
-
agents: Optional[list[
|
|
279
|
+
agents: Optional[list[AgentTemplateSpec]] = None
|
|
250
280
|
variables: Optional[list[ServiceTemplateVariable]] = None
|
|
251
|
-
ports: list[PortSpec] =
|
|
281
|
+
ports: Optional[list[PortSpec]] = None
|
|
252
282
|
container: Optional[ContainerTemplateSpec] = None
|
|
253
283
|
external: Optional[ExternalServiceTemplateSpec] = None
|
|
254
284
|
|
|
255
|
-
def to_service_spec(self
|
|
285
|
+
def to_service_spec(self) -> ServiceSpec:
|
|
256
286
|
env = []
|
|
257
287
|
if self.container is not None:
|
|
258
288
|
if self.container.environment is not None:
|
|
259
289
|
for e in self.container.environment:
|
|
260
290
|
env.append(
|
|
261
291
|
EnvironmentVariable(
|
|
262
|
-
name=e.name,
|
|
292
|
+
name=e.name,
|
|
293
|
+
value=e.value,
|
|
294
|
+
token=e.token,
|
|
263
295
|
)
|
|
264
296
|
)
|
|
265
297
|
|
|
@@ -269,9 +301,9 @@ class ServiceTemplateSpec(BaseModel):
|
|
|
269
301
|
agents=[
|
|
270
302
|
*(
|
|
271
303
|
AgentSpec(
|
|
272
|
-
name=
|
|
273
|
-
description=
|
|
274
|
-
annotations=
|
|
304
|
+
name=a.name,
|
|
305
|
+
description=a.description,
|
|
306
|
+
annotations=a.annotations,
|
|
275
307
|
)
|
|
276
308
|
for a in self.agents
|
|
277
309
|
)
|
|
@@ -279,40 +311,69 @@ class ServiceTemplateSpec(BaseModel):
|
|
|
279
311
|
if self.agents is not None
|
|
280
312
|
else None,
|
|
281
313
|
metadata=ServiceMetadata(
|
|
282
|
-
name=
|
|
283
|
-
description=
|
|
284
|
-
repo=
|
|
285
|
-
icon=
|
|
314
|
+
name=self.metadata.name,
|
|
315
|
+
description=self.metadata.description,
|
|
316
|
+
repo=self.metadata.repo,
|
|
317
|
+
icon=self.metadata.icon,
|
|
286
318
|
annotations={
|
|
287
|
-
|
|
288
|
-
"meshagent.service.template.values": json.dumps(values),
|
|
289
|
-
**(format_yaml_map(self.metadata.annotations, values) or {}),
|
|
319
|
+
**(self.metadata.annotations or {}),
|
|
290
320
|
},
|
|
291
321
|
),
|
|
292
322
|
container=ContainerSpec(
|
|
293
|
-
command=
|
|
294
|
-
image=
|
|
323
|
+
command=self.container.command,
|
|
324
|
+
image=self.container.image,
|
|
295
325
|
environment=env,
|
|
296
326
|
storage=ContainerMountSpec(
|
|
297
|
-
room=self.container.storage.room
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
if self.container.storage is not None
|
|
305
|
-
else None,
|
|
306
|
-
),
|
|
327
|
+
room=self.container.storage.room,
|
|
328
|
+
project=self.container.storage.project,
|
|
329
|
+
images=self.container.storage.images,
|
|
330
|
+
files=self.container.storage.files,
|
|
331
|
+
)
|
|
332
|
+
if self.container.storage is not None
|
|
333
|
+
else None,
|
|
307
334
|
writable_root_fs=self.container.writable_root_fs,
|
|
308
335
|
on_demand=self.container.on_demand,
|
|
309
336
|
)
|
|
310
337
|
if self.container is not None
|
|
311
338
|
else None,
|
|
312
339
|
external=ExternalServiceSpec(
|
|
313
|
-
url=
|
|
340
|
+
url=self.external.url,
|
|
314
341
|
)
|
|
315
342
|
if self.external is not None
|
|
316
343
|
else None,
|
|
317
344
|
ports=self.ports,
|
|
318
345
|
)
|
|
346
|
+
|
|
347
|
+
@staticmethod
|
|
348
|
+
def from_yaml(yaml: str, values: dict[str, str] = {}) -> "ServiceTemplateSpec":
|
|
349
|
+
from jinja2 import Template
|
|
350
|
+
|
|
351
|
+
class _ApplyTagLoader(SafeLoader):
|
|
352
|
+
pass
|
|
353
|
+
|
|
354
|
+
def _tagged_scalar(loader, tag_suffix, node):
|
|
355
|
+
value = loader.construct_scalar(node)
|
|
356
|
+
template = Template(value)
|
|
357
|
+
return template.render(**values)
|
|
358
|
+
|
|
359
|
+
_ApplyTagLoader.add_multi_constructor("!template", _tagged_scalar)
|
|
360
|
+
|
|
361
|
+
def load_yaml(y: str):
|
|
362
|
+
return YAML.load(y, Loader=_ApplyTagLoader)
|
|
363
|
+
|
|
364
|
+
template = Template(yaml)
|
|
365
|
+
|
|
366
|
+
rendered = template.render(**values)
|
|
367
|
+
|
|
368
|
+
spec = ServiceTemplateSpec.model_validate(load_yaml(rendered))
|
|
369
|
+
|
|
370
|
+
if spec.metadata.annotations is None:
|
|
371
|
+
spec.metadata.annotations = {}
|
|
372
|
+
|
|
373
|
+
spec.metadata.annotations["meshagent.service.template.yaml"] = yaml
|
|
374
|
+
|
|
375
|
+
spec.metadata.annotations["meshagent.service.template.values"] = json.dumps(
|
|
376
|
+
values
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
return spec
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.23.0"
|
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
|
|
3
|
-
from pydantic_yaml import parse_yaml_raw_as
|
|
4
|
-
|
|
5
|
-
from meshagent.api.specs.service import (
|
|
6
|
-
AgentSpec,
|
|
7
|
-
ContainerTemplateSpec,
|
|
8
|
-
EnvironmentVariable,
|
|
9
|
-
ExternalServiceTemplateSpec,
|
|
10
|
-
ServiceTemplateMetadata,
|
|
11
|
-
ServiceTemplateSpec,
|
|
12
|
-
)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def test_service_template_spec_renders_jinja_values():
|
|
16
|
-
template = ServiceTemplateSpec(
|
|
17
|
-
version="v1",
|
|
18
|
-
kind="ServiceTemplate",
|
|
19
|
-
metadata=ServiceTemplateMetadata(
|
|
20
|
-
name="!template {{service_name}}",
|
|
21
|
-
description="!template Hello {{user}}",
|
|
22
|
-
repo="https://example.com/{{service_name}}",
|
|
23
|
-
annotations={"greeting": "!template hi {{user}}"},
|
|
24
|
-
),
|
|
25
|
-
agents=[
|
|
26
|
-
AgentSpec(
|
|
27
|
-
name="!template agent-{{service_name}}",
|
|
28
|
-
description="!template handles {{role}}",
|
|
29
|
-
annotations={"role": "!template {{role}}"},
|
|
30
|
-
)
|
|
31
|
-
],
|
|
32
|
-
external=ExternalServiceTemplateSpec(url="!template https://{{host}}/api"),
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
values = {
|
|
36
|
-
"service_name": "Concierge",
|
|
37
|
-
"user": "Rina",
|
|
38
|
-
"role": "support",
|
|
39
|
-
"host": "meshagent.dev",
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
service = template.to_service_spec(values=values)
|
|
43
|
-
|
|
44
|
-
assert service.metadata.annotations is not None
|
|
45
|
-
assert service.agents is not None
|
|
46
|
-
assert service.external is not None
|
|
47
|
-
assert service.metadata.name == "Concierge"
|
|
48
|
-
assert service.metadata.description == "Hello Rina"
|
|
49
|
-
assert service.metadata.repo == "https://example.com/{{service_name}}"
|
|
50
|
-
assert service.metadata.annotations["greeting"] == "hi Rina"
|
|
51
|
-
assert service.external.url == "https://meshagent.dev/api"
|
|
52
|
-
assert service.agents[0].name == "agent-Concierge"
|
|
53
|
-
assert service.agents[0].description == "handles support"
|
|
54
|
-
assert service.agents[0].annotations is not None
|
|
55
|
-
assert service.agents[0].annotations["role"] == "support"
|
|
56
|
-
|
|
57
|
-
source = service.metadata.annotations["meshagent.service.template.source"]
|
|
58
|
-
values_json = service.metadata.annotations["meshagent.service.template.values"]
|
|
59
|
-
assert json.loads(values_json) == values
|
|
60
|
-
assert "ServiceTemplate" in source
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def test_service_template_spec_from_yaml():
|
|
64
|
-
yaml_spec = """
|
|
65
|
-
version: v1
|
|
66
|
-
kind: ServiceTemplate
|
|
67
|
-
metadata:
|
|
68
|
-
name: "!template {{service_name}}"
|
|
69
|
-
description: "!template Hello {{user}}"
|
|
70
|
-
repo: null
|
|
71
|
-
annotations:
|
|
72
|
-
greeting: "!template hi {{user}}"
|
|
73
|
-
agents:
|
|
74
|
-
- name: "!template agent-{{service_name}}"
|
|
75
|
-
description: "!template handles {{role}}"
|
|
76
|
-
external:
|
|
77
|
-
url: "!template https://{{host}}/api"
|
|
78
|
-
"""
|
|
79
|
-
|
|
80
|
-
template = parse_yaml_raw_as(ServiceTemplateSpec, yaml_spec.encode())
|
|
81
|
-
values = {
|
|
82
|
-
"service_name": "Concierge",
|
|
83
|
-
"user": "Rina",
|
|
84
|
-
"role": "support",
|
|
85
|
-
"host": "meshagent.dev",
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
service = template.to_service_spec(values=values)
|
|
89
|
-
|
|
90
|
-
assert service.metadata.annotations is not None
|
|
91
|
-
assert service.agents is not None
|
|
92
|
-
assert service.external is not None
|
|
93
|
-
assert service.metadata.name == "Concierge"
|
|
94
|
-
assert service.metadata.description == "Hello Rina"
|
|
95
|
-
assert service.metadata.annotations["greeting"] == "hi Rina"
|
|
96
|
-
assert service.external.url == "https://meshagent.dev/api"
|
|
97
|
-
assert service.agents[0].name == "agent-Concierge"
|
|
98
|
-
assert service.agents[0].description == "handles support"
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def test_service_template_spec_replaces_email_in_command():
|
|
102
|
-
template = ServiceTemplateSpec(
|
|
103
|
-
version="v1",
|
|
104
|
-
kind="ServiceTemplate",
|
|
105
|
-
metadata=ServiceTemplateMetadata(
|
|
106
|
-
name="PropertyAssistant",
|
|
107
|
-
description="Email template",
|
|
108
|
-
repo=None,
|
|
109
|
-
annotations=None,
|
|
110
|
-
),
|
|
111
|
-
container=ContainerTemplateSpec(
|
|
112
|
-
image="us-central1-docker.pkg.dev/meshagent-public/images/cli:{SERVER_VERSION}-esgz",
|
|
113
|
-
command=(
|
|
114
|
-
'!template meshagent multi service -c "chatbot --require-uuid '
|
|
115
|
-
"--agent-name=PropertyAssistant --image-generation=gpt-image-1 "
|
|
116
|
-
"--require-storage --require-toolkit=propertyemail --require-table-write=propertyinsurance "
|
|
117
|
-
"--require-table-write=propertyexpenses --mcp --web-search "
|
|
118
|
-
"-rr='agents/PropertyAssistant/assistantrules.txt' --rule='you have access to "
|
|
119
|
-
"the email tool, and you can send out emails.'; mailbot --reply-all "
|
|
120
|
-
"--enable-attachments --room-rules='/agents/PropertyAssistant/emailrules.txt' "
|
|
121
|
-
"--rule='never respnod in JSON or HTML, only in text.' --agent-name=PropertyAssistant "
|
|
122
|
-
"--require-table-write=propertyinsurance --require-table-write=propertyexpenses "
|
|
123
|
-
"--queue={{email}} --require-uuid --reply-all --require-storage "
|
|
124
|
-
"--email-address={{email}} --require-web-search --toolkit-name=propertyemail; "
|
|
125
|
-
"worker --require-storage --room-rules='/agents/PropertyAssistant/workerrules.txt' "
|
|
126
|
-
"--agent-name=PropertyAssistant --require-toolkit=propertyemail --queue=sendupdate "
|
|
127
|
-
"--require-table-read=propertyinsurance --require-table-read=propertyexpenses "
|
|
128
|
-
"--rule='Use the read_file tool to read PDFs.'\""
|
|
129
|
-
),
|
|
130
|
-
),
|
|
131
|
-
)
|
|
132
|
-
|
|
133
|
-
service = template.to_service_spec(values={"email": "owner@example.com"})
|
|
134
|
-
|
|
135
|
-
assert service.container is not None
|
|
136
|
-
assert service.container.command is not None
|
|
137
|
-
assert "{{email}}" not in service.container.command
|
|
138
|
-
assert "--queue=owner@example.com" in service.container.command
|
|
139
|
-
assert "--email-address=owner@example.com" in service.container.command
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
def test_service_template_spec_handles_none_values():
|
|
143
|
-
template = ServiceTemplateSpec(
|
|
144
|
-
version="v1",
|
|
145
|
-
kind="ServiceTemplate",
|
|
146
|
-
metadata=ServiceTemplateMetadata(
|
|
147
|
-
name="Plain Service",
|
|
148
|
-
description=None,
|
|
149
|
-
repo=None,
|
|
150
|
-
icon=None,
|
|
151
|
-
annotations=None,
|
|
152
|
-
),
|
|
153
|
-
agents=[
|
|
154
|
-
AgentSpec(
|
|
155
|
-
name="Support",
|
|
156
|
-
description=None,
|
|
157
|
-
annotations=None,
|
|
158
|
-
)
|
|
159
|
-
],
|
|
160
|
-
container=ContainerTemplateSpec(
|
|
161
|
-
image="meshagent/example",
|
|
162
|
-
command=None,
|
|
163
|
-
environment=[EnvironmentVariable(name="EMPTY", value=None)],
|
|
164
|
-
),
|
|
165
|
-
)
|
|
166
|
-
|
|
167
|
-
service = template.to_service_spec(values={})
|
|
168
|
-
|
|
169
|
-
assert service.metadata.annotations is not None
|
|
170
|
-
assert service.agents is not None
|
|
171
|
-
assert service.container is not None
|
|
172
|
-
assert service.container.environment is not None
|
|
173
|
-
assert service.metadata.description is None
|
|
174
|
-
assert service.metadata.repo is None
|
|
175
|
-
assert service.metadata.icon is None
|
|
176
|
-
assert service.metadata.annotations["meshagent.service.template.source"]
|
|
177
|
-
assert service.metadata.annotations["meshagent.service.template.values"] == "{}"
|
|
178
|
-
assert len(service.metadata.annotations) == 2
|
|
179
|
-
assert service.agents[0].description is None
|
|
180
|
-
assert service.agents[0].annotations is None
|
|
181
|
-
assert service.container.environment[0].value is None
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.22.2"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|