From 69dec326f4b6fb9d74f43ec071e49f202c602e5e Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Thu, 1 May 2025 15:12:55 -0700 Subject: [PATCH 1/5] WIP on adding a CLI --- agent_memory_server/cli.py | 200 ++++++++++++++++++++++++ agent_memory_server/docket_tasks.py | 2 + agent_memory_server/long_term_memory.py | 5 +- agent_memory_server/utils/keys.py | 4 +- agent_memory_server/worker.py | 110 ------------- pyproject.toml | 4 + test_cli.py | 96 ++++++++++++ 7 files changed, 306 insertions(+), 115 deletions(-) create mode 100644 agent_memory_server/cli.py delete mode 100644 agent_memory_server/worker.py create mode 100644 test_cli.py diff --git a/agent_memory_server/cli.py b/agent_memory_server/cli.py new file mode 100644 index 0000000..350052d --- /dev/null +++ b/agent_memory_server/cli.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python +""" +Command-line interface for agent-memory-server. +""" + +import datetime +import importlib +import sys + +import click +import uvicorn + +from agent_memory_server.config import settings +from agent_memory_server.logging import configure_logging, get_logger +from agent_memory_server.utils.redis import ensure_search_index_exists, get_redis_conn + + +# Set up logging +configure_logging() +logger = get_logger(__name__) + +# Define the version +VERSION = "0.2.0" # Matches the version in pyproject.toml + + +@click.group() +def cli(): + """Command-line interface for agent-memory-server.""" + pass + + +@cli.command() +def version(): + """Show the version of agent-memory-server.""" + click.echo(f"agent-memory-server version {VERSION}") + + +@cli.command() +@click.option("--port", default=settings.port, help="Port to run the server on") +@click.option("--host", default="0.0.0.0", help="Host to run the server on") +@click.option("--reload", is_flag=True, help="Enable auto-reload") +def api(port: int, host: str, reload: bool): + """Run the REST API server.""" + import asyncio + + from agent_memory_server.main import app, on_start_logger + + async def setup_redis(): + redis = await get_redis_conn() + await ensure_search_index_exists(redis) + + # Run the async setup + asyncio.run(setup_redis()) + + on_start_logger(port) + uvicorn.run( + app, + host=host, + port=port, + reload=reload, + ) + + +@cli.command() +@click.option("--port", default=settings.mcp_port, help="Port to run the MCP server on") +@click.option("--sse", is_flag=True, help="Run the MCP server in SSE mode") +def mcp(port: int, sse: bool): + """Run the MCP server.""" + import asyncio + + from agent_memory_server.mcp import mcp_app + + async def setup_and_run(): + redis = await get_redis_conn() + await ensure_search_index_exists(redis) + + # Run the MCP server + if sse: + await mcp_app.run_sse_async() + else: + await mcp_app.run_stdio_async() + + # Update the port in settings + settings.mcp_port = port + + # Update the port in the mcp_app + mcp_app._port = port + + click.echo(f"Starting MCP server on port {port}") + + if sse: + click.echo("Running in SSE mode") + else: + click.echo("Running in stdio mode") + + asyncio.run(setup_and_run()) + + +@cli.command() +@click.argument("task_path") +@click.option( + "--args", + "-a", + multiple=True, + help="Arguments to pass to the task in the format key=value", +) +def schedule_task(task_path: str, args: list[str]): + """ + Schedule a background task by path. + + TASK_PATH is the import path to the task function, e.g., + "agent_memory_server.long_term_memory.compact_long_term_memories" + """ + import asyncio + + from docket import Docket + + # Parse the arguments + task_args = {} + for arg in args: + try: + key, value = arg.split("=", 1) + # Try to convert to appropriate type + if value.lower() == "true": + task_args[key] = True + elif value.lower() == "false": + task_args[key] = False + elif value.isdigit(): + task_args[key] = int(value) + elif value.replace(".", "", 1).isdigit() and value.count(".") <= 1: + task_args[key] = float(value) + else: + task_args[key] = value + except ValueError: + click.echo(f"Invalid argument format: {arg}. Use key=value format.") + sys.exit(1) + + async def setup_and_run_task(): + redis = await get_redis_conn() + await ensure_search_index_exists(redis) + + # Import the task function + module_path, function_name = task_path.rsplit(".", 1) + try: + module = importlib.import_module(module_path) + task_func = getattr(module, function_name) + except (ImportError, AttributeError) as e: + click.echo(f"Error importing task: {e}") + sys.exit(1) + + # Initialize Docket client + async with Docket( + name=settings.docket_name, + url=settings.redis_url, + ) as docket: + click.echo(f"Scheduling task {task_path} with arguments: {task_args}") + await docket.add(task_func)(**task_args) + click.echo("Task scheduled successfully") + + asyncio.run(setup_and_run_task()) + + +@cli.command() +@click.option( + "--concurrency", default=10, help="Number of tasks to process concurrently" +) +@click.option( + "--redelivery-timeout", + default=30, + help="Seconds to wait before redelivering a task to another worker", +) +def task_worker(concurrency: int, redelivery_timeout: int): + """ + Start a Docket worker using the Docket name from settings. + + This command starts a worker that processes background tasks registered + with Docket. The worker uses the Docket name from settings. + """ + import asyncio + + from docket import Worker + + if not settings.use_docket: + click.echo("Docket is disabled in settings. Cannot run worker.") + sys.exit(1) + + asyncio.run( + Worker.run( + docket_name=settings.docket_name, + url=settings.redis_url, + name="agent-memory-worker", + concurrency=concurrency, + redelivery_timeout=datetime.timedelta(seconds=redelivery_timeout), + tasks=["agent_memory_server.docket_tasks:task_collection"], + ) + ) + + +if __name__ == "__main__": + cli() diff --git a/agent_memory_server/docket_tasks.py b/agent_memory_server/docket_tasks.py index a83b91e..583372e 100644 --- a/agent_memory_server/docket_tasks.py +++ b/agent_memory_server/docket_tasks.py @@ -8,6 +8,7 @@ from agent_memory_server.config import settings from agent_memory_server.long_term_memory import ( + compact_long_term_memories, extract_memory_structure, index_long_term_memories, ) @@ -22,6 +23,7 @@ extract_memory_structure, summarize_session, index_long_term_memories, + compact_long_term_memories, ] diff --git a/agent_memory_server/long_term_memory.py b/agent_memory_server/long_term_memory.py index e0d7cae..1aac617 100644 --- a/agent_memory_server/long_term_memory.py +++ b/agent_memory_server/long_term_memory.py @@ -283,7 +283,6 @@ async def compact_long_term_memories( if compact_hash_duplicates: logger.info("Starting hash-based duplicate compaction") try: - # TODO: Use RedisVL index index_name = Keys.search_index_name() # Create aggregation query to group by memory_hash and find duplicates @@ -386,7 +385,7 @@ async def compact_long_term_memories( logger.warning(f"Error checking index: {info_e}") # Get all memories matching the filters - index = await get_search_index(redis_client) + index = get_search_index(redis_client) query_str = filter_str if filter_str != "*" else "" # Create a query to get all memories @@ -675,8 +674,6 @@ async def index_long_term_memories( redis_client: Optional Redis client to use. If None, a new connection will be created. """ redis = redis_client or await get_redis_conn() - # Ensure search index exists before indexing memories - await ensure_search_index_exists(redis) background_tasks = get_background_tasks() vectorizer = OpenAITextVectorizer() embeddings = await vectorizer.aembed_many( diff --git a/agent_memory_server/utils/keys.py b/agent_memory_server/utils/keys.py index 21373e2..56452b5 100644 --- a/agent_memory_server/utils/keys.py +++ b/agent_memory_server/utils/keys.py @@ -2,6 +2,8 @@ import logging +from agent_memory_server.config import settings + logger = logging.getLogger(__name__) @@ -56,4 +58,4 @@ def metadata_key(session_id: str, namespace: str | None = None) -> str: @staticmethod def search_index_name() -> str: """Return the name of the search index.""" - return "memory_idx" + return settings.redisvl_index_name diff --git a/agent_memory_server/worker.py b/agent_memory_server/worker.py deleted file mode 100644 index 926c6ff..0000000 --- a/agent_memory_server/worker.py +++ /dev/null @@ -1,110 +0,0 @@ -""" -Run the Docket worker directly from Python. - -This module provides a way to run the background task worker in-process -instead of using the CLI command. - -Usage: - python -m agent_memory_server.worker -""" - -import asyncio -import signal -import sys -from datetime import timedelta - -from docket import Docket, Worker - -from agent_memory_server.config import settings -from agent_memory_server.docket_tasks import task_collection -from agent_memory_server.logging import get_logger - - -logger = get_logger(__name__) - - -async def run_worker(concurrency: int = 10, redelivery_timeout: int = 30): - """ - Run the Docket worker in Python. - - Args: - concurrency: Number of tasks to process concurrently - redelivery_timeout: Seconds to wait before redelivering a task to another worker - """ - if not settings.use_docket: - logger.error("Docket is disabled in settings. Cannot run worker.") - return None - - logger.info(f"Starting Docket worker for {settings.docket_name}") - logger.info( - f"Concurrency: {concurrency}, Redelivery timeout: {redelivery_timeout}s" - ) - - # Create a signal handler to gracefully shut down - shutdown_event = asyncio.Event() - - def handle_signal(sig, frame): - logger.info(f"Received signal {sig}, shutting down...") - shutdown_event.set() - - # Register signal handlers - signal.signal(signal.SIGINT, handle_signal) - signal.signal(signal.SIGTERM, handle_signal) - - try: - # Initialize Docket client - async with Docket( - name=settings.docket_name, - url=settings.redis_url, - ) as docket: - # Register all tasks - for task in task_collection: - docket.register(task) - - logger.info(f"Registered {len(task_collection)} tasks") - - # Create and run the worker - async with Worker( - docket, - concurrency=concurrency, - redelivery_timeout=timedelta(seconds=redelivery_timeout), - ) as worker: - # Run until shutdown is requested - await worker.run_forever() - - except Exception as e: - logger.error(f"Error running worker: {e}") - return 1 - - logger.info("Worker shut down gracefully") - return 0 - - -def main(): - """Command line entry point""" - # Parse command line arguments - concurrency = 10 - redelivery_timeout = 30 - - args = sys.argv[1:] - if "--concurrency" in args: - try: - idx = args.index("--concurrency") - concurrency = int(args[idx + 1]) - except (ValueError, IndexError): - pass - - if "--redelivery-timeout" in args: - try: - idx = args.index("--redelivery-timeout") - redelivery_timeout = int(args[idx + 1]) - except (ValueError, IndexError): - pass - - return asyncio.run( - run_worker(concurrency=concurrency, redelivery_timeout=redelivery_timeout) - ) - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/pyproject.toml b/pyproject.toml index d4f1c38..3375458 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,8 +31,12 @@ dependencies = [ "transformers<=4.50.3,>=4.30.0", "uvicorn>=0.24.0", "sniffio>=1.3.1", + "click>=8.1.0", ] +[project.scripts] +agent-memory = "agent_memory_server.cli:cli" + [tool.hatch.build.targets.wheel] packages = ["agent_memory_server"] diff --git a/test_cli.py b/test_cli.py new file mode 100644 index 0000000..67a63d3 --- /dev/null +++ b/test_cli.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python +""" +Test script for the agent-memory CLI. + +This script tests the basic functionality of the CLI commands. +It doesn't actually run the servers or schedule tasks, but it +verifies that the commands are properly registered and can be +invoked without errors. +""" + +import subprocess +import sys + + +def run_command(command): + """Run a command and return its output.""" + try: + result = subprocess.run( + command, + capture_output=True, + text=True, + check=True, + ) + return result.stdout + except subprocess.CalledProcessError as e: + print(f"Error running command: {e}") + print(f"stderr: {e.stderr}") + return None + + +def test_version(): + """Test the version command.""" + print("Testing 'agent-memory version'...") + output = run_command([sys.executable, "-m", "agent_memory_server.cli", "version"]) + if output and "agent-memory-server version" in output: + print("✅ Version command works") + else: + print("❌ Version command failed") + + +def test_api_help(): + """Test the api command help.""" + print("Testing 'agent-memory api --help'...") + output = run_command( + [sys.executable, "-m", "agent_memory_server.cli", "api", "--help"] + ) + if output and "Run the REST API server" in output: + print("✅ API command help works") + else: + print("❌ API command help failed") + + +def test_mcp_help(): + """Test the mcp command help.""" + print("Testing 'agent-memory mcp --help'...") + output = run_command( + [sys.executable, "-m", "agent_memory_server.cli", "mcp", "--help"] + ) + if output and "Run the MCP server" in output: + print("✅ MCP command help works") + else: + print("❌ MCP command help failed") + + +def test_schedule_task_help(): + """Test the schedule-task command help.""" + print("Testing 'agent-memory schedule-task --help'...") + output = run_command( + [sys.executable, "-m", "agent_memory_server.cli", "schedule-task", "--help"] + ) + if output and "Schedule a background task by path" in output: + print("✅ Schedule task command help works") + else: + print("❌ Schedule task command help failed") + + +def test_task_worker_help(): + """Test the task-worker command help.""" + print("Testing 'agent-memory task-worker --help'...") + output = run_command( + [sys.executable, "-m", "agent_memory_server.cli", "task-worker", "--help"] + ) + if output and "Start a Docket worker using the Docket name from settings" in output: + print("✅ Task worker command help works") + else: + print("❌ Task worker command help failed") + + +if __name__ == "__main__": + print("Testing agent-memory CLI commands...") + test_version() + test_api_help() + test_mcp_help() + test_schedule_task_help() + test_task_worker_help() + print("All tests completed.") From 5322526f1339f4474e3766e63596c696cd8b6b3b Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Thu, 8 May 2025 09:58:34 -0700 Subject: [PATCH 2/5] README cleanup --- README.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 227585c..3be2757 100644 --- a/README.md +++ b/README.md @@ -297,7 +297,7 @@ Currently, memory compaction is only available as a function in `agent_memory_se - **Semantic Deduplication**: Finds and merges memories with similar meaning using vector search - **LLM-powered Merging**: Uses language models to intelligently combine memories -### Contributing +## Contributing 1. Fork the repository 2. Create a feature branch 3. Commit your changes @@ -308,8 +308,5 @@ Currently, memory compaction is only available as a function in `agent_memory_se ```bash # Run all tests -python -m pytest tests/test_memory_compaction.py - -# Run specific integration test -python -m pytest tests/test_memory_compaction.py::TestMemoryCompaction::test_compact_memories_integration -v +pytest tests ``` From c6e863cf7cb3f5cb44eda7b8fe5527b4b1880a89 Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Thu, 8 May 2025 10:09:09 -0700 Subject: [PATCH 3/5] Work on test failures --- agent_memory_server/cli.py | 11 +++++------ tests/conftest.py | 6 +++++- tests/test_long_term_memory.py | 3 +++ tests/test_mcp.py | 25 +++++++++++++++++++++---- 4 files changed, 34 insertions(+), 11 deletions(-) diff --git a/agent_memory_server/cli.py b/agent_memory_server/cli.py index 350052d..82ccb55 100644 --- a/agent_memory_server/cli.py +++ b/agent_memory_server/cli.py @@ -15,12 +15,10 @@ from agent_memory_server.utils.redis import ensure_search_index_exists, get_redis_conn -# Set up logging configure_logging() logger = get_logger(__name__) -# Define the version -VERSION = "0.2.0" # Matches the version in pyproject.toml +VERSION = "0.2.0" @click.group() @@ -68,6 +66,10 @@ def mcp(port: int, sse: bool): """Run the MCP server.""" import asyncio + # Update the port in settings FIRST + settings.mcp_port = port + + # Import mcp_app AFTER settings have been updated from agent_memory_server.mcp import mcp_app async def setup_and_run(): @@ -83,9 +85,6 @@ async def setup_and_run(): # Update the port in settings settings.mcp_port = port - # Update the port in the mcp_app - mcp_app._port = port - click.echo(f"Starting MCP server on port {port}") if sse: diff --git a/tests/conftest.py b/tests/conftest.py index 1f09a94..958c179 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -23,6 +23,9 @@ from agent_memory_server.messages import ( MemoryMessage, ) + +# Import the module to access its global for resetting +from agent_memory_server.utils import redis as redis_utils_module from agent_memory_server.utils.keys import Keys from agent_memory_server.utils.redis import ensure_search_index_exists @@ -64,7 +67,8 @@ def mock_openai_client(): @pytest.fixture(autouse=True) async def search_index(async_redis_client): """Create a Redis connection pool for testing""" - # TODO: Replace with RedisVL index. + # Reset the cached index in redis_utils_module + redis_utils_module._index = None await async_redis_client.flushdb() diff --git a/tests/test_long_term_memory.py b/tests/test_long_term_memory.py index a3bff5b..0b820b9 100644 --- a/tests/test_long_term_memory.py +++ b/tests/test_long_term_memory.py @@ -13,6 +13,7 @@ search_long_term_memories, ) from agent_memory_server.models import LongTermMemory, LongTermMemoryResult +from agent_memory_server.utils.redis import ensure_search_index_exists class TestLongTermMemory: @@ -162,6 +163,7 @@ class TestLongTermMemoryIntegration: @pytest.mark.asyncio async def test_search_messages(self, async_redis_client): """Test searching messages""" + await ensure_search_index_exists(async_redis_client) long_term_memories = [ LongTermMemory(text="Paris is the capital of France", session_id="123"), @@ -192,6 +194,7 @@ async def test_search_messages(self, async_redis_client): @pytest.mark.asyncio async def test_search_messages_with_distance_threshold(self, async_redis_client): """Test searching messages with a distance threshold""" + await ensure_search_index_exists(async_redis_client) long_term_memories = [ LongTermMemory(text="Paris is the capital of France", session_id="123"), diff --git a/tests/test_mcp.py b/tests/test_mcp.py index 3fabba6..68bb662 100644 --- a/tests/test_mcp.py +++ b/tests/test_mcp.py @@ -1,4 +1,5 @@ import json +from unittest import mock import pytest from mcp.shared.memory import ( @@ -12,11 +13,27 @@ ) +@pytest.fixture +async def mcp_test_setup(async_redis_client, search_index): + with ( + mock.patch( + "agent_memory_server.long_term_memory.get_redis_conn", + return_value=async_redis_client, + ) as _mock_ltm_redis, + mock.patch( + "agent_memory_server.api.get_redis_conn", + return_value=async_redis_client, + create=True, + ) as _mock_api_redis, + ): + yield + + class TestMCP: """Test search functionality and memory prompt endpoints via client sessions.""" @pytest.mark.asyncio - async def test_create_long_term_memory(self, session): + async def test_create_long_term_memory(self, session, mcp_test_setup): async with client_session(mcp_app._mcp_server) as client: results = await client.call_tool( "create_long_term_memories", @@ -31,7 +48,7 @@ async def test_create_long_term_memory(self, session): assert results.content[0].text == '{"status": "ok"}' @pytest.mark.asyncio - async def test_search_memory(self, session): + async def test_search_memory(self, session, mcp_test_setup): """Test searching through session memory using the client.""" async with client_session(mcp_app._mcp_server) as client: results = await client.call_tool( @@ -65,7 +82,7 @@ async def test_search_memory(self, session): assert results["memories"][1]["session_id"] == session @pytest.mark.asyncio - async def test_memory_prompt(self, session): + async def test_memory_prompt(self, session, mcp_test_setup): """Test memory prompt with various parameter combinations.""" async with client_session(mcp_app._mcp_server) as client: prompt = await client.call_tool( @@ -98,7 +115,7 @@ async def test_memory_prompt(self, session): assert "assistant: Hi there" in message["content"]["text"] @pytest.mark.asyncio - async def test_memory_prompt_error_handling(self, session): + async def test_memory_prompt_error_handling(self, session, mcp_test_setup): """Test error handling in memory prompt generation via the client.""" async with client_session(mcp_app._mcp_server) as client: # Test with a non-existent session id From 909f0a8bed2077f86687fcaac00dd5c940f55604 Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Thu, 8 May 2025 10:28:46 -0700 Subject: [PATCH 4/5] Add CLI examples to README --- README.md | 85 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/README.md b/README.md index 3be2757..36ec055 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,91 @@ Agent Memory Server offers an MCP (Model Context Protocol) server interface powe - **search_memory**: Perform semantic search across long-term memories. - **memory_prompt**: Generate prompts enriched with session context and long-term memories. +## Command Line Interface + +The `agent-memory-server` provides a command-line interface (CLI) for managing the server and related tasks. You can access the CLI using the `agent-memory` command (assuming the package is installed in a way that makes the script available in your PATH, e.g., via `pip install ...`). + +### Available Commands + +Here's a list of available commands and their functions: + +#### `version` +Displays the current version of `agent-memory-server`. +```bash +agent-memory version +``` + +#### `api` +Starts the REST API server. +```bash +agent-memory api [OPTIONS] +``` +**Options:** +* `--port INTEGER`: Port to run the server on. (Default: value from `settings.port`, usually 8000) +* `--host TEXT`: Host to run the server on. (Default: "0.0.0.0") +* `--reload`: Enable auto-reload for development. + +Example: +```bash +agent-memory api --port 8080 --reload +``` + +#### `mcp` +Starts the Model Context Protocol (MCP) server. +```bash +agent-memory mcp [OPTIONS] +``` +**Options:** +* `--port INTEGER`: Port to run the MCP server on. (Default: value from `settings.mcp_port`, usually 9000) +* `--sse`: Run the MCP server in Server-Sent Events (SSE) mode. If not provided, it runs in stdio mode. + +Example (SSE mode): +```bash +agent-memory mcp --port 9001 --sse +``` +Example (stdio mode): +```bash +agent-memory mcp --port 9001 +``` + +#### `schedule-task` +Schedules a background task to be processed by a Docket worker. +```bash +agent-memory schedule-task [OPTIONS] +``` +**Arguments:** +* `TASK_PATH`: The Python import path to the task function. For example: `"agent_memory_server.long_term_memory.compact_long_term_memories"` + +**Options:** +* `--args TEXT` / `-a TEXT`: Arguments to pass to the task in `key=value` format. Can be specified multiple times. Values are automatically converted to boolean, integer, or float if possible, otherwise they remain strings. + +Example: +```bash +agent-memory schedule-task "agent_memory_server.long_term_memory.compact_long_term_memories" -a limit=500 -a namespace=my_namespace -a compact_semantic_duplicates=false +``` + +#### `task-worker` +Starts a Docket worker to process background tasks from the queue. This worker uses the Docket name configured in settings. +```bash +agent-memory task-worker [OPTIONS] +``` +**Options:** +* `--concurrency INTEGER`: Number of tasks to process concurrently. (Default: 10) +* `--redelivery-timeout INTEGER`: Seconds to wait before a task is redelivered to another worker if the current worker fails or times out. (Default: 30) + +Example: +```bash +agent-memory task-worker --concurrency 5 --redelivery-timeout 60 +``` + +To see help for any command, you can use `--help`: +```bash +agent-memory --help +agent-memory api --help +agent-memory mcp --help +# etc. +``` + ## Getting Started ### Local Install From 110e686dca7ffbc12822f1b1b3f8b2ba138c0afd Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Thu, 8 May 2025 14:40:09 -0700 Subject: [PATCH 5/5] Update agent_memory_server/cli.py Co-authored-by: Chris Guidry --- agent_memory_server/cli.py | 1 - 1 file changed, 1 deletion(-) diff --git a/agent_memory_server/cli.py b/agent_memory_server/cli.py index 82ccb55..6013858 100644 --- a/agent_memory_server/cli.py +++ b/agent_memory_server/cli.py @@ -187,7 +187,6 @@ def task_worker(concurrency: int, redelivery_timeout: int): Worker.run( docket_name=settings.docket_name, url=settings.redis_url, - name="agent-memory-worker", concurrency=concurrency, redelivery_timeout=datetime.timedelta(seconds=redelivery_timeout), tasks=["agent_memory_server.docket_tasks:task_collection"],