Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e2d763e
feat: Add native hybird search (#197)
Evenss Jan 27, 2026
4c0eae7
feat: User Profile Support native language output (#198)
Evenss Jan 27, 2026
2be7d4f
Reconstruct LLM setting (#200)
Evenss Jan 28, 2026
fafcf64
refactor: unify configuration governance for agent, core, and server …
Teingi Feb 2, 2026
282f527
Reconstruct setting in Rerank,Vector,Graph (#202)
Evenss Feb 2, 2026
5711e22
oceanbase native language case (#220)
Ripcord55 Feb 3, 2026
05fd244
Oceanbase Native Hybrid Search Cases (#223)
Ripcord55 Feb 3, 2026
d2b4d12
Optimise searching in Intelligent mode And fix SILICONFLOW_LLM_BASE_U…
Evenss Feb 3, 2026
408c967
Fix unit test issues caused by setting changes (#228)
Evenss Feb 4, 2026
5fa2d29
Fixed run failure caused by incorrect folder name (#229)
Evenss Feb 4, 2026
215cf8e
fixed: timezone config parsing in set_timezone & update version 0.5.0…
Teingi Feb 5, 2026
4afeda9
Optimize prompt content when extracting user profiles (#234)
Evenss Feb 5, 2026
1809eb3
Enhance Memory class query handling (#236)
Evenss Feb 5, 2026
6bce38d
tests: Action case adjust (#237)
Ripcord55 Feb 6, 2026
9a3600c
release 0.5.0 (#238)
Teingi Feb 6, 2026
45b855b
resolve conflicts (#240)
Evenss Feb 6, 2026
082e617
release 0.5.1 (#242)
Teingi Feb 6, 2026
0c02460
test: Regression repair (#245)
Ripcord55 Feb 9, 2026
fe7380e
prompts upadte:LANGUAGE DO NOT translate (#248)
Teingi Feb 10, 2026
7764d17
Enhance memory listing functionality with pagination and sorting supp…
Evenss Feb 10, 2026
750a047
v0.5.2
Teingi Feb 10, 2026
b1667d7
fix search bug (#253)
Evenss Feb 11, 2026
5d0c1e8
Merge remote-tracking branch 'origin/0.5.0_dev' into merge_0.5.2
Teingi Feb 11, 2026
0421b1a
Enhance PGVectorConfig for flexible database connection settings (#257)
Evenss Feb 11, 2026
24ca6ff
Merge remote-tracking branch 'origin/0.5.0_dev' into merge_0.5.2
Teingi Feb 11, 2026
4dece2c
Merge remote-tracking branch 'origin/main' into merge_0.5.2
Teingi Feb 11, 2026
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
9 changes: 2 additions & 7 deletions .github/workflows/regression.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,18 +88,12 @@ jobs:
run: |
pip install -e ".[dev,test]"

# - name: Install Docker
# run: |
# curl -fsSL https://get.docker.com -o get-docker.sh
# sudo sh get-docker.sh


- name: Deploy SeekDB (OceanBase)
run: |
# Remove existing container if it exists
sudo docker rm -f seekdb 2>/dev/null || true
# Start SeekDB container
sudo docker run -d -p 10001:2881 --name seekdb oceanbase/seekdb
sudo docker run -d -p 10001:2881 -e MEMORY_LIMIT=4G -e LOG_DISK_SIZE=4G -e DATAFILE_SIZE=4G -e DATAFILE_NEXT=4G -e DATAFILE_MAXSIZE=100G --name seekdb oceanbase/seekdb
# Wait for database to be ready
echo "Waiting for SeekDB to be ready..."
timeout=60
Expand Down Expand Up @@ -149,6 +143,7 @@ jobs:
sed -i 's|^GRAPH_STORE_PASSWORD=.*|GRAPH_STORE_PASSWORD=|' .env
sed -i "s|^LLM_API_KEY=.*|LLM_API_KEY=${SILICONFLOW_CN_API_KEY}|" .env
sed -i "s|^EMBEDDING_API_KEY=.*|EMBEDDING_API_KEY=${QWEN_API_KEY}|" .env
sed -i "s|^POWERMEM_SERVER_API_KEYS=.*|POWERMEM_SERVER_API_KEYS=key1,key2,key3|" .env

- name: Run regression tests
env:
Expand Down
2 changes: 1 addition & 1 deletion examples/langchain/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# Install with: pip install -r requirements.txt

# Core dependencies
powermem>=0.5.0
powermem>=0.5.1
python-dotenv>=1.0.0

openai>=1.109.1,<3.0.0
Expand Down
2 changes: 1 addition & 1 deletion examples/langgraph/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# Install with: pip install -r requirements.txt

# Core dependencies
powermem>=0.5.0
powermem>=0.5.1
python-dotenv>=1.0.0

# LangGraph and LangChain dependencies
Expand Down
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 = "0.5.0"
version = "0.5.1"
description = "Intelligent Memory System - Persistent memory layer for LLM applications"
readme = "README.md"
license = {text = "Apache-2.0"}
Expand Down
2 changes: 1 addition & 1 deletion src/powermem/core/audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def log_event(
"user_id": user_id,
"agent_id": agent_id,
"details": details,
"version": "0.5.0",
"version": "0.5.1",
}

# Log to file
Expand Down
4 changes: 2 additions & 2 deletions src/powermem/core/telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def capture_event(
"user_id": user_id,
"agent_id": agent_id,
"timestamp": get_current_datetime().isoformat(),
"version": "0.5.0",
"version": "0.5.1",
}

self.events.append(event)
Expand Down Expand Up @@ -182,7 +182,7 @@ def set_user_properties(self, user_id: str, properties: Dict[str, Any]) -> None:
"properties": properties,
"user_id": user_id,
"timestamp": get_current_datetime().isoformat(),
"version": "0.5.0",
"version": "0.5.1",
}

self.events.append(event)
Expand Down
4 changes: 3 additions & 1 deletion src/powermem/prompts/intelligent_memory_prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
2. COMPLETE: Extract self-contained facts with who/what/when/where when available.
3. SEPARATE: Extract distinct facts separately, especially when they have different time periods.
4. INTENTIONS & NEEDS: ALWAYS extract user intentions, needs, and requests even without time information. Examples: "Want to book a doctor appointment", "Need to call someone", "Plan to visit a place".
5. LANGUAGE: DO NOT translate. Preserve the original language of the source text for each extracted fact. If the input is Chinese, output facts in Chinese; if English, output in English; if mixed-language, keep each fact in the language it appears in.

Examples:
Input: Hi.
Expand All @@ -51,7 +52,7 @@
- Extract from user/assistant messages only
- Extract intentions, needs, and requests even without time information
- If no relevant facts, return empty list
- Preserve input language
- Output must preserve the input language (no translation)

Extract facts from the conversation below:"""

Expand Down Expand Up @@ -87,6 +88,7 @@
Delete: Only clear contradictions (e.g., "Loves pizza" vs "Dislikes pizza"). Prefer UPDATE for time conflicts.

Important: Use existing IDs only. Keep same ID when updating. Always preserve temporal information.
LANGUAGE (CRITICAL): Do NOT translate memory text. Keep the same language as the incoming fact(s) and the original memory whenever possible.
"""

# Alias for compatibility
Expand Down
83 changes: 8 additions & 75 deletions src/powermem/storage/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -484,14 +484,13 @@ def get_all_memories(
if run_id:
filters["run_id"] = run_id

# Get memories from vector store with filters (if supported)
if filters and hasattr(self.vector_store, 'list'):
# Pass filters to vector store's list method for database-level filtering
# Request more records to support offset
results = self.vector_store.list(filters=filters, limit=limit + offset)
else:
# Fallback: get all and filter in memory
results = self.vector_store.list(limit=limit + offset)
results = self.vector_store.list(
filters=filters if filters else None,
limit=limit,
offset=offset,
order_by=sort_by,
order=order
)

# OceanBase returns [memories], SQLite/PGVector return memories directly
if results and isinstance(results[0], list):
Expand Down Expand Up @@ -549,73 +548,7 @@ def get_all_memories(

memories.append(memory)

# Apply sorting if specified
if sort_by:
memories = self._sort_memories(memories, sort_by, order)

# Apply offset and limit
return memories[offset:offset + limit]

def _sort_memories(
self,
memories: List[Dict[str, Any]],
sort_by: str,
order: str = "desc"
) -> List[Dict[str, Any]]:
"""
Sort memories by specified field.

Args:
memories: List of memory dictionaries
sort_by: Field to sort by. Options: "created_at", "updated_at", "id"
order: Sort order. "desc" for descending (default), "asc" for ascending

Returns:
Sorted list of memories
"""
if not memories or not sort_by:
return memories

reverse = (order.lower() == "desc")

def get_sort_key(memory: Dict[str, Any]) -> Any:
"""Get the sort key value from memory."""
if sort_by == "created_at":
created_at = memory.get("created_at")
if created_at is None:
return datetime.min if reverse else datetime.max
# Handle both datetime objects and ISO format strings
if isinstance(created_at, str):
try:
from datetime import datetime as dt
return dt.fromisoformat(created_at.replace('Z', '+00:00'))
except (ValueError, AttributeError):
return datetime.min if reverse else datetime.max
return created_at if isinstance(created_at, datetime) else datetime.min
elif sort_by == "updated_at":
updated_at = memory.get("updated_at")
if updated_at is None:
return datetime.min if reverse else datetime.max
# Handle both datetime objects and ISO format strings
if isinstance(updated_at, str):
try:
from datetime import datetime as dt
return dt.fromisoformat(updated_at.replace('Z', '+00:00'))
except (ValueError, AttributeError):
return datetime.min if reverse else datetime.max
return updated_at if isinstance(updated_at, datetime) else datetime.min
elif sort_by == "id":
return memory.get("id", 0)
else:
# Unknown sort field, return original order
return None

try:
sorted_memories = sorted(memories, key=get_sort_key, reverse=reverse)
return sorted_memories
except Exception as e:
logger.warning(f"Failed to sort memories by {sort_by}: {e}, returning original order")
return memories
return memories

def clear_memories(
self,
Expand Down
12 changes: 10 additions & 2 deletions src/powermem/storage/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,16 @@ def col_info(self):
pass

@abstractmethod
def list(self, filters=None, limit=None):
"""List all memories."""
def list(self, filters=None, limit=None, offset=None, order_by=None, order="desc"):
"""List all memories with optional filtering, pagination and sorting.

Args:
filters: Optional filters to apply
limit: Maximum number of results to return
offset: Number of results to skip
order_by: Field to sort by (e.g., "created_at", "updated_at", "id")
order: Sort order, "desc" for descending or "asc" for ascending
"""
pass

@abstractmethod
Expand Down
6 changes: 4 additions & 2 deletions src/powermem/storage/config/pgvector.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,10 @@ def check_auth_and_connection(cls, values):
return values
if values.get("connection_string") is not None:
return values
user, password = values.get("user"), values.get("password")
host, port = values.get("host"), values.get("port")
user = values.get("user") or values.get("POSTGRES_USER")
password = values.get("password") or values.get("POSTGRES_PASSWORD")
host = values.get("host") or values.get("POSTGRES_HOST")
port = values.get("port") or values.get("POSTGRES_PORT")
if user is not None or password is not None:
if not user or not password:
raise ValueError("Both 'user' and 'password' must be provided.")
Expand Down
59 changes: 38 additions & 21 deletions src/powermem/storage/oceanbase/oceanbase.py
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,7 @@ def insert(self,
logger.error(f"Failed to insert vectors into collection '{self.collection_name}': {e}", exc_info=True)
raise

def _generate_where_clause(self, filters: Optional[Dict] = None, table = None) -> Optional[List]:
def _generate_where_clause(self, filters: Optional[Dict] = None, table = None):
"""
Generate a properly formatted where clause for OceanBase.

Expand All @@ -497,7 +497,7 @@ def _generate_where_clause(self, filters: Optional[Dict] = None, table = None) -
table: SQLAlchemy Table object to use for column references. If None, uses self.table.

Returns:
Optional[List]: List of SQLAlchemy ColumnElement objects for where clause.
SQLAlchemy ColumnElement or None: A single SQLAlchemy expression for where clause, or None if no filters.
"""
# Use provided table or fall back to self.table
if table is None:
Expand Down Expand Up @@ -582,7 +582,7 @@ def process_condition(cond):

# Handle complex filters with AND/OR
result = process_condition(filters)
return [result] if result is not None else None
return result

def _row_to_model(self, row):
"""
Expand Down Expand Up @@ -934,8 +934,8 @@ def _fulltext_search(self, query: str, limit: int = 5, filters: Optional[Dict] =

# Combine FTS condition with filter conditions
where_conditions = [fts_condition]
if filter_where_clause:
where_conditions.extend(filter_where_clause)
if filter_where_clause is not None:
where_conditions.append(filter_where_clause)

# Build custom query to include score field
try:
Expand Down Expand Up @@ -1069,9 +1069,8 @@ def _sparse_search(self, sparse_embedding: Dict[int, float], limit: int = 5, fil
stmt = select(*columns)

# Add where conditions
if filter_where_clause:
for condition in filter_where_clause:
stmt = stmt.where(condition)
if filter_where_clause is not None:
stmt = stmt.where(filter_where_clause)

# Order by score ASC (lower negative_inner_product means higher similarity)
stmt = stmt.order_by(text('score ASC'))
Expand Down Expand Up @@ -1991,7 +1990,8 @@ def col_info(self):
logger.error(f"Failed to get collection info for '{self.collection_name}': {e}", exc_info=True)
raise

def list(self, filters: Optional[Dict] = None, limit: Optional[int] = None):
def list(self, filters: Optional[Dict] = None, limit: Optional[int] = None,
offset: Optional[int] = None, order_by: Optional[str] = None, order: str = "desc"):
"""List all memories."""
try:
table = Table(self.collection_name, self.obvector.metadata_obj, autoload_with=self.obvector.engine)
Expand All @@ -2000,18 +2000,38 @@ def list(self, filters: Optional[Dict] = None, limit: Optional[int] = None):
where_clause = self._generate_where_clause(filters, table=table)

# Build output column name list
output_columns = self._get_standard_column_names(include_vector_field=True)
output_columns_names = self._get_standard_column_names(include_vector_field=True)

# Build select statement with columns
output_columns = [table.c[col_name] for col_name in output_columns_names if col_name in table.c]
stmt = select(*output_columns)

# Apply WHERE clause
if where_clause is not None:
stmt = stmt.where(where_clause)

# Apply ORDER BY clause for sorting
if order_by:
if order_by in table.c:
order_column = table.c[order_by]
if order.lower() == "desc":
stmt = stmt.order_by(order_column.desc())
else:
stmt = stmt.order_by(order_column.asc())

# Apply OFFSET and LIMIT for pagination
if offset is not None:
stmt = stmt.offset(offset)
if limit is not None:
stmt = stmt.limit(limit)

# Get all records
results = self.obvector.get(
table_name=self.collection_name,
ids=None,
output_column_name=output_columns,
where_clause=where_clause
)
# Execute query
with self.obvector.engine.connect() as conn:
results = conn.execute(stmt)
rows = results.fetchall()

memories = []
for row in results.fetchall():
for row in rows:
parsed = self._parse_row_to_dict(row, include_vector=True, extract_score=False)

memories.append(self._create_output_data(
Expand All @@ -2021,9 +2041,6 @@ def list(self, filters: Optional[Dict] = None, limit: Optional[int] = None):
parsed["metadata"]
))

if limit:
memories = memories[:limit]

logger.debug(f"Successfully listed {len(memories)} memories from collection '{self.collection_name}'")
return [memories]

Expand Down
Loading
Loading