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.
Files changed (47) hide show
  1. {meshagent_api-0.22.2/meshagent_api.egg-info → meshagent_api-0.23.0}/PKG-INFO +1 -1
  2. {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/client.py +45 -5
  3. meshagent_api-0.23.0/meshagent/api/service_template_test.py +184 -0
  4. {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/specs/service.py +89 -28
  5. meshagent_api-0.23.0/meshagent/api/version.py +1 -0
  6. {meshagent_api-0.22.2 → meshagent_api-0.23.0/meshagent_api.egg-info}/PKG-INFO +1 -1
  7. meshagent_api-0.22.2/meshagent/api/service_template_test.py +0 -181
  8. meshagent_api-0.22.2/meshagent/api/version.py +0 -1
  9. {meshagent_api-0.22.2 → meshagent_api-0.23.0}/LICENSE +0 -0
  10. {meshagent_api-0.22.2 → meshagent_api-0.23.0}/MANIFEST.in +0 -0
  11. {meshagent_api-0.22.2 → meshagent_api-0.23.0}/README.md +0 -0
  12. {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/__init__.py +0 -0
  13. {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/chan.py +0 -0
  14. {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/crdt.py +0 -0
  15. {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/entrypoint.js +0 -0
  16. {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/helpers.py +0 -0
  17. {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/keys.py +0 -0
  18. {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/messaging.py +0 -0
  19. {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/oauth.py +0 -0
  20. {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/participant.py +0 -0
  21. {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/participant_token.py +0 -0
  22. {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/participant_token_test.py +0 -0
  23. {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/port_forward.py +0 -0
  24. {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/protocol.py +0 -0
  25. {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/protocol_test.py +0 -0
  26. {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/py.typed +0 -0
  27. {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/reasoning_schema.py +0 -0
  28. {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/room_server_client.py +0 -0
  29. {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/runtime.py +0 -0
  30. {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/runtime_test.py +0 -0
  31. {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/schema.py +0 -0
  32. {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/schema_document.py +0 -0
  33. {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/schema_document_test.py +0 -0
  34. {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/schema_registry.py +0 -0
  35. {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/schema_test.py +0 -0
  36. {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/schema_util.py +0 -0
  37. {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/services.py +0 -0
  38. {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/token_test.py +0 -0
  39. {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/urls.py +0 -0
  40. {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/webhooks.py +0 -0
  41. {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent/api/websocket_protocol.py +0 -0
  42. {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent_api.egg-info/SOURCES.txt +0 -0
  43. {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent_api.egg-info/dependency_links.txt +0 -0
  44. {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent_api.egg-info/requires.txt +0 -0
  45. {meshagent_api-0.22.2 → meshagent_api-0.23.0}/meshagent_api.egg-info/top_level.txt +0 -0
  46. {meshagent_api-0.22.2 → meshagent_api-0.23.0}/pyproject.toml +0 -0
  47. {meshagent_api-0.22.2 → meshagent_api-0.23.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshagent-api
3
- Version: 0.22.2
3
+ Version: 0.23.0
4
4
  Summary: Python Server API for Meshagent
5
5
  License-Expression: Apache-2.0
6
6
  Project-URL: Documentation, https://docs.meshagent.com
@@ -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: ServiceTemplateSpec, values: Dict[str, str]
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.model_dump(mode="json"),
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: ServiceTemplateSpec,
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.model_dump(mode="json"),
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[EnvironmentVariable]] = None
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[AgentSpec]] = None
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, *, values: dict[str, str]) -> ServiceSpec:
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, value=format_yaml_value(e.value, values)
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=format_yaml_value(a.name, values),
273
- description=format_yaml_value(a.description, values),
274
- annotations=format_yaml_map(a.annotations, values),
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=format_yaml_value(self.metadata.name, values),
283
- description=format_yaml_value(self.metadata.description, values),
284
- repo=format_yaml_value(self.metadata.repo, values),
285
- icon=format_yaml_value(self.metadata.icon, values),
314
+ name=self.metadata.name,
315
+ description=self.metadata.description,
316
+ repo=self.metadata.repo,
317
+ icon=self.metadata.icon,
286
318
  annotations={
287
- "meshagent.service.template.source": self.model_dump_json(),
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=format_yaml_value(self.container.command, values),
294
- image=format_yaml_value(self.container.image, values),
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
- if self.container.storage is not None
299
- else None,
300
- project=self.container.storage.project
301
- if self.container.storage is not None
302
- else None,
303
- images=self.container.storage.images
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=format_yaml_value(self.external.url, values),
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshagent-api
3
- Version: 0.22.2
3
+ Version: 0.23.0
4
4
  Summary: Python Server API for Meshagent
5
5
  License-Expression: Apache-2.0
6
6
  Project-URL: Documentation, https://docs.meshagent.com
@@ -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