Skip to content

Commit e505120

Browse files
authored
Merge branch 'dev' into fix/pass-source-doc-id-in-completion-log
2 parents 9e88845 + b6efb0c commit e505120

File tree

6 files changed

+184
-74
lines changed

6 files changed

+184
-74
lines changed

src/memos/api/handlers/chat_handler.py

Lines changed: 34 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -405,33 +405,26 @@ def generate_chat_response() -> Generator[str, None, None]:
405405
async_mode="sync",
406406
)
407407

408-
# Use first readable cube ID for scheduler (backward compatibility)
409-
scheduler_cube_id = (
410-
readable_cube_ids[0] if readable_cube_ids else chat_req.user_id
411-
)
412-
self._send_message_to_scheduler(
413-
user_id=chat_req.user_id,
414-
mem_cube_id=scheduler_cube_id,
415-
query=chat_req.query,
416-
label=QUERY_TASK_LABEL,
417-
)
418-
419408
# ====== first search text mem with parse goal ======
420409
search_req = APISearchPlaygroundRequest(
421410
query=chat_req.query,
422411
user_id=chat_req.user_id,
423412
readable_cube_ids=readable_cube_ids,
424-
mode=chat_req.mode,
413+
mode="fast",
425414
internet_search=False,
426-
top_k=chat_req.top_k,
415+
top_k=5,
427416
chat_history=chat_req.history,
428417
session_id=chat_req.session_id,
429-
include_preference=chat_req.include_preference,
418+
include_preference=False,
430419
pref_top_k=chat_req.pref_top_k,
431420
filter=chat_req.filter,
421+
search_tool_memory=False,
432422
playground_search_goal_parser=False,
433423
)
424+
start_time = time.time()
434425
search_response = self.search_handler.handle_search_memories(search_req)
426+
end_time = time.time()
427+
self.logger.info(f"first search time: {end_time - start_time}")
435428

436429
yield f"data: {json.dumps({'type': 'status', 'data': '1'})}\n\n"
437430

@@ -447,17 +440,19 @@ def generate_chat_response() -> Generator[str, None, None]:
447440

448441
# Prepare reference data (first search)
449442
reference = prepare_reference_data(filtered_memories)
450-
# get preference string
451-
pref_string = search_response.data.get("pref_string", "")
452443

453444
yield f"data: {json.dumps({'type': 'reference', 'data': reference})}\n\n"
454445

455-
# Prepare preference markdown string
456-
if chat_req.include_preference:
457-
pref_list = search_response.data.get("pref_mem") or []
458-
pref_memories = pref_list[0].get("memories", []) if pref_list else []
459-
pref_md_string = self._build_pref_md_string_for_playground(pref_memories)
460-
yield f"data: {json.dumps({'type': 'pref_md_string', 'data': pref_md_string})}\n\n"
446+
# Use first readable cube ID for scheduler (backward compatibility)
447+
scheduler_cube_id = (
448+
readable_cube_ids[0] if readable_cube_ids else chat_req.user_id
449+
)
450+
self._send_message_to_scheduler(
451+
user_id=chat_req.user_id,
452+
mem_cube_id=scheduler_cube_id,
453+
query=chat_req.query,
454+
label=QUERY_TASK_LABEL,
455+
)
461456

462457
# parse goal for internet search
463458
searcher = self.dependencies.searcher
@@ -481,23 +476,28 @@ def generate_chat_response() -> Generator[str, None, None]:
481476
# internet status
482477
yield f"data: {json.dumps({'type': 'status', 'data': 'start_internet_search'})}\n\n"
483478

484-
# ====== internet search with parse goal ======
479+
# ====== second deep search ======
485480
search_req = APISearchPlaygroundRequest(
486481
query=parsed_goal.rephrased_query
487482
or chat_req.query + (f"{parsed_goal.tags}" if parsed_goal.tags else ""),
488483
user_id=chat_req.user_id,
489484
readable_cube_ids=readable_cube_ids,
490-
mode=chat_req.mode,
491-
internet_search=chat_req.internet_search,
485+
mode="fast",
486+
internet_search=chat_req.internet_search or parsed_goal.internet_search,
492487
top_k=chat_req.top_k,
493488
chat_history=chat_req.history,
494489
session_id=chat_req.session_id,
495-
include_preference=False,
490+
include_preference=chat_req.include_preference,
491+
pref_top_k=chat_req.pref_top_k,
496492
filter=chat_req.filter,
497493
search_memory_type="All",
494+
search_tool_memory=False,
498495
playground_search_goal_parser=False,
499496
)
497+
start_time = time.time()
500498
search_response = self.search_handler.handle_search_memories(search_req)
499+
end_time = time.time()
500+
self.logger.info(f"second search time: {end_time - start_time}")
501501

502502
# Extract memories from search results (second search)
503503
memories_list = []
@@ -516,12 +516,19 @@ def generate_chat_response() -> Generator[str, None, None]:
516516

517517
# Prepare remain reference data (second search)
518518
reference = prepare_reference_data(filtered_memories)
519+
# get preference string
520+
pref_string = search_response.data.get("pref_string", "")
519521
# get internet reference
520522
internet_reference = self._get_internet_reference(
521523
search_response.data.get("text_mem")[0]["memories"]
522524
)
523-
524525
yield f"data: {json.dumps({'type': 'reference', 'data': reference})}\n\n"
526+
# Prepare preference markdown string
527+
if chat_req.include_preference:
528+
pref_list = search_response.data.get("pref_mem") or []
529+
pref_memories = pref_list[0].get("memories", []) if pref_list else []
530+
pref_md_string = self._build_pref_md_string_for_playground(pref_memories)
531+
yield f"data: {json.dumps({'type': 'pref_md_string', 'data': pref_md_string})}\n\n"
525532

526533
# Step 2: Build system prompt with memories
527534
system_prompt = self._build_enhance_system_prompt(

src/memos/graph_dbs/polardb.py

Lines changed: 101 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3348,58 +3348,120 @@ def add_nodes_batch(
33483348
with conn.cursor() as cursor:
33493349
# Process each group separately
33503350
for embedding_column, nodes_group in nodes_by_embedding_column.items():
3351-
# Delete existing records first (batch delete)
3352-
for node in nodes_group:
3351+
# Batch delete existing records using IN clause
3352+
ids_to_delete = [node["id"] for node in nodes_group]
3353+
if ids_to_delete:
33533354
delete_query = f"""
33543355
DELETE FROM {self.db_name}_graph."Memory"
3355-
WHERE id = ag_catalog._make_graph_id('{self.db_name}_graph'::name, 'Memory'::name, %s::text::cstring)
3356+
WHERE id IN (
3357+
SELECT ag_catalog._make_graph_id('{self.db_name}_graph'::name, 'Memory'::name, unnest(%s::text[])::cstring)
3358+
)
33563359
"""
3357-
cursor.execute(delete_query, (node["id"],))
3360+
cursor.execute(delete_query, (ids_to_delete,))
3361+
3362+
# Batch get graph_ids for all nodes
3363+
get_graph_ids_query = f"""
3364+
SELECT
3365+
id_val,
3366+
ag_catalog._make_graph_id('{self.db_name}_graph'::name, 'Memory'::name, id_val::text::cstring) as graph_id
3367+
FROM unnest(%s::text[]) as id_val
3368+
"""
3369+
cursor.execute(get_graph_ids_query, (ids_to_delete,))
3370+
graph_id_map = {row[0]: row[1] for row in cursor.fetchall()}
33583371

3359-
# Insert nodes (batch insert using executemany for better performance)
3372+
# Add graph_id to properties
33603373
for node in nodes_group:
3361-
# Get graph_id for this node
3362-
get_graph_id_query = f"""
3363-
SELECT ag_catalog._make_graph_id('{self.db_name}_graph'::name, 'Memory'::name, %s::text::cstring)
3364-
"""
3365-
cursor.execute(get_graph_id_query, (node["id"],))
3366-
graph_id = cursor.fetchone()[0]
3367-
node["properties"]["graph_id"] = str(graph_id)
3368-
3369-
# Insert node
3370-
if node["embedding_vector"]:
3371-
insert_query = f"""
3372-
INSERT INTO {self.db_name}_graph."Memory"(id, properties, {embedding_column})
3373-
VALUES (
3374-
ag_catalog._make_graph_id('{self.db_name}_graph'::name, 'Memory'::name, %s::text::cstring),
3375-
%s,
3376-
%s
3374+
graph_id = graph_id_map.get(node["id"])
3375+
if graph_id:
3376+
node["properties"]["graph_id"] = str(graph_id)
3377+
3378+
# Batch insert using VALUES with multiple rows
3379+
# Use psycopg2.extras.execute_values for efficient batch insert
3380+
from psycopg2.extras import execute_values
3381+
3382+
if embedding_column and any(node["embedding_vector"] for node in nodes_group):
3383+
# Prepare data tuples for batch insert with embedding
3384+
data_tuples = []
3385+
for node in nodes_group:
3386+
# Each tuple: (id, properties_json, embedding_json)
3387+
data_tuples.append(
3388+
(
3389+
node["id"],
3390+
json.dumps(node["properties"]),
3391+
json.dumps(node["embedding_vector"])
3392+
if node["embedding_vector"]
3393+
else None,
33773394
)
3378-
"""
3379-
logger.info(
3380-
f"[add_nodes_batch] Inserting node insert_query={insert_query}"
33813395
)
3382-
cursor.execute(
3383-
insert_query,
3396+
3397+
# Build the INSERT query template
3398+
insert_query = f"""
3399+
INSERT INTO {self.db_name}_graph."Memory"(id, properties, {embedding_column})
3400+
VALUES %s
3401+
"""
3402+
3403+
# Build the VALUES template for execute_values
3404+
# Each row: (graph_id_function, agtype, vector)
3405+
# Note: properties column is agtype, not jsonb
3406+
template = f"""
3407+
(
3408+
ag_catalog._make_graph_id('{self.db_name}_graph'::name, 'Memory'::name, %s::text::cstring),
3409+
%s::text::agtype,
3410+
%s::vector
3411+
)
3412+
"""
3413+
logger.info(
3414+
f"[add_nodes_batch] embedding_column Inserting insert_query:{insert_query}"
3415+
)
3416+
logger.info(
3417+
f"[add_nodes_batch] embedding_column Inserting data_tuples:{data_tuples}"
3418+
)
3419+
3420+
# Execute batch insert
3421+
execute_values(
3422+
cursor,
3423+
insert_query,
3424+
data_tuples,
3425+
template=template,
3426+
page_size=100, # Insert in batches of 100
3427+
)
3428+
else:
3429+
# Prepare data tuples for batch insert without embedding
3430+
data_tuples = []
3431+
for node in nodes_group:
3432+
# Each tuple: (id, properties_json)
3433+
data_tuples.append(
33843434
(
33853435
node["id"],
33863436
json.dumps(node["properties"]),
3387-
json.dumps(node["embedding_vector"]),
3388-
),
3389-
)
3390-
else:
3391-
insert_query = f"""
3392-
INSERT INTO {self.db_name}_graph."Memory"(id, properties)
3393-
VALUES (
3394-
ag_catalog._make_graph_id('{self.db_name}_graph'::name, 'Memory'::name, %s::text::cstring),
3395-
%s
33963437
)
3397-
"""
3398-
cursor.execute(
3399-
insert_query,
3400-
(node["id"], json.dumps(node["properties"])),
34013438
)
34023439

3440+
# Build the INSERT query template
3441+
insert_query = f"""
3442+
INSERT INTO {self.db_name}_graph."Memory"(id, properties)
3443+
VALUES %s
3444+
"""
3445+
3446+
# Build the VALUES template for execute_values
3447+
# Note: properties column is agtype, not jsonb
3448+
template = f"""
3449+
(
3450+
ag_catalog._make_graph_id('{self.db_name}_graph'::name, 'Memory'::name, %s::text::cstring),
3451+
%s::text::agtype
3452+
)
3453+
"""
3454+
logger.info(f"[add_nodes_batch] Inserting insert_query:{insert_query}")
3455+
logger.info(f"[add_nodes_batch] Inserting data_tuples:{data_tuples}")
3456+
# Execute batch insert
3457+
execute_values(
3458+
cursor,
3459+
insert_query,
3460+
data_tuples,
3461+
template=template,
3462+
page_size=100, # Insert in batches of 100
3463+
)
3464+
34033465
logger.info(
34043466
f"[add_nodes_batch] Inserted {len(nodes_group)} nodes with embedding_column={embedding_column}"
34053467
)

src/memos/mem_reader/read_multi_modal/system_parser.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Parser for system messages."""
22

3+
import ast
34
import json
45
import re
56
import uuid
@@ -137,8 +138,14 @@ def parse_fine(
137138
tool_schema = json.loads(content)
138139
assert isinstance(tool_schema, list), "Tool schema must be a list[dict]"
139140
except json.JSONDecodeError:
140-
logger.warning(f"[SystemParser] Failed to parse tool schema: {content}")
141-
return []
141+
try:
142+
tool_schema = ast.literal_eval(content)
143+
assert isinstance(tool_schema, list), "Tool schema must be a list[dict]"
144+
except (ValueError, SyntaxError, AssertionError):
145+
logger.warning(
146+
f"[SystemParser] Failed to parse tool schema with both JSON and ast.literal_eval: {content}"
147+
)
148+
return []
142149
except AssertionError:
143150
logger.warning(f"[SystemParser] Tool schema must be a list[dict]: {content}")
144151
return []

src/memos/memories/textual/tree.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,17 @@ def delete_all(self) -> None:
343343
logger.error(f"An error occurred while deleting all memories: {e}")
344344
raise
345345

346+
def delete_by_filter(
347+
self,
348+
writable_cube_ids: list[str],
349+
file_ids: list[str] | None = None,
350+
filter: dict | None = None,
351+
) -> None:
352+
"""Delete memories by filter."""
353+
self.graph_store.delete_node_by_prams(
354+
writable_cube_ids=writable_cube_ids, file_ids=file_ids, filter=filter
355+
)
356+
346357
def load(self, dir: str) -> None:
347358
try:
348359
memory_file = os.path.join(dir, self.config.memory_filename)

src/memos/memories/textual/tree_text_memory/retrieve/searcher.py

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -701,15 +701,35 @@ def _sort_and_trim(
701701
"""Sort results by score and trim to top_k"""
702702
final_items = []
703703
if search_tool_memory:
704-
tool_results = [
704+
tool_schema_results = [
705705
(item, score)
706706
for item, score in results
707-
if item.metadata.memory_type in ["ToolSchemaMemory", "ToolTrajectoryMemory"]
707+
if item.metadata.memory_type == "ToolSchemaMemory"
708708
]
709-
sorted_tool_results = sorted(tool_results, key=lambda pair: pair[1], reverse=True)[
710-
:tool_mem_top_k
709+
sorted_tool_schema_results = sorted(
710+
tool_schema_results, key=lambda pair: pair[1], reverse=True
711+
)[:tool_mem_top_k]
712+
for item, score in sorted_tool_schema_results:
713+
if plugin and round(score, 2) == 0.00:
714+
continue
715+
meta_data = item.metadata.model_dump()
716+
meta_data["relativity"] = score
717+
final_items.append(
718+
TextualMemoryItem(
719+
id=item.id,
720+
memory=item.memory,
721+
metadata=SearchedTreeNodeTextualMemoryMetadata(**meta_data),
722+
)
723+
)
724+
tool_trajectory_results = [
725+
(item, score)
726+
for item, score in results
727+
if item.metadata.memory_type == "ToolTrajectoryMemory"
711728
]
712-
for item, score in sorted_tool_results:
729+
sorted_tool_trajectory_results = sorted(
730+
tool_trajectory_results, key=lambda pair: pair[1], reverse=True
731+
)[:tool_mem_top_k]
732+
for item, score in sorted_tool_trajectory_results:
713733
if plugin and round(score, 2) == 0.00:
714734
continue
715735
meta_data = item.metadata.model_dump()

src/memos/multi_mem_cube/single_cube.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
SearchMode,
3131
UserContext,
3232
)
33+
from memos.utils import timed
3334

3435

3536
logger = get_logger(__name__)
@@ -198,6 +199,7 @@ def _get_search_mode(self, mode: str) -> str:
198199
"""
199200
return mode
200201

202+
@timed
201203
def _search_text(
202204
self,
203205
search_req: APISearchRequest,
@@ -363,6 +365,7 @@ def _fine_search(
363365

364366
return formatted_memories
365367

368+
@timed
366369
def _search_pref(
367370
self,
368371
search_req: APISearchRequest,
@@ -429,7 +432,7 @@ def _fast_search(
429432
top_k=search_req.top_k,
430433
mode=SearchMode.FAST,
431434
manual_close_internet=not search_req.internet_search,
432-
momory_type=search_req.search_memory_type,
435+
memory_type=search_req.search_memory_type,
433436
search_filter=search_filter,
434437
search_priority=search_priority,
435438
info={

0 commit comments

Comments
 (0)