agentscope-runtime 1.0.0b1__py3-none-any.whl → 1.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. agentscope_runtime/adapters/agentscope/message.py +49 -6
  2. agentscope_runtime/adapters/agentscope/stream.py +81 -25
  3. agentscope_runtime/adapters/agentscope/tool/tool.py +1 -3
  4. agentscope_runtime/engine/app/agent_app.py +8 -1
  5. agentscope_runtime/engine/deployers/agentrun_deployer.py +2 -2
  6. agentscope_runtime/engine/deployers/utils/service_utils/fastapi_factory.py +52 -7
  7. agentscope_runtime/engine/runner.py +7 -6
  8. agentscope_runtime/engine/schemas/agent_schemas.py +21 -7
  9. agentscope_runtime/engine/services/agent_state/__init__.py +2 -0
  10. agentscope_runtime/engine/services/agent_state/state_service_factory.py +55 -0
  11. agentscope_runtime/engine/services/memory/__init__.py +2 -0
  12. agentscope_runtime/engine/services/memory/memory_service_factory.py +126 -0
  13. agentscope_runtime/engine/services/sandbox/__init__.py +2 -0
  14. agentscope_runtime/engine/services/sandbox/sandbox_service_factory.py +49 -0
  15. agentscope_runtime/engine/services/service_factory.py +119 -0
  16. agentscope_runtime/engine/services/session_history/__init__.py +2 -0
  17. agentscope_runtime/engine/services/session_history/session_history_service_factory.py +73 -0
  18. agentscope_runtime/engine/services/utils/tablestore_service_utils.py +35 -10
  19. agentscope_runtime/engine/tracing/wrapper.py +49 -31
  20. agentscope_runtime/sandbox/manager/sandbox_manager.py +0 -3
  21. agentscope_runtime/version.py +1 -1
  22. {agentscope_runtime-1.0.0b1.dist-info → agentscope_runtime-1.0.1.dist-info}/METADATA +60 -6
  23. {agentscope_runtime-1.0.0b1.dist-info → agentscope_runtime-1.0.1.dist-info}/RECORD +27 -22
  24. {agentscope_runtime-1.0.0b1.dist-info → agentscope_runtime-1.0.1.dist-info}/WHEEL +0 -0
  25. {agentscope_runtime-1.0.0b1.dist-info → agentscope_runtime-1.0.1.dist-info}/entry_points.txt +0 -0
  26. {agentscope_runtime-1.0.0b1.dist-info → agentscope_runtime-1.0.1.dist-info}/licenses/LICENSE +0 -0
  27. {agentscope_runtime-1.0.0b1.dist-info → agentscope_runtime-1.0.1.dist-info}/top_level.txt +0 -0
@@ -1,5 +1,5 @@
1
1
  # -*- coding: utf-8 -*-
2
- # pylint:disable=too-many-branches,too-many-statements
2
+ # pylint:disable=too-many-branches,too-many-statements,protected-access
3
3
  # TODO: support file block
4
4
  import json
5
5
 
@@ -7,6 +7,7 @@ from collections import OrderedDict
7
7
  from typing import Union, List
8
8
  from urllib.parse import urlparse
9
9
 
10
+ from mcp.types import CallToolResult
10
11
  from agentscope.message import (
11
12
  Msg,
12
13
  ToolUseBlock,
@@ -19,6 +20,7 @@ from agentscope.message import (
19
20
  URLSource,
20
21
  Base64Source,
21
22
  )
23
+ from agentscope.mcp._client_base import MCPClientBase
22
24
 
23
25
  from ...engine.schemas.agent_schemas import (
24
26
  Message,
@@ -338,6 +340,16 @@ def message_to_agentscope_msg(
338
340
  A single Msg object or a list of Msg objects.
339
341
  """
340
342
 
343
+ def _try_loads(v, default, keep_original=False):
344
+ if isinstance(v, (dict, list)):
345
+ return v
346
+ if isinstance(v, str) and v.strip():
347
+ try:
348
+ return json.loads(v)
349
+ except Exception:
350
+ return v if keep_original else default
351
+ return default
352
+
341
353
  def _convert_one(message: Message) -> Msg:
342
354
  # Normalize role
343
355
  if message.role == "tool":
@@ -365,12 +377,23 @@ def message_to_agentscope_msg(
365
377
  MessageType.FUNCTION_CALL,
366
378
  ):
367
379
  # convert PLUGIN_CALL, FUNCTION_CALL to ToolUseBlock
380
+ tool_args = None
381
+ for cnt in reversed(message.content):
382
+ if hasattr(cnt, "data"):
383
+ v = cnt.data.get("arguments")
384
+ if isinstance(v, (dict, list)) or (
385
+ isinstance(v, str) and v.strip()
386
+ ):
387
+ tool_args = _try_loads(v, {}, keep_original=False)
388
+ break
389
+ if tool_args is None:
390
+ tool_args = {}
368
391
  result["content"] = [
369
392
  ToolUseBlock(
370
393
  type="tool_use",
371
394
  id=message.content[0].data["call_id"],
372
395
  name=message.content[0].data.get("name"),
373
- input=json.loads(message.content[0].data["arguments"]),
396
+ input=tool_args,
374
397
  ),
375
398
  ]
376
399
  elif message.type in (
@@ -379,7 +402,20 @@ def message_to_agentscope_msg(
379
402
  ):
380
403
  # convert PLUGIN_CALL_OUTPUT, FUNCTION_CALL_OUTPUT to
381
404
  # ToolResultBlock
382
- blk = json.loads(message.content[0].data["output"])
405
+ out = None
406
+ raw_output = ""
407
+ for cnt in reversed(message.content):
408
+ if hasattr(cnt, "data"):
409
+ v = cnt.data.get("output")
410
+ if isinstance(v, (dict, list)) or (
411
+ isinstance(v, str) and v.strip()
412
+ ):
413
+ raw_output = v
414
+ out = _try_loads(v, "", keep_original=True)
415
+ break
416
+ if out is None:
417
+ out = ""
418
+ blk = out
383
419
 
384
420
  def is_valid_block(obj):
385
421
  return any(
@@ -389,12 +425,19 @@ def message_to_agentscope_msg(
389
425
 
390
426
  if isinstance(blk, list):
391
427
  if not all(is_valid_block(item) for item in blk):
392
- blk = message.content[0].data["output"]
428
+ try:
429
+ # Try to convert to MCP CallToolResult then to blocks
430
+ blk = CallToolResult.model_validate(blk)
431
+ blk = MCPClientBase._convert_mcp_content_to_as_blocks(
432
+ blk.content,
433
+ )
434
+ except Exception:
435
+ blk = raw_output
393
436
  elif isinstance(blk, dict):
394
437
  if not is_valid_block(blk):
395
- blk = message.content[0].data["output"]
438
+ blk = raw_output
396
439
  else:
397
- blk = message.content[0].data["output"]
440
+ blk = raw_output
398
441
 
399
442
  result["content"] = [
400
443
  ToolResultBlock(
@@ -14,6 +14,8 @@ from ...engine.schemas.agent_schemas import (
14
14
  ImageContent,
15
15
  AudioContent,
16
16
  DataContent,
17
+ McpCall,
18
+ McpCallOutput,
17
19
  FunctionCall,
18
20
  FunctionCallOutput,
19
21
  MessageType,
@@ -78,8 +80,6 @@ async def adapt_agentscope_message_stream(
78
80
  # Note: Tool use content only happens in the last of messages
79
81
  tool_start = False
80
82
 
81
- tool_use_messages_dict = {}
82
-
83
83
  # Cache msg id
84
84
  msg_id = msg.id
85
85
 
@@ -251,24 +251,65 @@ async def adapt_agentscope_message_stream(
251
251
  elif element.get("type") == "tool_use": # Tool use
252
252
  call_id = element.get("id")
253
253
 
254
+ if element.get("tool_type", "plugin") == "mcp":
255
+ msg_type = MessageType.MCP_TOOL_CALL
256
+ fc_cls = McpCall
257
+ fc_kwargs = {
258
+ "server_label": element.get("server_label"),
259
+ }
260
+ else:
261
+ msg_type = MessageType.PLUGIN_CALL
262
+ fc_cls = FunctionCall
263
+ fc_kwargs = {}
264
+
254
265
  if last:
255
- plugin_call_message = tool_use_messages_dict[
256
- call_id
257
- ]
258
- json_str = json.dumps(element.get("input"))
266
+ plugin_call_message = tool_use_messages_dict.get(
267
+ call_id,
268
+ )
269
+
270
+ if plugin_call_message is None:
271
+ # Only one tool use message yields, we fake
272
+ # Build a new tool call message
273
+ plugin_call_message = Message(
274
+ type=msg_type,
275
+ role="assistant",
276
+ )
277
+
278
+ data_delta_content = DataContent(
279
+ index=0,
280
+ data=fc_cls(
281
+ call_id=element.get("id"),
282
+ name=element.get("name"),
283
+ arguments="",
284
+ **fc_kwargs,
285
+ ).model_dump(),
286
+ delta=False,
287
+ )
288
+
289
+ plugin_call_message = _update_obj_attrs(
290
+ plugin_call_message,
291
+ metadata=metadata,
292
+ usage=usage,
293
+ )
294
+ yield plugin_call_message.in_progress()
295
+ yield data_delta_content.in_progress()
296
+
297
+ json_str = json.dumps(
298
+ element.get("input"),
299
+ ensure_ascii=False,
300
+ )
259
301
  data_delta_content = DataContent(
260
- index=index,
261
- data=FunctionCall(
302
+ index=None, # Will be set by `add_content`
303
+ data=fc_cls(
262
304
  call_id=element.get("id"),
263
305
  name=element.get("name"),
264
306
  arguments=json_str,
307
+ **fc_kwargs,
265
308
  ).model_dump(),
266
- delta=True,
309
+ delta=False,
267
310
  )
268
- data_delta_content = (
269
- plugin_call_message.add_delta_content(
270
- new_content=data_delta_content,
271
- )
311
+ plugin_call_message.add_content(
312
+ new_content=data_delta_content,
272
313
  )
273
314
  yield data_delta_content.completed()
274
315
  plugin_call_message = _update_obj_attrs(
@@ -284,18 +325,19 @@ async def adapt_agentscope_message_stream(
284
325
  else:
285
326
  # Build a new tool call message
286
327
  plugin_call_message = Message(
287
- type=MessageType.PLUGIN_CALL,
328
+ type=msg_type,
288
329
  role="assistant",
289
330
  )
290
331
 
291
332
  data_delta_content = DataContent(
292
- index=index,
293
- data=FunctionCall(
333
+ index=0,
334
+ data=fc_cls(
294
335
  call_id=element.get("id"),
295
336
  name=element.get("name"),
296
337
  arguments="",
338
+ **fc_kwargs,
297
339
  ).model_dump(),
298
- delta=True,
340
+ delta=False,
299
341
  )
300
342
 
301
343
  plugin_call_message = _update_obj_attrs(
@@ -304,32 +346,46 @@ async def adapt_agentscope_message_stream(
304
346
  usage=usage,
305
347
  )
306
348
  yield plugin_call_message.in_progress()
307
- data_delta_content = (
308
- plugin_call_message.add_delta_content(
309
- new_content=data_delta_content,
310
- )
311
- )
312
- yield data_delta_content
349
+ yield data_delta_content.in_progress()
313
350
 
314
351
  tool_use_messages_dict[
315
352
  call_id
316
353
  ] = plugin_call_message
317
354
 
318
355
  elif element.get("type") == "tool_result": # Tool result
356
+ call_id = element.get("id")
357
+
358
+ plugin_call_message = tool_use_messages_dict.get(
359
+ call_id,
360
+ )
361
+ # Determine the output message type and class to use
362
+ # for the tool result message based on the type of
363
+ # the original tool call message.
364
+ msg_type = MessageType.PLUGIN_CALL_OUTPUT
365
+ fc_cls = FunctionCallOutput
366
+
367
+ if plugin_call_message:
368
+ if (
369
+ plugin_call_message.type
370
+ == MessageType.MCP_TOOL_CALL
371
+ ):
372
+ msg_type = MessageType.MCP_TOOL_CALL_OUTPUT
373
+ fc_cls = McpCallOutput
374
+
319
375
  json_str = json.dumps(
320
376
  element.get("output"),
321
377
  ensure_ascii=False,
322
378
  )
323
379
  data_delta_content = DataContent(
324
380
  index=index,
325
- data=FunctionCallOutput(
381
+ data=fc_cls(
326
382
  call_id=element.get("id"),
327
383
  name=element.get("name"),
328
384
  output=json_str,
329
385
  ).model_dump(),
330
386
  )
331
387
  plugin_output_message = Message(
332
- type=MessageType.PLUGIN_CALL_OUTPUT,
388
+ type=msg_type,
333
389
  role="tool",
334
390
  content=[data_delta_content],
335
391
  )
@@ -9,9 +9,7 @@ from typing import (
9
9
  )
10
10
 
11
11
  from agentscope.tool import Toolkit, ToolResponse
12
- from agentscope.tool._registered_tool_function import (
13
- RegisteredToolFunction,
14
- )
12
+ from agentscope.tool._types import RegisteredToolFunction
15
13
 
16
14
  from agentscope_runtime.tools.base import Tool
17
15
 
@@ -1,6 +1,7 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  import logging
3
3
  import types
4
+ import platform
4
5
  import subprocess
5
6
  import shlex
6
7
  from typing import Optional, Callable, List
@@ -203,7 +204,13 @@ class AgentApp(BaseApp):
203
204
  "[AgentApp] Note: First WebUI launch may take extra time "
204
205
  "as dependencies are installed.",
205
206
  )
206
- with subprocess.Popen(shlex.split(cmd)):
207
+
208
+ cmd_kwarg = {}
209
+ if platform.system() == "Windows":
210
+ cmd_kwarg.update({"shell": True})
211
+ else:
212
+ cmd = shlex.split(cmd)
213
+ with subprocess.Popen(cmd, **cmd_kwarg):
207
214
  uvicorn.run(
208
215
  fastapi_app,
209
216
  host=host,
@@ -1022,7 +1022,7 @@ ls -lh /output/{zip_filename}
1022
1022
  artifact_type="Code",
1023
1023
  cpu=self.agentrun_config.cpu,
1024
1024
  memory=self.agentrun_config.memory,
1025
- port=8080,
1025
+ port=8090,
1026
1026
  code_configuration=CodeConfig(
1027
1027
  command=["python3", "/code/deploy_starter/main.py"],
1028
1028
  oss_bucket_name=oss_bucket_name,
@@ -1159,7 +1159,7 @@ ls -lh /output/{zip_filename}
1159
1159
  artifact_type="Code",
1160
1160
  cpu=self.agentrun_config.cpu,
1161
1161
  memory=self.agentrun_config.memory,
1162
- port=8080,
1162
+ port=8090,
1163
1163
  code_configuration=CodeConfig(
1164
1164
  command=["python3", "/code/deploy_starter/main.py"],
1165
1165
  oss_bucket_name=oss_bucket_name,
@@ -693,9 +693,32 @@ class FastAPIAppFactory:
693
693
  parsing."""
694
694
  is_async_gen = inspect.isasyncgenfunction(handler)
695
695
 
696
+ # NOTE:
697
+ # -----
698
+ # FastAPI >= 0.123.5 uses Dependant.is_coroutine_callable, which in
699
+ # turn unwraps callables via inspect.unwrap() and then inspects the
700
+ # unwrapped target to decide whether it is a coroutine function /
701
+ # generator / async generator.
702
+ #
703
+ # If we decorate an async-generator handler with
704
+ # functools.wraps(handler), FastAPI will unwrap back to the original
705
+ # async-generator function and *misclassify* the endpoint as
706
+ # non-coroutine. It will then call our async wrapper *without awaiting
707
+ # it*, and later try to JSON-encode the resulting coroutine object,
708
+ # causing errors like:
709
+ # TypeError("'coroutine' object is not iterable")
710
+ #
711
+ # To avoid that, we deliberately do NOT use functools.wraps() here.
712
+ # Instead, we manually copy the key metadata (name, qualname, doc,
713
+ # module, and signature) from the original handler, but we do NOT set
714
+ # __wrapped__. This ensures:
715
+ # * FastAPI sees the wrapper itself as the callable (an async def),
716
+ # so Dependant.is_coroutine_callable is True, and it is properly
717
+ # awaited.
718
+ # * FastAPI still sees the correct signature for parameter parsing.
719
+
696
720
  if is_async_gen:
697
721
 
698
- @functools.wraps(handler)
699
722
  async def wrapped_handler(*args, **kwargs):
700
723
  async def generate():
701
724
  try:
@@ -720,12 +743,8 @@ class FastAPIAppFactory:
720
743
  media_type="text/event-stream",
721
744
  )
722
745
 
723
- wrapped_handler.__signature__ = inspect.signature(handler)
724
- return wrapped_handler
725
-
726
746
  else:
727
747
 
728
- @functools.wraps(handler)
729
748
  def wrapped_handler(*args, **kwargs):
730
749
  def generate():
731
750
  try:
@@ -748,8 +767,34 @@ class FastAPIAppFactory:
748
767
  media_type="text/event-stream",
749
768
  )
750
769
 
751
- wrapped_handler.__signature__ = inspect.signature(handler)
752
- return wrapped_handler
770
+ # Manually propagate essential metadata without creating a __wrapped__
771
+ # chain that would confuse FastAPI's unwrap logic.
772
+ wrapped_handler.__name__ = getattr(
773
+ handler,
774
+ "__name__",
775
+ wrapped_handler.__name__,
776
+ )
777
+ wrapped_handler.__qualname__ = getattr(
778
+ handler,
779
+ "__qualname__",
780
+ wrapped_handler.__qualname__,
781
+ )
782
+ wrapped_handler.__doc__ = getattr(
783
+ handler,
784
+ "__doc__",
785
+ wrapped_handler.__doc__,
786
+ )
787
+ wrapped_handler.__module__ = getattr(
788
+ handler,
789
+ "__module__",
790
+ wrapped_handler.__module__,
791
+ )
792
+ wrapped_handler.__signature__ = inspect.signature(handler)
793
+
794
+ # Make sure FastAPI doesn't see any stale __wrapped__ pointing back to
795
+ # the original async-generator; if present, remove it.
796
+
797
+ return wrapped_handler
753
798
 
754
799
  @staticmethod
755
800
  def _add_custom_endpoints(app: FastAPI):
@@ -216,22 +216,23 @@ class Runner:
216
216
  if isinstance(request, dict):
217
217
  request = AgentRequest(**request)
218
218
 
219
+ # Assign session ID
220
+ request.session_id = request.session_id or str(uuid.uuid4())
221
+
222
+ # Assign user ID
223
+ request.user_id = request.user_id or request.session_id
224
+
219
225
  seq_gen = SequenceNumberGenerator()
220
226
 
221
227
  # Initial response
222
228
  response = AgentResponse(id=request.id)
229
+ response.session_id = request.session_id
223
230
  yield seq_gen.yield_with_sequence(response)
224
231
 
225
232
  # Set to in-progress status
226
233
  response.in_progress()
227
234
  yield seq_gen.yield_with_sequence(response)
228
235
 
229
- # Assign session ID
230
- request.session_id = request.session_id or str(uuid.uuid4())
231
-
232
- # Assign user ID
233
- request.user_id = request.session_id or request.session_id
234
-
235
236
  query_kwargs = {
236
237
  "request": request,
237
238
  }
@@ -27,6 +27,7 @@ class MessageType:
27
27
  MCP_APPROVAL_REQUEST = "mcp_approval_request"
28
28
  MCP_TOOL_CALL = "mcp_call"
29
29
  MCP_APPROVAL_RESPONSE = "mcp_approval_response"
30
+ MCP_TOOL_CALL_OUTPUT = "mcp_call_output"
30
31
  REASONING = "reasoning"
31
32
  HEARTBEAT = "heartbeat"
32
33
  ERROR = "error"
@@ -152,22 +153,35 @@ class FunctionCallOutput(BaseModel):
152
153
 
153
154
 
154
155
  class McpCall(BaseModel):
155
- id: str
156
+ """
157
+ MCP TOOL CALL MESSAGE BODY
158
+ """
159
+
160
+ call_id: Optional[str] = None
156
161
  """The unique ID of the tool call."""
157
162
 
158
- arguments: str
163
+ arguments: Optional[str] = None
159
164
  """A JSON string of the arguments passed to the tool."""
160
165
 
161
- name: str
166
+ name: Optional[str] = None
162
167
  """The name of the tool that was run."""
163
168
 
164
- server_label: str
169
+ server_label: Optional[str] = None
165
170
  """The label of the MCP server running the tool."""
166
171
 
167
- error: Optional[str] = None
168
- """The error from the tool call, if any."""
169
172
 
170
- output: Optional[str] = None
173
+ class McpCallOutput(BaseModel):
174
+ """
175
+ MCP TOOL CALL OUTPUT MESSAGE BODY
176
+ """
177
+
178
+ call_id: str
179
+ """The unique ID of the tool call."""
180
+
181
+ name: Optional[str] = None
182
+ """The name of the tool call."""
183
+
184
+ output: str
171
185
  """The output from the tool call."""
172
186
 
173
187
 
@@ -5,6 +5,7 @@ from ....common.utils.lazy_loader import install_lazy_loader
5
5
  if TYPE_CHECKING:
6
6
  from .state_service import StateService, InMemoryStateService
7
7
  from .redis_state_service import RedisStateService
8
+ from .state_service_factory import StateServiceFactory
8
9
 
9
10
  install_lazy_loader(
10
11
  globals(),
@@ -12,5 +13,6 @@ install_lazy_loader(
12
13
  "StateService": ".state_service",
13
14
  "InMemoryStateService": ".state_service",
14
15
  "RedisStateService": ".redis_state_service",
16
+ "StateServiceFactory": ".state_service_factory",
15
17
  },
16
18
  )
@@ -0,0 +1,55 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from typing import Callable, Dict
4
+
5
+ from ..service_factory import ServiceFactory
6
+ from .state_service import StateService, InMemoryStateService
7
+ from .redis_state_service import RedisStateService
8
+
9
+
10
+ class StateServiceFactory(ServiceFactory[StateService]):
11
+ """
12
+ Factory for StateService, supports both environment variables and kwargs
13
+ parameters.
14
+
15
+ Usage examples:
16
+ 1. Start with environment variables only:
17
+ export STATE_BACKEND=redis
18
+ export STATE_REDIS_REDIS_URL="redis://localhost:6379/5"
19
+ service = await StateServiceFactory.create()
20
+
21
+ 2. Override environment variables with arguments:
22
+ export STATE_BACKEND=redis
23
+ export STATE_REDIS_REDIS_URL="redis://localhost:6379/5"
24
+ service = await StateServiceFactory.create(
25
+ redis_url="redis://otherhost:6379/1"
26
+ )
27
+
28
+ 3. User-defined backend:
29
+ from my_backend import PostgresStateService
30
+ StateServiceFactory.register_backend(
31
+ "postgres",
32
+ PostgresStateService,
33
+ )
34
+ export STATE_BACKEND=postgres
35
+ export STATE_POSTGRES_DSN="postgresql://user:pass@localhost/db"
36
+ service = await StateServiceFactory.create()
37
+ """
38
+
39
+ _registry: Dict[str, Callable[..., StateService]] = {}
40
+ _env_prefix = "STATE_"
41
+ _default_backend = "in_memory"
42
+
43
+
44
+ StateServiceFactory.register_backend(
45
+ "in_memory",
46
+ lambda **kwargs: InMemoryStateService(),
47
+ )
48
+
49
+ StateServiceFactory.register_backend(
50
+ "redis",
51
+ lambda **kwargs: RedisStateService(
52
+ redis_url=kwargs.get("redis_url", "redis://localhost:6379/0"),
53
+ redis_client=kwargs.get("redis_client"),
54
+ ),
55
+ )
@@ -9,6 +9,7 @@ if TYPE_CHECKING:
9
9
  from .reme_personal_memory_service import ReMePersonalMemoryService
10
10
  from .mem0_memory_service import Mem0MemoryService
11
11
  from .tablestore_memory_service import TablestoreMemoryService
12
+ from .memory_service_factory import MemoryServiceFactory
12
13
 
13
14
  install_lazy_loader(
14
15
  globals(),
@@ -20,5 +21,6 @@ install_lazy_loader(
20
21
  "ReMePersonalMemoryService": ".reme_personal_memory_service",
21
22
  "Mem0MemoryService": ".mem0_memory_service",
22
23
  "TablestoreMemoryService": ".tablestore_memory_service",
24
+ "MemoryServiceFactory": ".memory_service_factory",
23
25
  },
24
26
  )