diff --git a/.github/workflows/pytest-redis-memory.yml b/.github/workflows/pytest-redis-memory.yml new file mode 100644 index 000000000000..bce5ba52ef30 --- /dev/null +++ b/.github/workflows/pytest-redis-memory.yml @@ -0,0 +1,67 @@ +name: Redis Memory Tests + +on: + push: + pull_request: + branches: + - main + +jobs: + test: + runs-on: ubuntu-latest + + services: + redis: + image: redis:latest + ports: + - 6379:6379 + env: + REDIS_URL: redis://localhost:6379 + + steps: + - name: Check out repository + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Wait for Redis + run: | + # give Redis time to start + sleep 5 + # Wait for Redis to respond to curl (expecting empty reply, code 52) + timeout 5s bash -c 'until curl -s localhost:6379 || [ $? -eq 52 ]; do sleep 1; done' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + + # install core packages + cd python/packages/autogen-core + pip install -e . + + cd ../autogen-agentchat + pip install -e . + + # install autogen-ext with its dependencies + cd ../autogen-ext + pip install -e ".[dev,redisvl]" + + # install test dependencies + pip install pytest pytest-asyncio pytest-cov + + # install additional dependencies for redis memory tests + pip install sentence-transformers + + - name: Run tests with coverage + run: | + cd python/packages/autogen-ext + pytest --cov=autogen_ext.memory.redis tests/memory/test_redis_memory.py -v --cov-report=xml + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + file: ./python/packages/autogen-ext/coverage.xml + name: codecov-redis-memory + fail_ci_if_error: false diff --git a/python/docs/src/user-guide/agentchat-user-guide/memory.ipynb b/python/docs/src/user-guide/agentchat-user-guide/memory.ipynb index e5e71b0fc03e..dbe5eb847c0a 100644 --- a/python/docs/src/user-guide/agentchat-user-guide/memory.ipynb +++ b/python/docs/src/user-guide/agentchat-user-guide/memory.ipynb @@ -26,7 +26,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ @@ -38,7 +38,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -72,7 +72,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 3, "metadata": {}, "outputs": [ { @@ -84,9 +84,9 @@ "---------- MemoryQueryEvent (assistant_agent) ----------\n", "[MemoryContent(content='The weather should be in metric units', mime_type=, metadata=None), MemoryContent(content='Meal recipe must be vegan', mime_type=, metadata=None)]\n", "---------- ToolCallRequestEvent (assistant_agent) ----------\n", - "[FunctionCall(id='call_apWw5JOedVvqsPfXWV7c5Uiw', arguments='{\"city\":\"New York\",\"units\":\"metric\"}', name='get_weather')]\n", + "[FunctionCall(id='call_33uMqZO6hwOfEpJavP9GW9LI', arguments='{\"city\":\"New York\",\"units\":\"metric\"}', name='get_weather')]\n", "---------- ToolCallExecutionEvent (assistant_agent) ----------\n", - "[FunctionExecutionResult(content='The weather in New York is 23 °C and Sunny.', name='get_weather', call_id='call_apWw5JOedVvqsPfXWV7c5Uiw', is_error=False)]\n", + "[FunctionExecutionResult(content='The weather in New York is 23 °C and Sunny.', name='get_weather', call_id='call_33uMqZO6hwOfEpJavP9GW9LI', is_error=False)]\n", "---------- ToolCallSummaryMessage (assistant_agent) ----------\n", "The weather in New York is 23 °C and Sunny.\n" ] @@ -94,10 +94,10 @@ { "data": { "text/plain": [ - "TaskResult(messages=[TextMessage(source='user', models_usage=None, metadata={}, created_at=datetime.datetime(2025, 6, 12, 17, 46, 33, 492791, tzinfo=datetime.timezone.utc), content='What is the weather in New York?', type='TextMessage'), MemoryQueryEvent(source='assistant_agent', models_usage=None, metadata={}, created_at=datetime.datetime(2025, 6, 12, 17, 46, 33, 494162, tzinfo=datetime.timezone.utc), content=[MemoryContent(content='The weather should be in metric units', mime_type=, metadata=None), MemoryContent(content='Meal recipe must be vegan', mime_type=, metadata=None)], type='MemoryQueryEvent'), ToolCallRequestEvent(source='assistant_agent', models_usage=RequestUsage(prompt_tokens=123, completion_tokens=19), metadata={}, created_at=datetime.datetime(2025, 6, 12, 17, 46, 34, 892272, tzinfo=datetime.timezone.utc), content=[FunctionCall(id='call_apWw5JOedVvqsPfXWV7c5Uiw', arguments='{\"city\":\"New York\",\"units\":\"metric\"}', name='get_weather')], type='ToolCallRequestEvent'), ToolCallExecutionEvent(source='assistant_agent', models_usage=None, metadata={}, created_at=datetime.datetime(2025, 6, 12, 17, 46, 34, 894081, tzinfo=datetime.timezone.utc), content=[FunctionExecutionResult(content='The weather in New York is 23 °C and Sunny.', name='get_weather', call_id='call_apWw5JOedVvqsPfXWV7c5Uiw', is_error=False)], type='ToolCallExecutionEvent'), ToolCallSummaryMessage(source='assistant_agent', models_usage=None, metadata={}, created_at=datetime.datetime(2025, 6, 12, 17, 46, 34, 895054, tzinfo=datetime.timezone.utc), content='The weather in New York is 23 °C and Sunny.', type='ToolCallSummaryMessage', tool_calls=[FunctionCall(id='call_apWw5JOedVvqsPfXWV7c5Uiw', arguments='{\"city\":\"New York\",\"units\":\"metric\"}', name='get_weather')], results=[FunctionExecutionResult(content='The weather in New York is 23 °C and Sunny.', name='get_weather', call_id='call_apWw5JOedVvqsPfXWV7c5Uiw', is_error=False)])], stop_reason=None)" + "TaskResult(messages=[TextMessage(source='user', models_usage=None, metadata={}, created_at=datetime.datetime(2025, 7, 1, 23, 53, 8, 867845, tzinfo=datetime.timezone.utc), content='What is the weather in New York?', type='TextMessage'), MemoryQueryEvent(source='assistant_agent', models_usage=None, metadata={}, created_at=datetime.datetime(2025, 7, 1, 23, 53, 8, 869589, tzinfo=datetime.timezone.utc), content=[MemoryContent(content='The weather should be in metric units', mime_type=, metadata=None), MemoryContent(content='Meal recipe must be vegan', mime_type=, metadata=None)], type='MemoryQueryEvent'), ToolCallRequestEvent(source='assistant_agent', models_usage=RequestUsage(prompt_tokens=123, completion_tokens=19), metadata={}, created_at=datetime.datetime(2025, 7, 1, 23, 53, 10, 240626, tzinfo=datetime.timezone.utc), content=[FunctionCall(id='call_33uMqZO6hwOfEpJavP9GW9LI', arguments='{\"city\":\"New York\",\"units\":\"metric\"}', name='get_weather')], type='ToolCallRequestEvent'), ToolCallExecutionEvent(source='assistant_agent', models_usage=None, metadata={}, created_at=datetime.datetime(2025, 7, 1, 23, 53, 10, 242633, tzinfo=datetime.timezone.utc), content=[FunctionExecutionResult(content='The weather in New York is 23 °C and Sunny.', name='get_weather', call_id='call_33uMqZO6hwOfEpJavP9GW9LI', is_error=False)], type='ToolCallExecutionEvent'), ToolCallSummaryMessage(source='assistant_agent', models_usage=None, metadata={}, created_at=datetime.datetime(2025, 7, 1, 23, 53, 10, 243722, tzinfo=datetime.timezone.utc), content='The weather in New York is 23 °C and Sunny.', type='ToolCallSummaryMessage')], stop_reason=None)" ] }, - "execution_count": 5, + "execution_count": 3, "metadata": {}, "output_type": "execute_result" } @@ -117,7 +117,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 4, "metadata": {}, "outputs": [ { @@ -125,11 +125,11 @@ "text/plain": [ "[UserMessage(content='What is the weather in New York?', source='user', type='UserMessage'),\n", " SystemMessage(content='\\nRelevant memory content (in chronological order):\\n1. The weather should be in metric units\\n2. Meal recipe must be vegan\\n', type='SystemMessage'),\n", - " AssistantMessage(content=[FunctionCall(id='call_apWw5JOedVvqsPfXWV7c5Uiw', arguments='{\"city\":\"New York\",\"units\":\"metric\"}', name='get_weather')], thought=None, source='assistant_agent', type='AssistantMessage'),\n", - " FunctionExecutionResultMessage(content=[FunctionExecutionResult(content='The weather in New York is 23 °C and Sunny.', name='get_weather', call_id='call_apWw5JOedVvqsPfXWV7c5Uiw', is_error=False)], type='FunctionExecutionResultMessage')]" + " AssistantMessage(content=[FunctionCall(id='call_33uMqZO6hwOfEpJavP9GW9LI', arguments='{\"city\":\"New York\",\"units\":\"metric\"}', name='get_weather')], thought=None, source='assistant_agent', type='AssistantMessage'),\n", + " FunctionExecutionResultMessage(content=[FunctionExecutionResult(content='The weather in New York is 23 °C and Sunny.', name='get_weather', call_id='call_33uMqZO6hwOfEpJavP9GW9LI', is_error=False)], type='FunctionExecutionResultMessage')]" ] }, - "execution_count": 6, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } @@ -149,7 +149,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 5, "metadata": {}, "outputs": [ { @@ -161,41 +161,44 @@ "---------- MemoryQueryEvent (assistant_agent) ----------\n", "[MemoryContent(content='The weather should be in metric units', mime_type=, metadata=None), MemoryContent(content='Meal recipe must be vegan', mime_type=, metadata=None)]\n", "---------- TextMessage (assistant_agent) ----------\n", - "Here's another vegan broth-based recipe:\n", + "Here's a brief vegan meal recipe using broth:\n", "\n", - "**Vegan Miso Soup**\n", + "**Vegan Vegetable Broth Soup**\n", "\n", "**Ingredients:**\n", + "- 1 tablespoon olive oil\n", + "- 1 onion, chopped\n", + "- 3 cloves garlic, minced\n", + "- 2 carrots, sliced\n", + "- 2 celery stalks, sliced\n", + "- 1 zucchini, chopped\n", + "- 1 cup mushrooms, sliced\n", + "- 1 cup kale or spinach, chopped\n", + "- 1 can (400g) diced tomatoes\n", "- 4 cups vegetable broth\n", - "- 3 tablespoons white miso paste\n", - "- 1 block firm tofu, cubed\n", - "- 1 cup mushrooms, sliced (shiitake or any variety you prefer)\n", - "- 2 green onions, chopped\n", - "- 1 tablespoon soy sauce (optional)\n", - "- 1/2 cup seaweed (such as wakame)\n", - "- 1 tablespoon sesame oil\n", - "- 1 tablespoon grated ginger\n", - "- Salt to taste\n", + "- 1 teaspoon dried thyme\n", + "- Salt and pepper to taste\n", + "- Fresh parsley, chopped (for garnish)\n", "\n", "**Instructions:**\n", - "1. In a pot, heat the sesame oil over medium heat.\n", - "2. Add the grated ginger and sauté for about a minute until fragrant.\n", - "3. Pour in the vegetable broth and bring it to a simmer.\n", - "4. Add the miso paste, stirring until fully dissolved.\n", - "5. Add the tofu cubes, mushrooms, and seaweed to the broth and cook for about 5 minutes.\n", - "6. Stir in soy sauce if using, and add salt to taste.\n", - "7. Garnish with chopped green onions before serving.\n", + "1. Heat olive oil in a large pot over medium heat. Add the onion and garlic, and sauté until soft.\n", + "2. Add the carrots, celery, zucchini, and mushrooms. Cook for about 5 minutes until the vegetables begin to soften.\n", + "3. Add the diced tomatoes, vegetable broth, and dried thyme. Bring to a boil.\n", + "4. Reduce heat and let it simmer for about 20 minutes, or until the vegetables are tender.\n", + "5. Stir in the chopped kale or spinach and cook for another 5 minutes.\n", + "6. Season with salt and pepper to taste.\n", + "7. Serve hot, garnished with fresh parsley.\n", "\n", - "Enjoy your delicious and nutritious vegan miso soup! TERMINATE\n" + "Enjoy your comforting vegan vegetable broth soup!\n" ] }, { "data": { "text/plain": [ - "TaskResult(messages=[TextMessage(source='user', models_usage=None, metadata={}, created_at=datetime.datetime(2025, 6, 12, 17, 47, 19, 247083, tzinfo=datetime.timezone.utc), content='Write brief meal recipe with broth', type='TextMessage'), MemoryQueryEvent(source='assistant_agent', models_usage=None, metadata={}, created_at=datetime.datetime(2025, 6, 12, 17, 47, 19, 248736, tzinfo=datetime.timezone.utc), content=[MemoryContent(content='The weather should be in metric units', mime_type=, metadata=None), MemoryContent(content='Meal recipe must be vegan', mime_type=, metadata=None)], type='MemoryQueryEvent'), TextMessage(source='assistant_agent', models_usage=RequestUsage(prompt_tokens=528, completion_tokens=233), metadata={}, created_at=datetime.datetime(2025, 6, 12, 17, 47, 26, 130554, tzinfo=datetime.timezone.utc), content=\"Here's another vegan broth-based recipe:\\n\\n**Vegan Miso Soup**\\n\\n**Ingredients:**\\n- 4 cups vegetable broth\\n- 3 tablespoons white miso paste\\n- 1 block firm tofu, cubed\\n- 1 cup mushrooms, sliced (shiitake or any variety you prefer)\\n- 2 green onions, chopped\\n- 1 tablespoon soy sauce (optional)\\n- 1/2 cup seaweed (such as wakame)\\n- 1 tablespoon sesame oil\\n- 1 tablespoon grated ginger\\n- Salt to taste\\n\\n**Instructions:**\\n1. In a pot, heat the sesame oil over medium heat.\\n2. Add the grated ginger and sauté for about a minute until fragrant.\\n3. Pour in the vegetable broth and bring it to a simmer.\\n4. Add the miso paste, stirring until fully dissolved.\\n5. Add the tofu cubes, mushrooms, and seaweed to the broth and cook for about 5 minutes.\\n6. Stir in soy sauce if using, and add salt to taste.\\n7. Garnish with chopped green onions before serving.\\n\\nEnjoy your delicious and nutritious vegan miso soup! TERMINATE\", type='TextMessage')], stop_reason=None)" + "TaskResult(messages=[TextMessage(source='user', models_usage=None, metadata={}, created_at=datetime.datetime(2025, 7, 1, 23, 53, 10, 256897, tzinfo=datetime.timezone.utc), content='Write brief meal recipe with broth', type='TextMessage'), MemoryQueryEvent(source='assistant_agent', models_usage=None, metadata={}, created_at=datetime.datetime(2025, 7, 1, 23, 53, 10, 258468, tzinfo=datetime.timezone.utc), content=[MemoryContent(content='The weather should be in metric units', mime_type=, metadata=None), MemoryContent(content='Meal recipe must be vegan', mime_type=, metadata=None)], type='MemoryQueryEvent'), TextMessage(source='assistant_agent', models_usage=RequestUsage(prompt_tokens=205, completion_tokens=266), metadata={}, created_at=datetime.datetime(2025, 7, 1, 23, 53, 14, 67151, tzinfo=datetime.timezone.utc), content=\"Here's a brief vegan meal recipe using broth:\\n\\n**Vegan Vegetable Broth Soup**\\n\\n**Ingredients:**\\n- 1 tablespoon olive oil\\n- 1 onion, chopped\\n- 3 cloves garlic, minced\\n- 2 carrots, sliced\\n- 2 celery stalks, sliced\\n- 1 zucchini, chopped\\n- 1 cup mushrooms, sliced\\n- 1 cup kale or spinach, chopped\\n- 1 can (400g) diced tomatoes\\n- 4 cups vegetable broth\\n- 1 teaspoon dried thyme\\n- Salt and pepper to taste\\n- Fresh parsley, chopped (for garnish)\\n\\n**Instructions:**\\n1. Heat olive oil in a large pot over medium heat. Add the onion and garlic, and sauté until soft.\\n2. Add the carrots, celery, zucchini, and mushrooms. Cook for about 5 minutes until the vegetables begin to soften.\\n3. Add the diced tomatoes, vegetable broth, and dried thyme. Bring to a boil.\\n4. Reduce heat and let it simmer for about 20 minutes, or until the vegetables are tender.\\n5. Stir in the chopped kale or spinach and cook for another 5 minutes.\\n6. Season with salt and pepper to taste.\\n7. Serve hot, garnished with fresh parsley.\\n\\nEnjoy your comforting vegan vegetable broth soup!\", type='TextMessage')], stop_reason=None)" ] }, - "execution_count": 8, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } @@ -216,16 +219,18 @@ "Specifically, you will need to overload the `add`, `query` and `update_context` methods to implement the desired functionality and pass the memory store to your agent.\n", "\n", "\n", - "Currently the following example memory stores are available as part of the {py:class}`~autogen_ext` extensions package. \n", + "Currently the following example memory stores are available as part of the {py:class}`~autogen_ext` extensions package.\n", "\n", - "- `autogen_ext.memory.chromadb.ChromaDBVectorMemory`: A memory store that uses a vector database to store and retrieve information. \n", + "- `autogen_ext.memory.chromadb.ChromaDBVectorMemory`: A memory store that uses a vector database to store and retrieve information.\n", "\n", - "- `autogen_ext.memory.chromadb.SentenceTransformerEmbeddingFunctionConfig`: A configuration class for the SentenceTransformer embedding function used by the `ChromaDBVectorMemory` store. Note that other embedding functions such as `autogen_ext.memory.openai.OpenAIEmbeddingFunctionConfig` can also be used with the `ChromaDBVectorMemory` store.\n" + "- `autogen_ext.memory.chromadb.SentenceTransformerEmbeddingFunctionConfig`: A configuration class for the SentenceTransformer embedding function used by the `ChromaDBVectorMemory` store. Note that other embedding functions such as `autogen_ext.memory.openai.OpenAIEmbeddingFunctionConfig` can also be used with the `ChromaDBVectorMemory` store.\n", + "\n", + "- `autogen_ext.memory.redis_memory.RedisMemory`: A memory store that uses a Redis vector database to store and retrieve information.\n" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": {}, "outputs": [ { @@ -235,11 +240,11 @@ "---------- TextMessage (user) ----------\n", "What is the weather in New York?\n", "---------- MemoryQueryEvent (assistant_agent) ----------\n", - "[MemoryContent(content='The weather should be in metric units', mime_type='MemoryMimeType.TEXT', metadata={'type': 'units', 'mime_type': 'MemoryMimeType.TEXT', 'category': 'preferences', 'score': 0.4342840313911438, 'id': 'd7ed6e42-0bf5-4ee8-b5b5-fbe06f583477'})]\n", + "[MemoryContent(content='The weather should be in metric units', mime_type='MemoryMimeType.TEXT', metadata={'category': 'preferences', 'mime_type': 'MemoryMimeType.TEXT', 'type': 'units', 'score': 0.4342913031578064, 'id': 'b8a70e90-a39f-47ed-ab7b-5a274009d9f0'}), MemoryContent(content='The weather should be in metric units', mime_type='MemoryMimeType.TEXT', metadata={'mime_type': 'MemoryMimeType.TEXT', 'type': 'units', 'category': 'preferences', 'score': 0.4342913031578064, 'id': 'b240f12a-1440-42d1-8f5e-3d8a388363f2'})]\n", "---------- ToolCallRequestEvent (assistant_agent) ----------\n", - "[FunctionCall(id='call_ufpz7LGcn19ZroowyEraj9bd', arguments='{\"city\":\"New York\",\"units\":\"metric\"}', name='get_weather')]\n", + "[FunctionCall(id='call_YmKqq1nWXgAkAAyXWWk9YpFW', arguments='{\"city\":\"New York\",\"units\":\"metric\"}', name='get_weather')]\n", "---------- ToolCallExecutionEvent (assistant_agent) ----------\n", - "[FunctionExecutionResult(content='The weather in New York is 23 °C and Sunny.', name='get_weather', call_id='call_ufpz7LGcn19ZroowyEraj9bd', is_error=False)]\n", + "[FunctionExecutionResult(content='The weather in New York is 23 °C and Sunny.', name='get_weather', call_id='call_YmKqq1nWXgAkAAyXWWk9YpFW', is_error=False)]\n", "---------- ToolCallSummaryMessage (assistant_agent) ----------\n", "The weather in New York is 23 °C and Sunny.\n" ] @@ -316,16 +321,16 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "'{\"provider\":\"autogen_ext.memory.chromadb.ChromaDBVectorMemory\",\"component_type\":\"memory\",\"version\":1,\"component_version\":1,\"description\":\"Store and retrieve memory using vector similarity search powered by ChromaDB.\",\"label\":\"ChromaDBVectorMemory\",\"config\":{\"client_type\":\"persistent\",\"collection_name\":\"preferences\",\"distance_metric\":\"cosine\",\"k\":2,\"score_threshold\":0.4,\"allow_reset\":false,\"tenant\":\"default_tenant\",\"database\":\"default_database\",\"embedding_function_config\":{\"function_type\":\"sentence_transformer\",\"model_name\":\"all-MiniLM-L6-v2\"},\"persistence_path\":\"/var/folders/wg/hgs_dt8n5lbd3gx3pq7k6lym0000gn/T/tmp9qcaqchy\"}}'" + "'{\"provider\":\"autogen_ext.memory.chromadb.ChromaDBVectorMemory\",\"component_type\":\"memory\",\"version\":1,\"component_version\":1,\"description\":\"Store and retrieve memory using vector similarity search powered by ChromaDB.\",\"label\":\"ChromaDBVectorMemory\",\"config\":{\"client_type\":\"persistent\",\"collection_name\":\"preferences\",\"distance_metric\":\"cosine\",\"k\":2,\"score_threshold\":0.4,\"allow_reset\":false,\"tenant\":\"default_tenant\",\"database\":\"default_database\",\"persistence_path\":\"/Users/justin.cechmanek/.chromadb_autogen\"}}'" ] }, - "execution_count": 10, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } @@ -334,6 +339,95 @@ "chroma_user_memory.dump_component().model_dump_json()" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Redis Memory\n", + "You can perform the same persistent memory storage using Redis. Note, you will need to have a running Redis instance to connect to.\n", + "\n", + "See {py:class}`~autogen_ext.memory.redis.RedisMemory` for instructions to run Redis locally or via Docker." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "---------- TextMessage (user) ----------\n", + "What is the weather in New York?\n", + "---------- MemoryQueryEvent (assistant_agent) ----------\n", + "[MemoryContent(content='The weather should be in metric units', mime_type=, metadata={'category': 'preferences', 'type': 'units'})]\n", + "---------- ToolCallRequestEvent (assistant_agent) ----------\n", + "[FunctionCall(id='call_1R6wV3uDOK8mGK2Vh2t0h4ld', arguments='{\"city\":\"New York\",\"units\":\"metric\"}', name='get_weather')]\n", + "---------- ToolCallExecutionEvent (assistant_agent) ----------\n", + "[FunctionExecutionResult(content='The weather in New York is 23 °C and Sunny.', name='get_weather', call_id='call_1R6wV3uDOK8mGK2Vh2t0h4ld', is_error=False)]\n", + "---------- ToolCallSummaryMessage (assistant_agent) ----------\n", + "The weather in New York is 23 °C and Sunny.\n" + ] + } + ], + "source": [ + "from logging import WARNING, getLogger\n", + "\n", + "from autogen_agentchat.agents import AssistantAgent\n", + "from autogen_agentchat.ui import Console\n", + "from autogen_core.memory import MemoryContent, MemoryMimeType\n", + "from autogen_ext.memory.redis_memory import RedisMemory, RedisMemoryConfig\n", + "from autogen_ext.models.openai import OpenAIChatCompletionClient\n", + "\n", + "logger = getLogger()\n", + "logger.setLevel(WARNING)\n", + "\n", + "# Initailize Redis memory\n", + "redis_memory = RedisMemory(\n", + " config=RedisMemoryConfig(\n", + " redis_url=\"redis://localhost:6379\",\n", + " index_name=\"chat_history\",\n", + " prefix=\"memory\",\n", + " )\n", + ")\n", + "\n", + "# Add user preferences to memory\n", + "await redis_memory.add(\n", + " MemoryContent(\n", + " content=\"The weather should be in metric units\",\n", + " mime_type=MemoryMimeType.TEXT,\n", + " metadata={\"category\": \"preferences\", \"type\": \"units\"},\n", + " )\n", + ")\n", + "\n", + "await redis_memory.add(\n", + " MemoryContent(\n", + " content=\"Meal recipe must be vegan\",\n", + " mime_type=MemoryMimeType.TEXT,\n", + " metadata={\"category\": \"preferences\", \"type\": \"dietary\"},\n", + " )\n", + ")\n", + "\n", + "model_client = OpenAIChatCompletionClient(\n", + " model=\"gpt-4o\",\n", + ")\n", + "\n", + "# Create assistant agent with ChromaDB memory\n", + "assistant_agent = AssistantAgent(\n", + " name=\"assistant_agent\",\n", + " model_client=model_client,\n", + " tools=[get_weather],\n", + " memory=[redis_memory],\n", + ")\n", + "\n", + "stream = assistant_agent.run_stream(task=\"What is the weather in New York?\")\n", + "await Console(stream)\n", + "\n", + "await model_client.close()\n", + "await redis_memory.close()" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -356,7 +450,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "metadata": {}, "outputs": [], "source": [ @@ -440,14 +534,14 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Indexed 72 chunks from 4 AutoGen documents\n" + "Indexed 70 chunks from 4 AutoGen documents\n" ] } ], @@ -492,20 +586,29 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 12, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "---------- user ----------\n", + "---------- TextMessage (user) ----------\n", "What is AgentChat?\n", - "Query results: results=[MemoryContent(content='ng OpenAI\\'s GPT-4o model. See [other supported models](https://microsoft.github.io/autogen/stable/user-guide/agentchat-user-guide/tutorial/models.html). ```python import asyncio from autogen_agentchat.agents import AssistantAgent from autogen_ext.models.openai import OpenAIChatCompletionClient async def main() -> None: model_client = OpenAIChatCompletionClient(model=\"gpt-4o\") agent = AssistantAgent(\"assistant\", model_client=model_client) print(await agent.run(task=\"Say \\'Hello World!\\'\")) await model_client.close() asyncio.run(main()) ``` ### Web Browsing Agent Team Create a group chat team with a web surfer agent and a user proxy agent for web browsing tasks. You need to install [playwright](https://playwright.dev/python/docs/library). ```python # pip install -U autogen-agentchat autogen-ext[openai,web-surfer] # playwright install import asyncio from autogen_agentchat.agents import UserProxyAgent from autogen_agentchat.conditions import TextMentionTermination from autogen_agentchat.teams import RoundRobinGroupChat from autogen_agentchat.ui import Console from autogen_ext.models.openai import OpenAIChatCompletionClient from autogen_ext.agents.web_surfer import MultimodalWebSurfer async def main() -> None: model_client = OpenAIChatCompletionClient(model=\"gpt-4o\") # The web surfer will open a Chromium browser window to perform web browsing tasks. web_surfer = MultimodalWebSurfer(\"web_surfer\", model_client, headless=False, animate_actions=True) # The user proxy agent is used to ge', mime_type='MemoryMimeType.TEXT', metadata={'chunk_index': 1, 'mime_type': 'MemoryMimeType.TEXT', 'source': 'https://raw.githubusercontent.com/microsoft/autogen/main/README.md', 'score': 0.48810458183288574, 'id': '16088e03-0153-4da3-9dec-643b39c549f5'}), MemoryContent(content='els_usage=None content='AutoGen is a programming framework for building multi-agent applications.' type='ToolCallSummaryMessage' The call to the on_messages() method returns a Response that contains the agent’s final response in the chat_message attribute, as well as a list of inner messages in the inner_messages attribute, which stores the agent’s “thought process” that led to the final response. Note It is important to note that on_messages() will update the internal state of the agent – it will add the messages to the agent’s history. So you should call this method with new messages. You should not repeatedly call this method with the same messages or the complete history. Note Unlike in v0.2 AgentChat, the tools are executed by the same agent directly within the same call to on_messages() . By default, the agent will return the result of the tool call as the final response. You can also call the run() method, which is a convenience method that calls on_messages() . It follows the same interface as Teams and returns a TaskResult object. Multi-Modal Input # The AssistantAgent can handle multi-modal input by providing the input as a MultiModalMessage . from io import BytesIO import PIL import requests from autogen_agentchat.messages import MultiModalMessage from autogen_core import Image # Create a multi-modal message with random image and text. pil_image = PIL . Image . open ( BytesIO ( requests . get ( "https://picsum.photos/300/200" ) . content )', mime_type='MemoryMimeType.TEXT', metadata={'chunk_index': 3, 'mime_type': 'MemoryMimeType.TEXT', 'source': 'https://microsoft.github.io/autogen/dev/user-guide/agentchat-user-guide/tutorial/agents.html', 'score': 0.4665141701698303, 'id': '3d603b62-7cab-4f74-b671-586fe36306f2'}), MemoryContent(content='AgentChat Termination Termination # In the previous section, we explored how to define agents, and organize them into teams that can solve tasks. However, a run can go on forever, and in many cases, we need to know when to stop them. This is the role of the termination condition. AgentChat supports several termination condition by providing a base TerminationCondition class and several implementations that inherit from it. A termination condition is a callable that takes a sequence of BaseAgentEvent or BaseChatMessage objects since the last time the condition was called , and returns a StopMessage if the conversation should be terminated, or None otherwise. Once a termination condition has been reached, it must be reset by calling reset() before it can be used again. Some important things to note about termination conditions: They are stateful but reset automatically after each run ( run() or run_stream() ) is finished. They can be combined using the AND and OR operators. Note For group chat teams (i.e., RoundRobinGroupChat , SelectorGroupChat , and Swarm ), the termination condition is called after each agent responds. While a response may contain multiple inner messages, the team calls its termination condition just once for all the messages from a single response. So the condition is called with the “delta sequence” of messages since the last time it was called. Built-In Termination Conditions: MaxMessageTermination : Stops after a specified number of messages have been produced,', mime_type='MemoryMimeType.TEXT', metadata={'chunk_index': 1, 'mime_type': 'MemoryMimeType.TEXT', 'source': 'https://microsoft.github.io/autogen/dev/user-guide/agentchat-user-guide/tutorial/termination.html', 'score': 0.461774212772051, 'id': '699ef490-d108-4cd3-b629-c1198d6b78ba'})]\n", - "---------- rag_assistant ----------\n", - "[MemoryContent(content='ng OpenAI\\'s GPT-4o model. See [other supported models](https://microsoft.github.io/autogen/stable/user-guide/agentchat-user-guide/tutorial/models.html). ```python import asyncio from autogen_agentchat.agents import AssistantAgent from autogen_ext.models.openai import OpenAIChatCompletionClient async def main() -> None: model_client = OpenAIChatCompletionClient(model=\"gpt-4o\") agent = AssistantAgent(\"assistant\", model_client=model_client) print(await agent.run(task=\"Say \\'Hello World!\\'\")) await model_client.close() asyncio.run(main()) ``` ### Web Browsing Agent Team Create a group chat team with a web surfer agent and a user proxy agent for web browsing tasks. You need to install [playwright](https://playwright.dev/python/docs/library). ```python # pip install -U autogen-agentchat autogen-ext[openai,web-surfer] # playwright install import asyncio from autogen_agentchat.agents import UserProxyAgent from autogen_agentchat.conditions import TextMentionTermination from autogen_agentchat.teams import RoundRobinGroupChat from autogen_agentchat.ui import Console from autogen_ext.models.openai import OpenAIChatCompletionClient from autogen_ext.agents.web_surfer import MultimodalWebSurfer async def main() -> None: model_client = OpenAIChatCompletionClient(model=\"gpt-4o\") # The web surfer will open a Chromium browser window to perform web browsing tasks. web_surfer = MultimodalWebSurfer(\"web_surfer\", model_client, headless=False, animate_actions=True) # The user proxy agent is used to ge', mime_type='MemoryMimeType.TEXT', metadata={'chunk_index': 1, 'mime_type': 'MemoryMimeType.TEXT', 'source': 'https://raw.githubusercontent.com/microsoft/autogen/main/README.md', 'score': 0.48810458183288574, 'id': '16088e03-0153-4da3-9dec-643b39c549f5'}), MemoryContent(content='els_usage=None content='AutoGen is a programming framework for building multi-agent applications.' type='ToolCallSummaryMessage' The call to the on_messages() method returns a Response that contains the agent’s final response in the chat_message attribute, as well as a list of inner messages in the inner_messages attribute, which stores the agent’s “thought process” that led to the final response. Note It is important to note that on_messages() will update the internal state of the agent – it will add the messages to the agent’s history. So you should call this method with new messages. You should not repeatedly call this method with the same messages or the complete history. Note Unlike in v0.2 AgentChat, the tools are executed by the same agent directly within the same call to on_messages() . By default, the agent will return the result of the tool call as the final response. You can also call the run() method, which is a convenience method that calls on_messages() . It follows the same interface as Teams and returns a TaskResult object. Multi-Modal Input # The AssistantAgent can handle multi-modal input by providing the input as a MultiModalMessage . from io import BytesIO import PIL import requests from autogen_agentchat.messages import MultiModalMessage from autogen_core import Image # Create a multi-modal message with random image and text. pil_image = PIL . Image . open ( BytesIO ( requests . get ( "https://picsum.photos/300/200" ) . content )', mime_type='MemoryMimeType.TEXT', metadata={'chunk_index': 3, 'mime_type': 'MemoryMimeType.TEXT', 'source': 'https://microsoft.github.io/autogen/dev/user-guide/agentchat-user-guide/tutorial/agents.html', 'score': 0.4665141701698303, 'id': '3d603b62-7cab-4f74-b671-586fe36306f2'}), MemoryContent(content='AgentChat Termination Termination # In the previous section, we explored how to define agents, and organize them into teams that can solve tasks. However, a run can go on forever, and in many cases, we need to know when to stop them. This is the role of the termination condition. AgentChat supports several termination condition by providing a base TerminationCondition class and several implementations that inherit from it. A termination condition is a callable that takes a sequenceBaseChatMessageent or BaseChatMessage objects since the last time the condition was called , and returns a StopMessage if the conversation should be terminated, or None otherwise. Once a termination condition has been reached, it must be reset by calling reset() before it can be used again. Some important things to note about termination conditions: They are stateful but reset automatically after each run ( run() or run_stream() ) is finished. They can be combined using the AND and OR operators. Note For group chat teams (i.e., RoundRobinGroupChat , SelectorGroupChat , and Swarm ), the termination condition is called after each agent responds. While a response may contain multiple inner messages, the team calls its termination condition just once for all the messages from a single response. So the condition is called with the “delta sequence” of messages since the last time it was called. Built-In Termination Conditions: MaxMessageTermination : Stops after a specified number of messages have been produced,', mime_type='MemoryMimeType.TEXT', metadata={'chunk_index': 1, 'mime_type': 'MemoryMimeType.TEXT', 'source': 'https://microsoft.github.io/autogen/dev/user-guide/agentchat-user-guide/tutorial/termination.html', 'score': 0.461774212772051, 'id': '699ef490-d108-4cd3-b629-c1198d6b78ba'})]\n", - "---------- rag_assistant ----------\n", - "AgentChat is part of the AutoGen framework, a programming environment for building multi-agent applications. In AgentChat, agents can interact with each other and with users to perform various tasks, including web browsing and engaging in dialogue. It utilizes models from OpenAI for chat completions and supports multi-modal input, which means agents can handle inputs that include both text and images. Additionally, AgentChat provides mechanisms to define termination conditions to control when a conversation or task should be concluded, ensuring that the agent interactions are efficient and goal-oriented. TERMINATE\n" + "---------- MemoryQueryEvent (rag_assistant) ----------\n", + "[MemoryContent(content='e of the AssistantAgent , we can now proceed to the next section to learn about the teams feature in AgentChat. previous Messages next Teams On this page Assistant Agent Getting Result Multi-Modal Input Streaming Messages Using Tools and Workbench Built-in Tools and Workbench Function Tool Model Context Protocol (MCP) Workbench Agent as a Tool Parallel Tool Calls Tool Iterations Structured Output Streaming Tokens Using Model Context Other Preset Agents Next Step Edit on GitHub Show Source so the DOM is not blocked --> © Copyright 2024, Microsoft. Privacy Policy | Consumer Health Privacy Built with the PyData Sphinx Theme 0.16.0.', mime_type='MemoryMimeType.TEXT', metadata={'chunk_index': 16, 'mime_type': 'MemoryMimeType.TEXT', 'source': 'https://microsoft.github.io/autogen/dev/user-guide/agentchat-user-guide/tutorial/agents.html', 'score': 0.6237251460552216, 'id': '6457da13-1c25-44f0-bea3-158e5c0c5bb4'}), MemoryContent(content='h Literature Review API Reference PyPi Source AgentChat Agents Agents # AutoGen AgentChat provides a set of preset Agents, each with variations in how an agent might respond to messages. All agents share the following attributes and methods: name : The unique name of the agent. description : The description of the agent in text. run : The method that runs the agent given a task as a string or a list of messages, and returns a TaskResult . Agents are expected to be stateful and this method is expected to be called with new messages, not complete history . run_stream : Same as run() but returns an iterator of messages that subclass BaseAgentEvent or BaseChatMessage followed by a TaskResult as the last item. See autogen_agentchat.messages for more information on AgentChat message types. Assistant Agent # AssistantAgent is a built-in agent that uses a language model and has the ability to use tools. Warning AssistantAgent is a “kitchen sink” agent for prototyping and educational purpose – it is very general. Make sure you read the documentation and implementation to understand the design choices. Once you fully understand the design, you may want to implement your own agent. See Custom Agent . from autogen_agentchat.agents import AssistantAgent from autogen_agentchat.messages import StructuredMessage from autogen_agentchat.ui import Console from autogen_ext.models.openai import OpenAIChatCompletionClient # Define a tool that searches the web for information. # For simplicity, we', mime_type='MemoryMimeType.TEXT', metadata={'chunk_index': 1, 'mime_type': 'MemoryMimeType.TEXT', 'source': 'https://microsoft.github.io/autogen/dev/user-guide/agentchat-user-guide/tutorial/agents.html', 'score': 0.6212755441665649, 'id': 'ab3a553f-bb69-41ff-b6a9-8397b4cb3cb1'}), MemoryContent(content='Literature Review API Reference PyPi Source AgentChat Teams Teams # In this section you’ll learn how to create a multi-agent team (or simply team) using AutoGen. A team is a group of agents that work together to achieve a common goal. We’ll first show you how to create and run a team. We’ll then explain how to observe the team’s behavior, which is crucial for debugging and understanding the team’s performance, and common operations to control the team’s behavior. AgentChat supports several team presets: RoundRobinGroupChat : A team that runs a group chat with participants taking turns in a round-robin fashion (covered on this page). Tutorial SelectorGroupChat : A team that selects the next speaker using a ChatCompletion model after each message. Tutorial MagenticOneGroupChat : A generalist multi-agent system for solving open-ended web and file-based tasks across a variety of domains. Tutorial Swarm : A team that uses HandoffMessage to signal transitions between agents. Tutorial Note When should you use a team? Teams are for complex tasks that require collaboration and diverse expertise. However, they also demand more scaffolding to steer compared to single agents. While AutoGen simplifies the process of working with teams, start with a single agent for simpler tasks, and transition to a multi-agent team when a single agent proves inadequate. Ensure that you have optimized your single agent with the appropriate tools and instructions before moving to a team-based approach. Cre', mime_type='MemoryMimeType.TEXT', metadata={'mime_type': 'MemoryMimeType.TEXT', 'chunk_index': 1, 'source': 'https://microsoft.github.io/autogen/dev/user-guide/agentchat-user-guide/tutorial/teams.html', 'score': 0.5267025232315063, 'id': '554b20a9-e041-4ac6-b2f1-11261336861c'})]\n", + "---------- TextMessage (rag_assistant) ----------\n", + "AgentChat is a framework that provides a set of preset agents designed to handle conversations and tasks using a variety of response strategies. It includes features for managing individual agents as well as creating teams of agents that can work collaboratively on complex goals. These agents are stateful, meaning they can manage and track ongoing conversations. AgentChat also includes agents that can utilize tools to enhance their capabilities.\n", + "\n", + "Key features of AgentChat include:\n", + "- **Preset Agents**: These agents are pre-configured with specific behavior patterns for handling tasks and messages.\n", + "- **Agent Attributes and Methods**: Each agent has a unique name and description, and methods like `run` and `run_stream` to execute tasks and handle messages.\n", + "- **AssistantAgent**: A built-in general-purpose agent used primarily for prototyping and educational purposes.\n", + "- **Team Configurations**: AgentChat allows for the creation of multi-agent teams for tasks that are too complex for a single agent. Teams run in preset formats like RoundRobinGroupChat or Swarm, providing structured interaction among agents.\n", + "\n", + "Overall, AgentChat is designed for flexible deployment of conversational agents, either singly or in groups, across a variety of tasks. \n", + "\n", + "TERMINATE\n" ] } ], @@ -549,7 +652,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 13, "metadata": {}, "outputs": [], "source": [ @@ -619,7 +722,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 14, "metadata": {}, "outputs": [], "source": [ @@ -627,16 +730,11 @@ "config_json = mem0_memory.dump_component().model_dump_json()\n", "print(f\"Memory config JSON: {config_json[:100]}...\")" ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [] } ], "metadata": { "kernelspec": { - "display_name": ".venv", + "display_name": "python", "language": "python", "name": "python3" }, @@ -650,7 +748,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.13" + "version": "3.12.2" } }, "nbformat": 4, diff --git a/python/packages/autogen-ext/pyproject.toml b/python/packages/autogen-ext/pyproject.toml index 6390ee2e0b6f..c8f13fdab040 100644 --- a/python/packages/autogen-ext/pyproject.toml +++ b/python/packages/autogen-ext/pyproject.toml @@ -155,6 +155,8 @@ canvas = [ "unidiff>=0.7.5", ] +redisvl = ["redisvl>=0.6.0"] + [tool.hatch.build.targets.wheel] packages = ["src/autogen_ext"] diff --git a/python/packages/autogen-ext/src/autogen_ext/memory/redis/__init__.py b/python/packages/autogen-ext/src/autogen_ext/memory/redis/__init__.py new file mode 100644 index 000000000000..606cf2d46178 --- /dev/null +++ b/python/packages/autogen-ext/src/autogen_ext/memory/redis/__init__.py @@ -0,0 +1,9 @@ +from ._redis_memory import ( + RedisMemory, + RedisMemoryConfig, +) + +__all__ = [ + "RedisMemoryConfig", + "RedisMemory", +] diff --git a/python/packages/autogen-ext/src/autogen_ext/memory/redis/_redis_memory.py b/python/packages/autogen-ext/src/autogen_ext/memory/redis/_redis_memory.py new file mode 100644 index 000000000000..f828e4e2d7e1 --- /dev/null +++ b/python/packages/autogen-ext/src/autogen_ext/memory/redis/_redis_memory.py @@ -0,0 +1,299 @@ +import logging +from typing import Any, Literal + +from autogen_core import CancellationToken, Component +from autogen_core.memory import Memory, MemoryContent, MemoryMimeType, MemoryQueryResult, UpdateContextResult +from autogen_core.model_context import ChatCompletionContext +from autogen_core.models import SystemMessage +from pydantic import BaseModel, Field + +logger = logging.getLogger(__name__) + +try: + from redis import Redis + from redisvl.extensions.message_history import SemanticMessageHistory + from redisvl.utils.utils import deserialize, serialize +except ImportError as e: + raise ImportError("To use Redis Memory RedisVL must be installed. Run `pip install autogen-ext[redisvl]`") from e + + +class RedisMemoryConfig(BaseModel): + """ + Configuration for Redis-based vector memory. + + This class defines the configuration options for using Redis as a vector memory store, + supporting semantic memory. It allows customization of the Redis connection, index settings, + similarity search parameters, and embedding model. + """ + + redis_url: str = Field(default="redis://localhost:6379", description="url of the Redis instance") + index_name: str = Field(default="chat_history", description="Name of the Redis collection") + prefix: str = Field(default="memory", description="prefix of the Redis collection") + distance_metric: Literal["cosine", "ip", "l2"] = "cosine" + algorithm: Literal["flat", "hnsw"] = "flat" + top_k: int = Field(default=10, description="Number of results to return in queries") + datatype: Literal["uint8", "int8", "float16", "float32", "float64", "bfloat16"] = "float32" + distance_threshold: float = Field(default=0.7, description="Minimum similarity score threshold") + model_name: str | None = Field( + default="sentence-transformers/all-mpnet-base-v2", description="Embedding model name" + ) + + +class RedisMemory(Memory, Component[RedisMemoryConfig]): + """ + Store and retrieve memory using vector similarity search powered by RedisVL. + + `RedisMemory` provides a vector-based memory implementation that uses RedisVL for storing and + retrieving content based on semantic similarity. It enhances agents with the ability to recall + contextually relevant information during conversations by leveraging vector embeddings to find + similar content. + + This implementation requires the RedisVL extra to be installed. Install with: + + .. code-block:: bash + + pip install "autogen-ext[redisvl]" + + Additionally, you will need access to a Redis instance. + To run a local instance of redis in docker: + + .. code-block:: bash + + docker run -d --name redis -p 6379:6379 redis:8 + + To download and run Redis locally: + + .. code-block:: bash + + curl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg + echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/redis.list + sudo apt-get update > /dev/null 2>&1 + sudo apt-get install redis-server > /dev/null 2>&1 + redis-server --daemonize yes + + Args: + config (RedisMemoryConfig | None): Configuration for the Redis memory. + If None, defaults to a RedisMemoryConfig with recommended settings. + + Example: + + .. code-block:: python + + from logging import WARNING, getLogger + + import asyncio + from autogen_agentchat.agents import AssistantAgent + from autogen_agentchat.ui import Console + from autogen_core.memory import MemoryContent, MemoryMimeType + from autogen_ext.memory.redis import RedisMemory, RedisMemoryConfig + from autogen_ext.models.openai import OpenAIChatCompletionClient + + logger = getLogger() + logger.setLevel(WARNING) + + + # Define tool to use + async def get_weather(city: str, units: str = "imperial") -> str: + if units == "imperial": + return f"The weather in {city} is 73 °F and Sunny." + elif units == "metric": + return f"The weather in {city} is 23 °C and Sunny." + else: + return f"Sorry, I don't know the weather in {city}." + + + async def main(): + # Initailize Redis memory + redis_memory = RedisMemory( + config=RedisMemoryConfig( + redis_url="redis://localhost:6379", + index_name="chat_history", + prefix="memory", + ) + ) + + # Add user preferences to memory + await redis_memory.add( + MemoryContent( + content="The weather should be in metric units", + mime_type=MemoryMimeType.TEXT, + metadata={"category": "preferences", "type": "units"}, + ) + ) + + await redis_memory.add( + MemoryContent( + content="Meal recipe must be vegan", + mime_type=MemoryMimeType.TEXT, + metadata={"category": "preferences", "type": "dietary"}, + ) + ) + + model_client = OpenAIChatCompletionClient( + model="gpt-4o", + ) + + # Create assistant agent with ChromaDB memory + assistant_agent = AssistantAgent( + name="assistant_agent", + model_client=model_client, + tools=[get_weather], + memory=[redis_memory], + ) + + stream = assistant_agent.run_stream(task="What is the weather in New York?") + await Console(stream) + + await model_client.close() + await redis_memory.close() + + + asyncio.run(main()) + + Output: + + .. code-block:: text + + ---------- TextMessage (user) ---------- + What is the weather in New York? + ---------- MemoryQueryEvent (assistant_agent) ---------- + [MemoryContent(content='The weather should be in metric units', mime_type=, metadata={'category': 'preferences', 'type': 'units'})] + ---------- ToolCallRequestEvent (assistant_agent) ---------- + [FunctionCall(id='call_tyCPvPPAV4SHWhtfpM6UMemr', arguments='{"city":"New York","units":"metric"}', name='get_weather')] + ---------- ToolCallExecutionEvent (assistant_agent) ---------- + [FunctionExecutionResult(content='The weather in New York is 23 °C and Sunny.', name='get_weather', call_id='call_tyCPvPPAV4SHWhtfpM6UMemr', is_error=False)] + ---------- ToolCallSummaryMessage (assistant_agent) ---------- + The weather in New York is 23 °C and Sunny. + + """ + + component_config_schema = RedisMemoryConfig + component_provider_override = "autogen_ext.memory.redis_memory.RedisMemory" + + def __init__(self, config: RedisMemoryConfig | None = None) -> None: + """Initialize RedisMemory.""" + self.config = config or RedisMemoryConfig() + client = Redis.from_url(url=self.config.redis_url) # type: ignore[reportUknownMemberType] + + self.message_history = SemanticMessageHistory(name=self.config.index_name, redis_client=client) + + async def update_context( + self, + model_context: ChatCompletionContext, + ) -> UpdateContextResult: + """ + Update the model context with relevant memory content. + + This method retrieves memory content relevant to the last message in the context + and adds it as a system message. This implementation uses the last message in the context + as a query to find semantically similar memories and adds them all to the context as a + single system message. + + Args: + model_context (ChatCompletionContext): The model context to update with relevant + memories. + + Returns: + UpdateContextResult: Object containing the memories that were used to update the + context. + """ + messages = await model_context.get_messages() + if messages: + last_message = str(messages[-1].content) + else: + last_message = "" + + query_results = await self.query(last_message) + + stringified_messages = "\n\n".join([str(m.content) for m in query_results.results]) + + await model_context.add_message(SystemMessage(content=stringified_messages)) + + return UpdateContextResult(memories=query_results) + + async def add(self, content: MemoryContent, cancellation_token: CancellationToken | None = None) -> None: + """Add a memory content object to Redis. + + .. note:: + + To perform semantic search over stored memories RedisMemory creates a vector embedding + from the content field of a MemoryContent object. This content is assumed to be text, and + is passed to the vector embedding model specified in RedisMemoryConfig. + + Args: + content (MemoryContent): The memory content to store within Redis. + cancellation_token (CancellationToken): Token passed to cease operation. Not used. + """ + if content.mime_type != MemoryMimeType.TEXT: + raise NotImplementedError( + f"Error: {content.mime_type} is not supported. Only MemoryMimeType.TEXT is currently supported." + ) + + self.message_history.add_message( + {"role": "user", "content": content.content, "tool_call_id": serialize(content.metadata)} # type: ignore[reportArgumentType] + ) + + async def query( + self, + query: str | MemoryContent, + cancellation_token: CancellationToken | None = None, + **kwargs: Any, + ) -> MemoryQueryResult: + """Query memory content based on semantic vector similarity. + + .. note:: + + RedisMemory.query() supports additional keyword arguments to improve query performance. + top_k (int): The maximum number of relevant memories to include. Defaults to 10. + distance_threshold (float): The maximum distance in vector space to consider a memory + semantically similar when performining cosine similarity search. Defaults to 0.7. + + Args: + query (str | MemoryContent): query to perform vector similarity search with. If a + string is passed, a vector embedding is created from it with the model specified + in the RedisMemoryConfig. If a MemoryContent object is passed, the content field + of this object is extracted and a vector embedding is created from it with the + model specified in the RedisMemoryConfig. + cancellation_token (CancellationToken): Token passed to cease operation. Not used. + + Returns: + memoryQueryResult: Object containing memories relevant to the provided query. + """ + # get the query string, or raise an error for unsupported MemoryContent types + if isinstance(query, MemoryContent): + if query.mime_type != MemoryMimeType.TEXT: + raise NotImplementedError( + f"Error: {query.mime_type} is not supported. Only MemoryMimeType.TEXT is currently supported." + ) + prompt = query.content + else: + prompt = query + + top_k = kwargs.pop("top_k", self.config.top_k) + distance_threshold = kwargs.pop("distance_threshold", self.config.distance_threshold) + + results = self.message_history.get_relevant( + prompt=prompt, # type: ignore[reportArgumentType] + top_k=top_k, + distance_threshold=distance_threshold, + raw=False, + ) + + memories = [] + for result in results: + memory = MemoryContent( + content=result["content"], # type: ignore[reportArgumentType] + mime_type=MemoryMimeType.TEXT, + metadata=deserialize(result["tool_call_id"]), # type: ignore[reportArgumentType] + ) + memories.append(memory) # type: ignore[reportUknownMemberType] + + return MemoryQueryResult(results=memories) # type: ignore[reportUknownMemberType] + + async def clear(self) -> None: + """Clear all entries from memory, preserving the RedisMemory resources.""" + self.message_history.clear() + + async def close(self) -> None: + """Clears all entries from memory, and cleans up Redis client, index and resources.""" + self.message_history.delete() diff --git a/python/packages/autogen-ext/tests/memory/test_redis_memory.py b/python/packages/autogen-ext/tests/memory/test_redis_memory.py new file mode 100644 index 000000000000..83d1b6781f20 --- /dev/null +++ b/python/packages/autogen-ext/tests/memory/test_redis_memory.py @@ -0,0 +1,324 @@ +from collections.abc import AsyncGenerator +from unittest.mock import MagicMock, patch + +import pytest +import pytest_asyncio +from autogen_core.memory import MemoryContent, MemoryMimeType +from autogen_core.model_context import BufferedChatCompletionContext +from autogen_core.models import UserMessage +from autogen_ext.memory.redis import RedisMemory, RedisMemoryConfig +from pydantic import ValidationError +from redis import Redis +from redisvl.exceptions import RedisSearchError + + +@pytest.mark.asyncio +async def test_redis_memory_add_with_mock() -> None: + with patch("autogen_ext.memory.redis._redis_memory.SemanticMessageHistory") as MockHistory: + mock_history = MagicMock() + MockHistory.return_value = mock_history + + config = RedisMemoryConfig() + memory = RedisMemory(config=config) + + content = MemoryContent(content="test content", mime_type=MemoryMimeType.TEXT, metadata={"foo": "bar"}) + await memory.add(content) + mock_history.add_message.assert_called_once() + + +@pytest.mark.asyncio +async def test_redis_memory_query_with_mock() -> None: + with patch("autogen_ext.memory.redis._redis_memory.SemanticMessageHistory") as MockHistory: + mock_history = MagicMock() + MockHistory.return_value = mock_history + + config = RedisMemoryConfig() + memory = RedisMemory(config=config) + + mock_history.get_relevant.return_value = [{"content": "test content", "tool_call_id": '{"foo": "bar"}'}] + result = await memory.query("test") + assert len(result.results) == 1 + assert result.results[0].content == "test content" + assert result.results[0].metadata == {"foo": "bar"} + mock_history.get_relevant.assert_called_once() + + +@pytest.mark.asyncio +async def test_redis_memory_clear_with_mock() -> None: + with patch("autogen_ext.memory.redis._redis_memory.SemanticMessageHistory") as MockHistory: + mock_history = MagicMock() + MockHistory.return_value = mock_history + + config = RedisMemoryConfig() + memory = RedisMemory(config=config) + + await memory.clear() + mock_history.clear.assert_called_once() + + +@pytest.mark.asyncio +async def test_redis_memory_close_with_mock() -> None: + with patch("autogen_ext.memory.redis._redis_memory.SemanticMessageHistory") as MockHistory: + mock_history = MagicMock() + MockHistory.return_value = mock_history + + config = RedisMemoryConfig() + memory = RedisMemory(config=config) + + await memory.close() + mock_history.delete.assert_called_once() + + +def redis_available() -> bool: + try: + client = Redis.from_url("redis://localhost:6379") # type: ignore[reportUnkownMemberType] + client.ping() # type: ignore[reportUnkownMemberType] + return True + except Exception: + return False + + +@pytest.fixture +def semantic_config() -> RedisMemoryConfig: + """Create base configuration using semantic memory.""" + return RedisMemoryConfig(top_k=5, distance_threshold=0.5, model_name="sentence-transformers/all-mpnet-base-v2") + + +@pytest_asyncio.fixture # type: ignore[reportUntypedFunctionDecorator] +async def semantic_memory(semantic_config: RedisMemoryConfig) -> AsyncGenerator[RedisMemory]: + memory = RedisMemory(semantic_config) + yield memory + await memory.close() + + +## UNIT TESTS ## +def test_memory_config() -> None: + default_config = RedisMemoryConfig() + assert default_config.redis_url == "redis://localhost:6379" + assert default_config.index_name == "chat_history" + assert default_config.prefix == "memory" + assert default_config.distance_metric == "cosine" + assert default_config.algorithm == "flat" + assert default_config.top_k == 10 + assert default_config.distance_threshold == 0.7 + assert default_config.model_name == "sentence-transformers/all-mpnet-base-v2" + + # test we can specify each of these values + url = "rediss://localhost:7010" + name = "custom name" + prefix = "custom prefix" + metric = "ip" + algorithm = "hnsw" + k = 5 + distance = 0.25 + model = "redis/langcache-embed-v1" + + custom_config = RedisMemoryConfig( + redis_url=url, + index_name=name, + prefix=prefix, + distance_metric=metric, # type: ignore[arg-type] + algorithm=algorithm, # type: ignore[arg-type] + top_k=k, + distance_threshold=distance, + model_name=model, + ) + assert custom_config.redis_url == url + assert custom_config.index_name == name + assert custom_config.prefix == prefix + assert custom_config.distance_metric == metric + assert custom_config.algorithm == algorithm + assert custom_config.top_k == k + assert custom_config.distance_threshold == distance + assert custom_config.model_name == model + + # test that Literal values are validated correctly + with pytest.raises(ValidationError): + _ = RedisMemoryConfig(distance_metric="approximate") # type: ignore[arg-type] + + with pytest.raises(ValidationError): + _ = RedisMemoryConfig(algorithm="pythagoras") # type: ignore[arg-type] + + +@pytest.mark.asyncio +@pytest.mark.skipif(not redis_available(), reason="Redis instance not available locally") +async def test_create_semantic_memory() -> None: + config = RedisMemoryConfig(index_name="semantic_agent") + memory = RedisMemory(config=config) + + assert memory.message_history is not None + await memory.close() + + +@pytest.mark.asyncio +@pytest.mark.skipif(not redis_available(), reason="Redis instance not available locally") +async def test_update_context(semantic_memory: RedisMemory) -> None: + """Test updating model context with retrieved memories.""" + await semantic_memory.clear() + + # Add content to memory + await semantic_memory.add( + MemoryContent( + content="Canada is the second largest country in the world.", + mime_type=MemoryMimeType.TEXT, + metadata={"category": "geography"}, + ) + ) + + # Create a model context with a message + context = BufferedChatCompletionContext(buffer_size=5) + await context.add_message(UserMessage(content="Tell me about Canada", source="user")) + + # Update context with memory + result = await semantic_memory.update_context(context) + + # Verify results + assert len(result.memories.results) > 0 + assert any("Canada" in str(r.content) for r in result.memories.results) + + # Verify context was updated + messages = await context.get_messages() + assert len(messages) > 1 # Should have the original message plus the memory content + + await semantic_memory.clear() + + await semantic_memory.add( + MemoryContent( + content="Napoleon was Emporor of France from 18 May 1804 to 6 April 1814.", + mime_type=MemoryMimeType.TEXT, + metadata={}, + ) + ) + await semantic_memory.add( + MemoryContent( + content="Napoleon was also Emporor during his second reign from 20 March 1815 to 22 June 1815.", + mime_type=MemoryMimeType.TEXT, + metadata={}, + ) + ) + + context = BufferedChatCompletionContext( + buffer_size=5, + initial_messages=[ + UserMessage(content="Can you tell me about the reign of Emperor Napoleon?", source="user"), + ], + ) + + updated_context = await semantic_memory.update_context(context) + assert updated_context is not None + assert updated_context.memories is not None + assert updated_context.memories.results is not None + assert len(updated_context.memories.results) == 2 + assert ( + updated_context.memories.results[0].content + == "Napoleon was Emporor of France from 18 May 1804 to 6 April 1814." + ) + assert ( + updated_context.memories.results[1].content + == "Napoleon was also Emporor during his second reign from 20 March 1815 to 22 June 1815." + ) + + +@pytest.mark.asyncio +@pytest.mark.skipif(not redis_available(), reason="Redis instance not available locally") +async def test_add_and_query(semantic_memory: RedisMemory) -> None: + content_1 = MemoryContent( + content="I enjoy fruits like apples, oranges, and bananas.", mime_type=MemoryMimeType.TEXT, metadata={} + ) + await semantic_memory.add(content_1) + + # find matches with a similar query + memories = await semantic_memory.query("Fruits that I like.") + assert len(memories.results) == 1 + + # don't return anything for dissimilar queries + no_memories = await semantic_memory.query("The king of England") + assert len(no_memories.results) == 0 + + # match multiple relevant memories + content_2 = MemoryContent( + content="I also like mangos and pineapples.", + mime_type=MemoryMimeType.TEXT, + metadata={"description": "additional info"}, + ) + await semantic_memory.add(content_2) + + memories = await semantic_memory.query("Fruits that I like.") + assert len(memories.results) == 2 + assert memories.results[0].metadata == {} + assert memories.results[1].metadata == {"description": "additional info"} + + +@pytest.mark.asyncio +@pytest.mark.skipif(not redis_available(), reason="Redis instance not available locally") +async def test_clear(semantic_memory: RedisMemory) -> None: + content = MemoryContent(content="I enjoy fruits like apples, oranges, and bananas.", mime_type=MemoryMimeType.TEXT) + await semantic_memory.add(content) + + # find matches with a similar query + memories = await semantic_memory.query("Fruits that I like.") + assert len(memories.results) == 1 + + await semantic_memory.clear() + # don't return anything for dissimilar queries + no_memories = await semantic_memory.query("Fruits that I like.") + assert len(no_memories.results) == 0 + + +@pytest.mark.asyncio +@pytest.mark.skipif(not redis_available(), reason="Redis instance not available locally") +async def test_close(semantic_config: RedisMemoryConfig) -> None: + semantic_memory = RedisMemory(semantic_config) + content = MemoryContent(content="This sentence should be deleted.", mime_type=MemoryMimeType.TEXT) + await semantic_memory.add(content) + + await semantic_memory.close() + + with pytest.raises(RedisSearchError): + _ = await semantic_memory.query("This query should fail.") + + +## INTEGRATION TESTS ## +@pytest.mark.asyncio +@pytest.mark.skipif(not redis_available(), reason="Redis instance not available locally") +async def test_basic_workflow(semantic_config: RedisMemoryConfig) -> None: + """Test basic memory operations with semantic memory.""" + memory = RedisMemory(config=semantic_config) + await memory.clear() + + await memory.add( + MemoryContent( + content="Virginia Tech is the best engineering university in the state.", + mime_type=MemoryMimeType.TEXT, + metadata={"topic": "higher education", "department": "engineering"}, + ) + ) + + results = await memory.query("Which engineering university should I attend?") + assert len(results.results) == 1 + assert any("engineering" in str(r.content) for r in results.results) + assert all(isinstance(r.metadata, dict) for r in results.results if r.metadata) + + await memory.close() + + +@pytest.mark.asyncio +@pytest.mark.skipif(not redis_available(), reason="Redis instance not available locally") +async def test_content_types(semantic_memory: RedisMemory) -> None: + """Test different content types with semantic memory.""" + await semantic_memory.clear() + + # Test text content + text_content = MemoryContent(content="Simple text content for testing", mime_type=MemoryMimeType.TEXT) + await semantic_memory.add(text_content) + + # Query for text content + results = await semantic_memory.query("simple text content") + assert len(results.results) > 0 + assert any("Simple text content" in str(r.content) for r in results.results) + + # Test JSON content + json_data = {"key": "value", "number": 42} + json_content = MemoryContent(content=json_data, mime_type=MemoryMimeType.JSON) + with pytest.raises(NotImplementedError): + await semantic_memory.add(json_content) diff --git a/python/uv.lock b/python/uv.lock index 8c40183ab20b..6b95fddb7540 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -668,6 +668,9 @@ openai = [ redis = [ { name = "redis" }, ] +redisvl = [ + { name = "redisvl" }, +] rich = [ { name = "rich" }, ] @@ -782,6 +785,7 @@ requires-dist = [ { name = "playwright", marker = "extra == 'magentic-one'", specifier = ">=1.48.0" }, { name = "playwright", marker = "extra == 'web-surfer'", specifier = ">=1.48.0" }, { name = "redis", marker = "extra == 'redis'", specifier = ">=5.2.1" }, + { name = "redisvl", marker = "extra == 'redisvl'", specifier = ">=0.6.0" }, { name = "requests", marker = "extra == 'docker-jupyter-executor'", specifier = ">=2.32.3" }, { name = "rich", marker = "extra == 'rich'", specifier = ">=13.9.4" }, { name = "semantic-kernel", marker = "extra == 'semantic-kernel-core'", specifier = ">=1.17.1" }, @@ -800,7 +804,7 @@ requires-dist = [ { name = "unidiff", marker = "extra == 'canvas'", specifier = ">=0.7.5" }, { name = "websockets", marker = "extra == 'docker-jupyter-executor'", specifier = ">=15.0.1" }, ] -provides-extras = ["anthropic", "langchain", "azure", "docker", "ollama", "openai", "file-surfer", "llama-cpp", "graphrag", "chromadb", "mem0", "mem0-local", "web-surfer", "magentic-one", "video-surfer", "diskcache", "redis", "grpc", "jupyter-executor", "docker-jupyter-executor", "task-centric-memory", "semantic-kernel-core", "gemini", "semantic-kernel-google", "semantic-kernel-hugging-face", "semantic-kernel-mistralai", "semantic-kernel-ollama", "semantic-kernel-onnx", "semantic-kernel-anthropic", "semantic-kernel-pandas", "semantic-kernel-aws", "semantic-kernel-dapr", "http-tool", "semantic-kernel-all", "rich", "mcp", "canvas"] +provides-extras = ["anthropic", "langchain", "azure", "docker", "ollama", "openai", "file-surfer", "llama-cpp", "graphrag", "chromadb", "mem0", "mem0-local", "web-surfer", "magentic-one", "video-surfer", "diskcache", "redis", "grpc", "jupyter-executor", "docker-jupyter-executor", "task-centric-memory", "semantic-kernel-core", "gemini", "semantic-kernel-google", "semantic-kernel-hugging-face", "semantic-kernel-mistralai", "semantic-kernel-ollama", "semantic-kernel-onnx", "semantic-kernel-anthropic", "semantic-kernel-pandas", "semantic-kernel-aws", "semantic-kernel-dapr", "http-tool", "semantic-kernel-all", "rich", "mcp", "canvas", "redisvl"] [package.metadata.requires-dev] dev = [ @@ -3124,6 +3128,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898, upload-time = "2023-06-16T21:01:28.466Z" }, ] +[[package]] +name = "jsonpath-ng" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ply" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/86/08646239a313f895186ff0a4573452038eed8c86f54380b3ebac34d32fb2/jsonpath-ng-1.7.0.tar.gz", hash = "sha256:f6f5f7fd4e5ff79c785f1573b394043b39849fb2bb47bcead935d12b00beab3c", size = 37838, upload-time = "2024-10-11T15:41:42.404Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/5a/73ecb3d82f8615f32ccdadeb9356726d6cae3a4bbc840b437ceb95708063/jsonpath_ng-1.7.0-py3-none-any.whl", hash = "sha256:f3d7f9e848cba1b6da28c55b1c26ff915dc9e0b1ba7e752a53d6da8d5cbd00b6", size = 30105, upload-time = "2024-11-20T17:58:30.418Z" }, +] + [[package]] name = "jsonpath-python" version = "1.0.6" @@ -4212,7 +4228,7 @@ wheels = [ [[package]] name = "mcp" -version = "1.11.0" +version = "1.12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -4227,9 +4243,9 @@ dependencies = [ { name = "starlette" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3a/f5/9506eb5578d5bbe9819ee8ba3198d0ad0e2fbe3bab8b257e4131ceb7dfb6/mcp-1.11.0.tar.gz", hash = "sha256:49a213df56bb9472ff83b3132a4825f5c8f5b120a90246f08b0dac6bedac44c8", size = 406907, upload-time = "2025-07-10T16:41:09.388Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/5a/16cef13b2e60d5f865fbc96372efb23dc8b0591f102dd55003b4ae62f9b1/mcp-1.12.1.tar.gz", hash = "sha256:d1d0bdeb09e4b17c1a72b356248bf3baf75ab10db7008ef865c4afbeb0eb810e", size = 425768, upload-time = "2025-07-22T16:51:41.66Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/92/9c/c9ca79f9c512e4113a5d07043013110bb3369fc7770040c61378c7fbcf70/mcp-1.11.0-py3-none-any.whl", hash = "sha256:58deac37f7483e4b338524b98bc949b7c2b7c33d978f5fafab5bde041c5e2595", size = 155880, upload-time = "2025-07-10T16:41:07.935Z" }, + { url = "https://files.pythonhosted.org/packages/b9/04/9a967a575518fc958bda1e34a52eae0c7f6accf3534811914fdaf57b0689/mcp-1.12.1-py3-none-any.whl", hash = "sha256:34147f62891417f8b000c39718add844182ba424c8eb2cea250b4267bda4b08b", size = 158463, upload-time = "2025-07-22T16:51:40.086Z" }, ] [[package]] @@ -4287,6 +4303,29 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/58/e7/7147c75c383a975c58c33f8e7ee7dbbb0e7390fbcb1ecd321f63e4c73efd/mistralai-1.5.0-py3-none-any.whl", hash = "sha256:9372537719f87bd6f9feef4747d0bf1f4fbe971f8c02945ca4b4bf3c94571c97", size = 271559, upload-time = "2025-01-28T15:50:31.031Z" }, ] +[[package]] +name = "ml-dtypes" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/32/49/6e67c334872d2c114df3020e579f3718c333198f8312290e09ec0216703a/ml_dtypes-0.5.1.tar.gz", hash = "sha256:ac5b58559bb84a95848ed6984eb8013249f90b6bab62aa5acbad876e256002c9", size = 698772, upload-time = "2025-01-07T03:34:55.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/88/11ebdbc75445eeb5b6869b708a0d787d1ed812ff86c2170bbfb95febdce1/ml_dtypes-0.5.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:bd73f51957949069573ff783563486339a9285d72e2f36c18e0c1aa9ca7eb190", size = 671450, upload-time = "2025-01-07T03:33:52.724Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a4/9321cae435d6140f9b0e7af8334456a854b60e3a9c6101280a16e3594965/ml_dtypes-0.5.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:810512e2eccdfc3b41eefa3a27402371a3411453a1efc7e9c000318196140fed", size = 4621075, upload-time = "2025-01-07T03:33:54.878Z" }, + { url = "https://files.pythonhosted.org/packages/16/d8/4502e12c6a10d42e13a552e8d97f20198e3cf82a0d1411ad50be56a5077c/ml_dtypes-0.5.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:141b2ea2f20bb10802ddca55d91fe21231ef49715cfc971998e8f2a9838f3dbe", size = 4738414, upload-time = "2025-01-07T03:33:57.709Z" }, + { url = "https://files.pythonhosted.org/packages/6b/7e/bc54ae885e4d702e60a4bf50aa9066ff35e9c66b5213d11091f6bffb3036/ml_dtypes-0.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:26ebcc69d7b779c8f129393e99732961b5cc33fcff84090451f448c89b0e01b4", size = 209718, upload-time = "2025-01-07T03:34:00.585Z" }, + { url = "https://files.pythonhosted.org/packages/c9/fd/691335926126bb9beeb030b61a28f462773dcf16b8e8a2253b599013a303/ml_dtypes-0.5.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:023ce2f502efd4d6c1e0472cc58ce3640d051d40e71e27386bed33901e201327", size = 671448, upload-time = "2025-01-07T03:34:03.153Z" }, + { url = "https://files.pythonhosted.org/packages/ff/a6/63832d91f2feb250d865d069ba1a5d0c686b1f308d1c74ce9764472c5e22/ml_dtypes-0.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7000b6e4d8ef07542c05044ec5d8bbae1df083b3f56822c3da63993a113e716f", size = 4625792, upload-time = "2025-01-07T03:34:04.981Z" }, + { url = "https://files.pythonhosted.org/packages/cc/2a/5421fd3dbe6eef9b844cc9d05f568b9fb568503a2e51cb1eb4443d9fc56b/ml_dtypes-0.5.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c09526488c3a9e8b7a23a388d4974b670a9a3dd40c5c8a61db5593ce9b725bab", size = 4743893, upload-time = "2025-01-07T03:34:08.333Z" }, + { url = "https://files.pythonhosted.org/packages/60/30/d3f0fc9499a22801219679a7f3f8d59f1429943c6261f445fb4bfce20718/ml_dtypes-0.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:15ad0f3b0323ce96c24637a88a6f44f6713c64032f27277b069f285c3cf66478", size = 209712, upload-time = "2025-01-07T03:34:12.182Z" }, + { url = "https://files.pythonhosted.org/packages/47/56/1bb21218e1e692506c220ffabd456af9733fba7aa1b14f73899979f4cc20/ml_dtypes-0.5.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:6f462f5eca22fb66d7ff9c4744a3db4463af06c49816c4b6ac89b16bfcdc592e", size = 670372, upload-time = "2025-01-07T03:34:15.258Z" }, + { url = "https://files.pythonhosted.org/packages/20/95/d8bd96a3b60e00bf31bd78ca4bdd2d6bbaf5acb09b42844432d719d34061/ml_dtypes-0.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f76232163b5b9c34291b54621ee60417601e2e4802a188a0ea7157cd9b323f4", size = 4635946, upload-time = "2025-01-07T03:34:20.412Z" }, + { url = "https://files.pythonhosted.org/packages/08/57/5d58fad4124192b1be42f68bd0c0ddaa26e44a730ff8c9337adade2f5632/ml_dtypes-0.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad4953c5eb9c25a56d11a913c2011d7e580a435ef5145f804d98efa14477d390", size = 4694804, upload-time = "2025-01-07T03:34:23.608Z" }, + { url = "https://files.pythonhosted.org/packages/38/bc/c4260e4a6c6bf684d0313308de1c860467275221d5e7daf69b3fcddfdd0b/ml_dtypes-0.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:9626d0bca1fb387d5791ca36bacbba298c5ef554747b7ebeafefb4564fc83566", size = 210853, upload-time = "2025-01-07T03:34:26.027Z" }, +] + [[package]] name = "mmh3" version = "5.1.0" @@ -5576,6 +5615,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, ] +[[package]] +name = "ply" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/69/882ee5c9d017149285cab114ebeab373308ef0f874fcdac9beb90e0ac4da/ply-3.11.tar.gz", hash = "sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3", size = 159130, upload-time = "2018-02-15T19:01:31.097Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/58/35da89ee790598a0700ea49b2a66594140f44dec458c07e8e3d4979137fc/ply-3.11-py2.py3-none-any.whl", hash = "sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce", size = 49567, upload-time = "2018-02-15T19:01:27.172Z" }, +] + [[package]] name = "poethepoet" version = "0.32.2" @@ -6332,6 +6380,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8a/a3/c69806f30dd81df5a99d592e7db4c930c3a9b098555aa97b0eb866b20b11/python_socketio-5.12.1-py3-none-any.whl", hash = "sha256:24a0ea7cfff0e021eb28c68edbf7914ee4111bdf030b95e4d250c4dc9af7a386", size = 76947, upload-time = "2024-12-29T20:11:48.876Z" }, ] +[[package]] +name = "python-ulid" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/db/e5e67aeca9c2420cb91f94007f30693cc3628ae9783a565fd33ffb3fbfdd/python_ulid-3.0.0.tar.gz", hash = "sha256:e50296a47dc8209d28629a22fc81ca26c00982c78934bd7766377ba37ea49a9f", size = 28822, upload-time = "2024-10-11T15:31:55.475Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/4e/cc2ba2c0df2589f35a4db8473b8c2ba9bbfc4acdec4a94f1c78934d2350f/python_ulid-3.0.0-py3-none-any.whl", hash = "sha256:e4c4942ff50dbd79167ad01ac725ec58f924b4018025ce22c858bfcff99a5e31", size = 11194, upload-time = "2024-10-11T15:31:54.368Z" }, +] + [[package]] name = "pytz" version = "2024.2" @@ -6475,6 +6532,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3c/5f/fa26b9b2672cbe30e07d9a5bdf39cf16e3b80b42916757c5f92bca88e4ba/redis-5.2.1-py3-none-any.whl", hash = "sha256:ee7e1056b9aea0f04c6c2ed59452947f34c4940ee025f5dd83e6a6418b6989e4", size = 261502, upload-time = "2024-12-06T09:50:39.656Z" }, ] +[[package]] +name = "redisvl" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonpath-ng" }, + { name = "ml-dtypes" }, + { name = "numpy" }, + { name = "pydantic" }, + { name = "python-ulid" }, + { name = "pyyaml" }, + { name = "redis" }, + { name = "tenacity" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/88/be/22d3f21d5cf1caa96527cb9c61950c172b23342d8e6acae570882da05c75/redisvl-0.8.0.tar.gz", hash = "sha256:00645cf126039ee4d734a1ff273cc4e8fea59118f7790625eeff510fce08b0d4", size = 551876, upload-time = "2025-06-24T13:30:38.207Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/74/484d1adefe84ab4eb3cd77bb6aa5dc7a1d3920bb0d5ca281bcceedf89ad4/redisvl-0.8.0-py3-none-any.whl", hash = "sha256:365c31819224b3e4e9acca1ed2ac9eed347d4ee4ca8d822010dbd51a8b725705", size = 152348, upload-time = "2025-06-24T13:30:36.548Z" }, +] + [[package]] name = "referencing" version = "0.36.2"