Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "powermem"
version = "1.1.0"
version = "1.2.0"
description = "Intelligent Memory System - Persistent memory layer for LLM applications"
readme = "README.md"
license = {text = "Apache-2.0"}
Expand Down
10 changes: 10 additions & 0 deletions src/powermem/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@
# Import configuration loader
from .config_loader import load_config_from_env, create_config, validate_config, auto_config

# Import intelligence extensions
from .intelligence.search_query_optimizer import SearchQueryOptimizer
from .intelligence.experience_query_rewriter import ExperienceQueryRewriter
from .intelligence.experience_manager import ExperienceManager
from .intelligence.content_reviewer import ContentReviewer


def create_memory(
config: Any = None,
Expand Down Expand Up @@ -295,4 +301,8 @@ def _deprecated_memory_from_config(cls, config=None, **kwargs):
"create_memory",
"from_config",
"auto_config",
"SearchQueryOptimizer",
"ExperienceQueryRewriter",
"ExperienceManager",
"ContentReviewer",
]
4 changes: 2 additions & 2 deletions src/powermem/agent/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ def _get_default_multi_agent_config(self) -> Dict[str, Any]:
'default_collaboration_level': default_collaboration_level,
'default_access_permission': default_access_permission,
'default_permissions': {
'owner': ['read', 'write', 'delete', 'admin'],
'owner': ['read', 'write', 'delete', 'share', 'admin'],
'collaborator': ['read', 'write'],
'viewer': ['read']
},
Expand Down Expand Up @@ -760,7 +760,7 @@ def share_memory(self, memory_id: str, from_agent: str, to_agents: List[str], pe
Returns:
Dictionary containing sharing result
"""
if self.mode not in ['multi_agent', 'hybrid']:
if self.mode not in ['multi_agent', 'hybrid', 'auto']:
raise RuntimeError(f"share_memory() not supported in {self.mode} mode")

if hasattr(self._agent_manager, 'share_memory'):
Expand Down
145 changes: 141 additions & 4 deletions src/powermem/agent/implementations/multi_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -546,7 +546,7 @@ def get_memories(
try:
scope = MemoryScope(scope_str) if isinstance(scope_str, str) else scope_str
except (ValueError, TypeError):
scope = MemoryScope.AGENT # Default scope
scope = MemoryScope.AGENT_GROUP

try:
memory_type = MemoryType(memory_type_str) if isinstance(memory_type_str, str) else memory_type_str
Expand Down Expand Up @@ -627,6 +627,69 @@ def get_memories(
else:
logger.debug(f"Scope access denied for agent {agent_id} on memory {memory_id}")

# Memories shared *to* this agent: primary DB query filters by agent_id and omits the owner's row.
# share_memory() grants READ on the canonical memory id — load those rows by id.
try:
filter_uid = filters.get("user_id") if filters else None
seen_ids = {str(m.get("id")) for m in accessible_memories if m.get("id") is not None}
for mem_key, agents_map in self.permission_controller.memory_permissions.items():
if agent_id not in agents_map:
continue
if AccessPermission.READ not in agents_map[agent_id]:
continue
try:
mid_int = int(mem_key) if not isinstance(mem_key, int) else mem_key
except (TypeError, ValueError):
continue
if str(mid_int) in seen_ids:
continue
raw_get = self._memory_instance.get(mid_int, user_id=None, agent_id=None)
if not raw_get:
continue
if filter_uid is not None and raw_get.get("user_id") != filter_uid:
continue
if raw_get.get("agent_id") == agent_id:
continue
db_memory = raw_get
memory_id = db_memory.get("id", mid_int)
content = db_memory.get("memory") or db_memory.get("content") or db_memory.get("document", "")
memory_data = {
"id": memory_id,
"content": content,
"agent_id": db_memory.get("agent_id", agent_id),
"user_id": db_memory.get("user_id"),
"run_id": db_memory.get("run_id"),
"metadata": db_memory.get("metadata", {}),
"created_at": db_memory.get("created_at"),
"updated_at": db_memory.get("updated_at"),
"access_count": 0,
"last_accessed": None,
}
metadata = memory_data.get("metadata", {})
scope_str = metadata.get("scope") or metadata.get("agent", {}).get("scope", "agent")
memory_type_str = metadata.get("memory_type") or metadata.get("agent", {}).get("memory_type", "working")
try:
scope = MemoryScope(scope_str) if isinstance(scope_str, str) else scope_str
except (ValueError, TypeError):
scope = MemoryScope.AGENT_GROUP
try:
memory_type = MemoryType(memory_type_str) if isinstance(memory_type_str, str) else memory_type_str
except (ValueError, TypeError):
memory_type = MemoryType.WORKING
memory_data["scope"] = scope
memory_data["memory_type"] = memory_type
if memory_id not in self.scope_memories[scope][memory_type]:
self.scope_memories[scope][memory_type][memory_id] = memory_data
if self.scope_controller and memory_id not in self.scope_controller.scope_storage[scope][memory_type]:
self.scope_controller.scope_storage[scope][memory_type][memory_id] = memory_data
total_memories += 1
scope_access_passed += 1
permission_passed += 1
accessible_memories.append(memory_data)
seen_ids.add(str(memory_id))
except Exception as e:
logger.warning(f"Could not merge cross-agent shared memories for {agent_id}: {e}")

# Apply query filtering if provided
if query:
accessible_memories = [
Expand Down Expand Up @@ -840,7 +903,20 @@ def share_memory(
break

logger.info(f"Shared memory {memory_id} from {from_agent} to {len(shared_with)} agents")


# Persist recipients so subsequent HTTP requests (new AgentMemory instances) can search/list.
uniq_recipients: List[str] = []
for aid in shared_with:
if aid not in uniq_recipients:
uniq_recipients.append(aid)
try:
self._persist_shared_with_to_storage(memory_id, uniq_recipients)
except Exception as persist_err:
logger.warning(
"Persisting shared_with to vector store failed (share still in RAM): %s",
persist_err,
)

return {
'success': True,
'memory_id': memory_id,
Expand All @@ -853,6 +929,51 @@ def share_memory(
logger.error(f"Failed to share memory {memory_id}: {e}")
raise

def _persist_shared_with_to_storage(self, memory_id: Any, recipients: List[str]) -> None:
"""Merge recipients into payload metadata.shared_with in the backing Memory store."""
if not recipients:
return
try:
mid_int = int(memory_id) if not isinstance(memory_id, int) else memory_id
except (TypeError, ValueError):
logger.warning(f"Cannot persist shared_with: invalid memory id {memory_id!r}")
return

if not hasattr(self, "_memory_instance"):
from powermem.core.memory import Memory
if hasattr(self.config, "_data"):
config_dict = self.config._data
elif hasattr(self.config, "to_dict"):
config_dict = self.config.to_dict()
else:
config_dict = self.config
self._memory_instance = Memory(config_dict)

existing = self._memory_instance.get(mid_int, user_id=None, agent_id=None)
if not existing:
logger.warning(f"Persist shared_with: memory {mid_int} not found in storage")
return

md = dict(existing.get("metadata") or {})
sw = list(md.get("shared_with") or [])
for aid in recipients:
if aid not in sw:
sw.append(aid)
md["shared_with"] = sw

content = existing.get("memory") or existing.get("content") or ""
if isinstance(content, str) and not content.strip():
logger.warning(f"Persist shared_with: empty content for memory {mid_int}, skipping storage update")
return

self._memory_instance.update(
mid_int,
content=content,
user_id=existing.get("user_id"),
agent_id=existing.get("agent_id"),
metadata=md,
)

def get_context_info(self, agent_id: str) -> Dict[str, Any]:
"""
Get context information for an agent.
Expand Down Expand Up @@ -1092,8 +1213,24 @@ def _find_memory(self, memory_id: str) -> Optional[Dict[str, Any]]:
"""Find a memory by ID across all scopes and types."""
for scope in MemoryScope:
for memory_type in MemoryType:
if memory_id in self.scope_memories[scope][memory_type]:
return self.scope_memories[scope][memory_type][memory_id]
store = self.scope_memories[scope][memory_type]
if memory_id in store:
return store[memory_id]

# IDs can be int/str depending on source (database/cache), so
# normalize both forms before giving up.
try:
if isinstance(memory_id, str) and memory_id.isdigit():
numeric_id = int(memory_id)
if numeric_id in store:
return store[numeric_id]
except Exception:
pass

target_id = str(memory_id)
for key, value in store.items():
if str(key) == target_id:
return value
return None

def create_group(self, group_name: str, agent_ids: List[str], permissions: Optional[Dict[str, List[str]]] = None) -> Dict[str, Any]:
Expand Down
1 change: 1 addition & 0 deletions src/powermem/agent/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class AccessPermission(Enum):
READ = "read"
WRITE = "write"
DELETE = "delete"
SHARE = "share"
ADMIN = "admin"


Expand Down
2 changes: 1 addition & 1 deletion src/powermem/config_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ class AgentMemorySettings(_BasePowermemSettings):

enabled: bool = Field(default=True)
memory_mode: str = Field(default="auto", serialization_alias="mode")
default_scope: str = Field(default="AGENT")
default_scope: str = Field(default="AGENT_GROUP")
default_privacy_level: str = Field(default="PRIVATE")
default_collaboration_level: str = Field(default="READ_ONLY")
default_access_permission: str = Field(default="OWNER_ONLY")
Expand Down
Loading
Loading