555e282cc4
pi-agent-plugin checks / lint (push) Has been cancelled
pi-agent-plugin checks / test (20) (push) Has been cancelled
pi-agent-plugin checks / test (22) (push) Has been cancelled
pi-agent-plugin checks / build (push) Has been cancelled
TypeScript SDK CI / check_changes (push) Has been cancelled
TypeScript SDK CI / changelog_check (push) Has been cancelled
ci / changelog_check (push) Has been cancelled
ci / check_changes (push) Has been cancelled
ci / build_mem0 (3.10) (push) Has been cancelled
ci / build_mem0 (3.11) (push) Has been cancelled
ci / build_mem0 (3.12) (push) Has been cancelled
CLI Node CI / lint (push) Has been cancelled
CLI Node CI / test (20) (push) Has been cancelled
CLI Node CI / test (22) (push) Has been cancelled
CLI Node CI / build (push) Has been cancelled
CLI Python CI / lint (push) Has been cancelled
CLI Python CI / test (3.10) (push) Has been cancelled
CLI Python CI / test (3.11) (push) Has been cancelled
CLI Python CI / test (3.12) (push) Has been cancelled
CLI Python CI / build (push) Has been cancelled
openclaw checks / lint (push) Has been cancelled
openclaw checks / test (20) (push) Has been cancelled
openclaw checks / test (22) (push) Has been cancelled
openclaw checks / build (push) Has been cancelled
opencode-plugin checks / build (push) Has been cancelled
TypeScript SDK CI / build_ts_sdk (20) (push) Has been cancelled
TypeScript SDK CI / build_ts_sdk (22) (push) Has been cancelled
TypeScript SDK CI / integration_ts_sdk (20) (push) Has been cancelled
TypeScript SDK CI / integration_ts_sdk (22) (push) Has been cancelled
386 lines
16 KiB
Python
386 lines
16 KiB
Python
|
|
"""
|
|
GitHub Repository Research Agent with Persistent Memory
|
|
|
|
This example demonstrates how to build an AI agent with persistent memory using:
|
|
- Mem0 for memory orchestration and lifecycle management
|
|
- Amazon ElastiCache for Valkey for high-performance vector similarity search
|
|
- Amazon Neptune Analytics for graph-based relationship storage and traversal
|
|
- Strands Agents framework for agent orchestration and tool management
|
|
|
|
The agent can research GitHub repositories, store information in both vector and graph memory,
|
|
and retrieve relevant information for future queries with significant performance improvements.
|
|
|
|
For detailed explanation and architecture, see the blog posts:
|
|
- AWS Blog: https://aws.amazon.com/blogs/database/build-persistent-memory-for-agentic-ai-applications-with-mem0-open-source-amazon-elasticache-for-valkey-and-amazon-neptune-analytics/
|
|
- Mem0 Blog: https://mem0.ai/blog/build-persistent-memory-for-agentic-ai-applications-with-mem0-open-source-amazon-elasticache-for-valkey-and-amazon-neptune-analytics
|
|
|
|
Prerequisites:
|
|
1. ElastiCache cluster running Valkey 8.2+ with vector search support
|
|
2. Neptune Analytics graph with vector indexes and public access
|
|
3. AWS credentials with access to Bedrock, ElastiCache, and Neptune
|
|
|
|
Environment Variables:
|
|
- AWS_REGION=us-east-1
|
|
- AWS_ACCESS_KEY_ID=your_aws_access_key
|
|
- AWS_SECRET_ACCESS_KEY=your_aws_secret_key
|
|
- NEPTUNE_ENDPOINT=neptune-graph://your-graph-id (optional, defaults to g-6n3v83av7a)
|
|
- VALKEY_URL=valkey://your-cluster-endpoint:6379 (optional, defaults to localhost:6379)
|
|
|
|
Installation:
|
|
pip install strands-agents strands-agents-tools mem0ai streamlit
|
|
|
|
Usage:
|
|
streamlit run agent1.py
|
|
|
|
Example queries:
|
|
1. "What is the URL for the project mem0 and its most important metrics?"
|
|
2. "Find the top contributors for Mem0 and store this information in a graph"
|
|
3. "Who works in the core packages and the SDK updates?"
|
|
"""
|
|
|
|
import os
|
|
|
|
import streamlit as st
|
|
from strands import Agent, tool
|
|
from strands_tools import http_request
|
|
|
|
from mem0.memory.main import Memory
|
|
|
|
|
|
config = {
|
|
"embedder": {
|
|
"provider": "aws_bedrock",
|
|
"config": {
|
|
"model": "amazon.titan-embed-text-v2:0"
|
|
}
|
|
},
|
|
"llm": {
|
|
"provider": "aws_bedrock",
|
|
"config": {
|
|
"model": "us.anthropic.claude-sonnet-4-20250514-v1:0",
|
|
"max_tokens": 512,
|
|
"temperature": 0.5
|
|
}
|
|
},
|
|
"vector_store": {
|
|
"provider": "valkey",
|
|
"config": {
|
|
"collection_name": "blogpost1",
|
|
"embedding_model_dims": 1024,
|
|
"valkey_url": os.getenv("VALKEY_URL", "valkey://localhost:6379"),
|
|
"index_type": "hnsw",
|
|
"hnsw_m": 32,
|
|
"hnsw_ef_construction": 400,
|
|
"hnsw_ef_runtime": 40
|
|
}
|
|
}
|
|
,
|
|
"graph_store": {
|
|
"provider": "neptune",
|
|
"config": {
|
|
"endpoint": os.getenv("NEPTUNE_ENDPOINT", "neptune-graph://g-6n3v83av7a"),
|
|
},
|
|
}
|
|
|
|
}
|
|
|
|
m = Memory.from_config(config)
|
|
|
|
def get_assistant_response(messages):
|
|
"""
|
|
Send the entire conversation thread to the agent in the proper Strands message format.
|
|
|
|
Args:
|
|
messages: List of message dictionaries with 'role' and 'content' keys
|
|
|
|
Returns:
|
|
Agent response result
|
|
"""
|
|
# Format messages for Strands Agent
|
|
formatted_messages = []
|
|
|
|
for message in messages:
|
|
formatted_message = {
|
|
"role": message["role"],
|
|
"content": [{"text": message["content"]}]
|
|
}
|
|
formatted_messages.append(formatted_message)
|
|
|
|
# Send the properly formatted message list to the agent
|
|
result = agent(formatted_messages)
|
|
return result
|
|
|
|
|
|
|
|
@tool
|
|
def store_memory_tool(information: str, user_id: str = "user", category: str = "conversation") -> str:
|
|
"""
|
|
Store standalone facts, preferences, descriptions, or unstructured information in vector-based memory.
|
|
|
|
Use this tool for:
|
|
- User preferences ("User prefers dark mode", "Alice likes coffee")
|
|
- Standalone facts ("The meeting was productive", "Project deadline is next Friday")
|
|
- Descriptions ("Alice is a software engineer", "The office is located downtown")
|
|
- General context that doesn't involve relationships between entities
|
|
|
|
Do NOT use for relationship information - use store_graph_memory_tool instead.
|
|
|
|
Args:
|
|
information: The standalone information to store in vector memory
|
|
user_id: User identifier for memory storage (default: "user")
|
|
category: Category for organizing memories (e.g., "preferences", "projects", "facts")
|
|
|
|
Returns:
|
|
Confirmation message about memory storage
|
|
"""
|
|
try:
|
|
# Create a simple message format for mem0 vector storage
|
|
memory_message = [{"role": "user", "content": information}]
|
|
m.add(memory_message, user_id=user_id, metadata={"category": category, "storage_type": "vector"})
|
|
return f"✅ Successfully stored information in vector memory: '{information[:100]}...'"
|
|
except Exception as e:
|
|
print(f"Error storing vector memory: {e}")
|
|
return f"❌ Failed to store vector memory: {str(e)}"
|
|
|
|
@tool
|
|
def store_graph_memory_tool(information: str, user_id: str = "user", category: str = "relationships") -> str:
|
|
"""
|
|
Store relationship-based information, connections, or structured data in graph-based memory.
|
|
|
|
In memory we will keep the information about projects and repositories we've learned about, including its URL and key metrics
|
|
|
|
Use this tool for:
|
|
- Relationships between people ("John manages Sarah", "Alice works with Bob")
|
|
- Entity connections ("Project A depends on Project B", "Alice is part of Team X")
|
|
- Hierarchical information ("Sarah reports to John", "Department A contains Team B")
|
|
- Network connections ("Alice knows Bob through work", "Company X partners with Company Y")
|
|
- Temporal sequences ("Event A led to Event B", "Meeting A was scheduled after Meeting B")
|
|
- Any information where entities are connected to each other
|
|
|
|
Use this instead of store_memory_tool when the information describes relationships or connections.
|
|
|
|
Args:
|
|
information: The relationship or connection information to store in graph memory
|
|
user_id: User identifier for memory storage (default: "user")
|
|
category: Category for organizing memories (default: "relationships")
|
|
|
|
Returns:
|
|
Confirmation message about graph memory storage
|
|
"""
|
|
try:
|
|
memory_message = [{"role": "user", "content": f"RELATIONSHIP: {information}"}]
|
|
m.add(memory_message, user_id=user_id, metadata={"category": category, "storage_type": "graph"})
|
|
return f"✅ Successfully stored relationship in graph memory: '{information[:100]}...'"
|
|
except Exception as e:
|
|
return f"❌ Failed to store graph memory: {str(e)}"
|
|
|
|
@tool
|
|
def search_memory_tool(query: str, user_id: str = "user") -> str:
|
|
"""
|
|
Search through vector-based memories using semantic similarity to find relevant standalone information.
|
|
|
|
In memory we will keep the information about projects and repositories we've learned about, including its URL and key metrics
|
|
|
|
Use this tool for:
|
|
- Finding similar concepts or topics ("What do we know about AI?")
|
|
- Semantic searches ("Find information about preferences")
|
|
- Content-based searches ("What was said about the project deadline?")
|
|
- General information retrieval that doesn't involve relationships
|
|
|
|
For relationship-based queries, use search_graph_memory_tool instead.
|
|
|
|
Args:
|
|
query: Search query to find semantically similar memories
|
|
user_id: User identifier to search memories for (default: "user")
|
|
|
|
Returns:
|
|
Relevant vector memories found or message if none found
|
|
"""
|
|
try:
|
|
results = m.search(query, user_id=user_id)
|
|
|
|
if isinstance(results, dict) and 'results' in results:
|
|
memory_list = results['results']
|
|
if memory_list:
|
|
memory_texts = []
|
|
for i, result in enumerate(memory_list, 1):
|
|
memory_text = result.get('memory', 'No memory text available')
|
|
metadata = result.get('metadata', {})
|
|
category = metadata.get('category', 'unknown') if isinstance(metadata, dict) else 'unknown'
|
|
storage_type = metadata.get('storage_type', 'unknown') if isinstance(metadata, dict) else 'unknown'
|
|
score = result.get('score', 0)
|
|
memory_texts.append(f"{i}. [{category}|{storage_type}] {memory_text} (score: {score:.3f})")
|
|
|
|
return f"🔍 Found {len(memory_list)} relevant vector memories:\n" + "\n".join(memory_texts)
|
|
else:
|
|
return f"🔍 No vector memories found for query: '{query}'"
|
|
else:
|
|
return f"🔍 No vector memories found for query: '{query}'"
|
|
except Exception as e:
|
|
print(f"Error searching vector memories: {e}")
|
|
return f"❌ Failed to search vector memories: {str(e)}"
|
|
|
|
@tool
|
|
def search_graph_memory_tool(query: str, user_id: str = "user") -> str:
|
|
"""
|
|
Search through graph-based memories to find relationship and connection information.
|
|
|
|
Use this tool for:
|
|
- Finding connections between entities ("How is Alice related to the project?")
|
|
- Discovering relationships ("Who works with whom?")
|
|
- Path-based queries ("What connects concept A to concept B?")
|
|
- Hierarchical questions ("Who reports to whom?")
|
|
- Network analysis ("What are all the connections to this person/entity?")
|
|
- Relationship-based searches ("Find all partnerships", "Show team structures")
|
|
|
|
This searches specifically for relationship and connection information stored in the graph.
|
|
|
|
Args:
|
|
query: Search query focused on relationships and connections
|
|
user_id: User identifier to search memories for (default: "user")
|
|
|
|
Returns:
|
|
Relevant graph memories and relationships found or message if none found
|
|
"""
|
|
try:
|
|
graph_query = f"relationships connections {query}"
|
|
results = m.search(graph_query, user_id=user_id)
|
|
|
|
if isinstance(results, dict) and 'results' in results:
|
|
memory_list = results['results']
|
|
if memory_list:
|
|
memory_texts = []
|
|
relationship_count = 0
|
|
for i, result in enumerate(memory_list, 1):
|
|
memory_text = result.get('memory', 'No memory text available')
|
|
metadata = result.get('metadata', {})
|
|
category = metadata.get('category', 'unknown') if isinstance(metadata, dict) else 'unknown'
|
|
storage_type = metadata.get('storage_type', 'unknown') if isinstance(metadata, dict) else 'unknown'
|
|
score = result.get('score', 0)
|
|
|
|
# Prioritize graph/relationship memories
|
|
if 'RELATIONSHIP:' in memory_text or storage_type == 'graph' or category == 'relationships':
|
|
relationship_count += 1
|
|
memory_texts.append(f"{i}. 🔗 [{category}|{storage_type}] {memory_text} (score: {score:.3f})")
|
|
else:
|
|
memory_texts.append(f"{i}. [{category}|{storage_type}] {memory_text} (score: {score:.3f})")
|
|
|
|
result_summary = f"🔗 Found {len(memory_list)} relevant memories ({relationship_count} relationship-focused):\n"
|
|
return result_summary + "\n".join(memory_texts)
|
|
else:
|
|
return f"🔗 No graph memories found for query: '{query}'"
|
|
else:
|
|
return f"🔗 No graph memories found for query: '{query}'"
|
|
except Exception as e:
|
|
print(f"Error searching graph memories: {e}")
|
|
return f"Failed to search graph memories: {str(e)}"
|
|
|
|
@tool
|
|
def get_all_memories_tool(user_id: str = "user") -> str:
|
|
"""
|
|
Retrieve all stored memories for a user to get comprehensive context.
|
|
Use this tool when you need to understand the full history of what has been remembered
|
|
about a user or when you need comprehensive context for decision making.
|
|
|
|
Args:
|
|
user_id: User identifier to get all memories for (default: "user")
|
|
|
|
Returns:
|
|
All memories for the user or message if none found
|
|
"""
|
|
try:
|
|
all_memories = m.get_all(user_id=user_id)
|
|
|
|
if isinstance(all_memories, dict) and 'results' in all_memories:
|
|
memory_list = all_memories['results']
|
|
if memory_list:
|
|
memory_texts = []
|
|
for i, memory in enumerate(memory_list, 1):
|
|
memory_text = memory.get('memory', 'No memory text available')
|
|
metadata = memory.get('metadata', {})
|
|
category = metadata.get('category', 'unknown') if isinstance(metadata, dict) else 'unknown'
|
|
created_at = memory.get('created_at', 'unknown time')
|
|
memory_texts.append(f"{i}. [{category}] {memory_text} (stored: {created_at})")
|
|
|
|
return f"📚 Found {len(memory_list)} total memories:\n" + "\n".join(memory_texts)
|
|
else:
|
|
return f"📚 No memories found for user: '{user_id}'"
|
|
else:
|
|
return f"📚 No memories found for user: '{user_id}'"
|
|
except Exception as e:
|
|
print(f"Error retrieving all memories: {e}")
|
|
return f"❌ Failed to retrieve memories: {str(e)}"
|
|
|
|
# Initialize agent with tools (must be after tool definitions)
|
|
agent = Agent(tools=[http_request, store_memory_tool, store_graph_memory_tool, search_memory_tool, search_graph_memory_tool, get_all_memories_tool])
|
|
|
|
def store_memory(messages, user_id="alice", category="conversation"):
|
|
"""
|
|
Store the conversation thread in mem0 memory.
|
|
|
|
Args:
|
|
messages: List of message dictionaries with 'role' and 'content' keys
|
|
user_id: User identifier for memory storage
|
|
category: Category for organizing memories
|
|
|
|
Returns:
|
|
Memory storage result
|
|
"""
|
|
try:
|
|
result = m.add(messages, user_id=user_id, metadata={"category": category})
|
|
#print(f"Memory stored successfully: {result}")
|
|
return result
|
|
except Exception:
|
|
#print(f"Error storing memory: {e}")
|
|
return None
|
|
|
|
def get_agent_metrics(result):
|
|
agent_metrics = f"I've used {result.metrics.cycle_count} cycle counts," + f" {result.metrics.accumulated_usage['totalTokens']} tokens" + f", and {sum(result.metrics.cycle_durations):.2f} seconds finding that answer"
|
|
print(agent_metrics)
|
|
return agent_metrics
|
|
|
|
st.title("Repo Research Agent")
|
|
|
|
|
|
# Initialize chat history
|
|
if "messages" not in st.session_state:
|
|
st.session_state.messages = []
|
|
|
|
# Create a container with the chat frame styling
|
|
with st.container():
|
|
st.markdown('<div class="chat-frame">', unsafe_allow_html=True)
|
|
|
|
# Display chat messages from history on app rerun
|
|
for message in st.session_state.messages:
|
|
with st.chat_message(message["role"]):
|
|
st.markdown(message["content"])
|
|
|
|
st.markdown('</div>', unsafe_allow_html=True)
|
|
|
|
# React to user input
|
|
if prompt := st.chat_input("Send a message"):
|
|
# Display user message in chat message container
|
|
with st.chat_message("user"):
|
|
st.markdown(prompt)
|
|
# Add user message to chat history
|
|
st.session_state.messages.append({"role": "user", "content": prompt})
|
|
|
|
# Let the agent decide autonomously when to store memories
|
|
# Pass the entire conversation thread to the agent
|
|
response = get_assistant_response(st.session_state.messages)
|
|
|
|
# Extract the text content from the AgentResult
|
|
response_text = str(response)
|
|
|
|
# Display assistant response in chat message container
|
|
with st.chat_message("assistant"):
|
|
st.markdown(response_text)
|
|
# Add assistant response to chat history (store as string, not AgentResult)
|
|
st.session_state.messages.append({"role": "assistant", "content": response_text})
|
|
|
|
tokenusage = get_agent_metrics(response)
|
|
# Add assistant token usage to chat history
|
|
with st.chat_message("assistant"):
|
|
st.markdown(tokenusage)
|