allowing Memory_Manager to search by tags with AND/OR operators
Browse files
app.py
CHANGED
@@ -1077,19 +1077,129 @@ def _mem_list(
|
|
1077 |
return "\n".join(lines)
|
1078 |
|
1079 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1080 |
def _mem_search(
|
1081 |
-
query: Annotated[str, "
|
1082 |
limit: Annotated[int, "Maximum number of matches (1–200)."] = 20,
|
1083 |
) -> str:
|
1084 |
-
"""(Internal)
|
1085 |
-
|
1086 |
-
Search
|
1087 |
-
-
|
1088 |
-
-
|
1089 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
1090 |
|
1091 |
Parameters:
|
1092 |
-
query (str):
|
1093 |
limit (int): Max rows to return; clamped to [1, 200].
|
1094 |
|
1095 |
Returns:
|
@@ -1098,18 +1208,21 @@ def _mem_search(
|
|
1098 |
q = (query or "").strip()
|
1099 |
if not q:
|
1100 |
return "Error: empty query."
|
1101 |
-
|
1102 |
-
|
1103 |
-
|
|
|
|
|
|
|
1104 |
limit = max(1, min(200, limit))
|
1105 |
with _MEMORY_LOCK:
|
1106 |
memories = _load_memories()
|
1107 |
-
|
1108 |
-
|
|
|
1109 |
total_matches = 0
|
1110 |
-
for m in reversed(memories): # newest
|
1111 |
-
|
1112 |
-
if all(t in hay for t in terms):
|
1113 |
total_matches += 1
|
1114 |
if len(matches) < limit:
|
1115 |
matches.append(m)
|
@@ -1479,7 +1592,7 @@ def Memory_Manager(
|
|
1479 |
action: Annotated[Literal["save","list","search","delete"], "Action to perform: save | list | search | delete"],
|
1480 |
text: Annotated[Optional[str], "Text content (Save only)"] = None,
|
1481 |
tags: Annotated[Optional[str], "Comma-separated tags (Save only)"] = None,
|
1482 |
-
query: Annotated[Optional[str], "
|
1483 |
limit: Annotated[int, "Max results (List/Search only)"] = 20,
|
1484 |
memory_id: Annotated[Optional[str], "Full UUID or unique prefix (Delete only)"] = None,
|
1485 |
include_tags: Annotated[bool, "Include tags (List/Search only)"] = True,
|
@@ -1494,7 +1607,7 @@ def Memory_Manager(
|
|
1494 |
Supported Actions:
|
1495 |
- save : Store a new memory (requires 'text'; optional 'tags').
|
1496 |
- list : Return the most recent memories (respects 'limit' + 'include_tags').
|
1497 |
-
- search : AND match
|
1498 |
- delete : Remove one memory by full UUID or unique prefix (uses 'memory_id').
|
1499 |
|
1500 |
Parameter Usage by Action:
|
@@ -1507,7 +1620,11 @@ def Memory_Manager(
|
|
1507 |
action (Literal[save|list|search|delete]): Operation selector (case-insensitive).
|
1508 |
text (str): Raw memory content; leading/trailing whitespace trimmed (save only).
|
1509 |
tags (str): Optional comma-separated tags; stored verbatim (save only).
|
1510 |
-
query (str):
|
|
|
|
|
|
|
|
|
1511 |
limit (int): Maximum rows for list/search (clamped internally to 1–200).
|
1512 |
memory_id (str): Full UUID or unique prefix (>=4 chars) (delete only).
|
1513 |
include_tags (bool): When True, show tag column in list/search output.
|
@@ -1561,7 +1678,7 @@ memory_interface = gr.Interface(
|
|
1561 |
gr.Dropdown(label="Action", choices=["save","list","search","delete"], value="list"),
|
1562 |
gr.Textbox(label="Text", lines=3, placeholder="Memory text (save)"),
|
1563 |
gr.Textbox(label="Tags", placeholder="tag1, tag2"),
|
1564 |
-
gr.Textbox(label="Query", placeholder="
|
1565 |
gr.Slider(1, 200, value=20, step=1, label="Limit"),
|
1566 |
gr.Textbox(label="Memory ID / Prefix", placeholder="UUID or prefix (delete)"),
|
1567 |
gr.Checkbox(value=True, label="Include Tags"),
|
@@ -1573,8 +1690,10 @@ memory_interface = gr.Interface(
|
|
1573 |
),
|
1574 |
api_description=(
|
1575 |
"Manage short text memories with optional tags. Actions: save(text,tags), list(limit,include_tags), "
|
1576 |
-
"search(query,limit,include_tags), delete(memory_id).
|
1577 |
-
"
|
|
|
|
|
1578 |
),
|
1579 |
flagging_mode="never",
|
1580 |
)
|
|
|
1077 |
return "\n".join(lines)
|
1078 |
|
1079 |
|
1080 |
+
def _parse_search_query(query: str) -> Dict[str, List[str]]:
|
1081 |
+
"""Parse a search query into structured components.
|
1082 |
+
|
1083 |
+
Supports:
|
1084 |
+
- tag:name - search for specific tag
|
1085 |
+
- AND/OR operators (case-insensitive)
|
1086 |
+
- Regular text terms
|
1087 |
+
- Implicit AND between terms when no operator specified
|
1088 |
+
|
1089 |
+
Examples:
|
1090 |
+
'tag:work' -> {'tag_terms': ['work'], 'text_terms': [], 'operator': 'and'}
|
1091 |
+
'tag:work AND tag:project' -> {'tag_terms': ['work', 'project'], 'text_terms': [], 'operator': 'and'}
|
1092 |
+
'tag:personal OR tag:todo' -> {'tag_terms': ['personal', 'todo'], 'text_terms': [], 'operator': 'or'}
|
1093 |
+
'meeting tag:work' -> {'tag_terms': ['work'], 'text_terms': ['meeting'], 'operator': 'and'}
|
1094 |
+
'tag:urgent OR important' -> {'tag_terms': ['urgent'], 'text_terms': ['important'], 'operator': 'or'}
|
1095 |
+
|
1096 |
+
Returns:
|
1097 |
+
Dict with keys: 'tag_terms', 'text_terms', 'operator' (and/or)
|
1098 |
+
"""
|
1099 |
+
import re
|
1100 |
+
|
1101 |
+
# Initialize result
|
1102 |
+
result = {
|
1103 |
+
'tag_terms': [],
|
1104 |
+
'text_terms': [],
|
1105 |
+
'operator': 'and' # default
|
1106 |
+
}
|
1107 |
+
|
1108 |
+
if not query or not query.strip():
|
1109 |
+
return result
|
1110 |
+
|
1111 |
+
# Normalize whitespace and detect OR operator
|
1112 |
+
query = re.sub(r'\s+', ' ', query.strip())
|
1113 |
+
if re.search(r'\bOR\b', query, re.IGNORECASE):
|
1114 |
+
result['operator'] = 'or'
|
1115 |
+
# Split on OR (case-insensitive)
|
1116 |
+
parts = re.split(r'\s+OR\s+', query, flags=re.IGNORECASE)
|
1117 |
+
else:
|
1118 |
+
# Split on AND (case-insensitive) or just whitespace
|
1119 |
+
parts = re.split(r'\s+(?:AND\s+)?', query, flags=re.IGNORECASE)
|
1120 |
+
# Remove empty AND tokens that might have been left
|
1121 |
+
parts = [p for p in parts if p.strip() and p.strip().upper() != 'AND']
|
1122 |
+
|
1123 |
+
# Process each part
|
1124 |
+
for part in parts:
|
1125 |
+
part = part.strip()
|
1126 |
+
if not part:
|
1127 |
+
continue
|
1128 |
+
|
1129 |
+
# Check if it's a tag query
|
1130 |
+
tag_match = re.match(r'^tag:(.+)$', part, re.IGNORECASE)
|
1131 |
+
if tag_match:
|
1132 |
+
tag_name = tag_match.group(1).strip()
|
1133 |
+
if tag_name:
|
1134 |
+
result['tag_terms'].append(tag_name.lower())
|
1135 |
+
else:
|
1136 |
+
# Regular text term
|
1137 |
+
result['text_terms'].append(part.lower())
|
1138 |
+
|
1139 |
+
return result
|
1140 |
+
|
1141 |
+
|
1142 |
+
def _match_memory_with_query(memory: Dict[str, str], parsed_query: Dict[str, List[str]]) -> bool:
|
1143 |
+
"""Check if a memory matches the parsed search query."""
|
1144 |
+
tag_terms = parsed_query['tag_terms']
|
1145 |
+
text_terms = parsed_query['text_terms']
|
1146 |
+
operator = parsed_query['operator']
|
1147 |
+
|
1148 |
+
# If no terms, no match
|
1149 |
+
if not tag_terms and not text_terms:
|
1150 |
+
return False
|
1151 |
+
|
1152 |
+
# Get memory content (case-insensitive)
|
1153 |
+
memory_text = memory.get('text', '').lower()
|
1154 |
+
memory_tags = memory.get('tags', '').lower()
|
1155 |
+
|
1156 |
+
# Split memory tags into individual tags
|
1157 |
+
memory_tag_list = [tag.strip() for tag in memory_tags.split(',') if tag.strip()]
|
1158 |
+
|
1159 |
+
# Check tag matches
|
1160 |
+
tag_matches = []
|
1161 |
+
for tag_term in tag_terms:
|
1162 |
+
# Check if tag_term matches any of the memory's tags
|
1163 |
+
tag_matches.append(any(tag_term in tag for tag in memory_tag_list))
|
1164 |
+
|
1165 |
+
# Check text matches
|
1166 |
+
text_matches = []
|
1167 |
+
combined_text = memory_text + ' ' + memory_tags # For backward compatibility
|
1168 |
+
for text_term in text_terms:
|
1169 |
+
text_matches.append(text_term in combined_text)
|
1170 |
+
|
1171 |
+
# Combine all matches
|
1172 |
+
all_matches = tag_matches + text_matches
|
1173 |
+
|
1174 |
+
if not all_matches:
|
1175 |
+
return False
|
1176 |
+
|
1177 |
+
# Apply operator logic
|
1178 |
+
if operator == 'or':
|
1179 |
+
return any(all_matches)
|
1180 |
+
else: # 'and'
|
1181 |
+
return all(all_matches)
|
1182 |
+
|
1183 |
+
|
1184 |
def _mem_search(
|
1185 |
+
query: Annotated[str, "Advanced search with tag:name syntax, AND/OR operators, and text terms."],
|
1186 |
limit: Annotated[int, "Maximum number of matches (1–200)."] = 20,
|
1187 |
) -> str:
|
1188 |
+
"""(Internal) Enhanced search with tag queries and boolean operators.
|
1189 |
+
|
1190 |
+
Search Syntax:
|
1191 |
+
- tag:name - search for specific tag
|
1192 |
+
- AND/OR operators (case-insensitive, default is AND)
|
1193 |
+
- Regular text terms search in text content and tags
|
1194 |
+
- Examples:
|
1195 |
+
* 'tag:work' - memories with 'work' tag
|
1196 |
+
* 'tag:work AND tag:project' - memories with both tags
|
1197 |
+
* 'tag:personal OR tag:todo' - memories with either tag
|
1198 |
+
* 'meeting tag:work' - memories with "meeting" in text and 'work' tag
|
1199 |
+
* 'tag:urgent OR important' - memories with 'urgent' tag OR "important" anywhere
|
1200 |
|
1201 |
Parameters:
|
1202 |
+
query (str): Enhanced query string with tag: syntax and AND/OR operators.
|
1203 |
limit (int): Max rows to return; clamped to [1, 200].
|
1204 |
|
1205 |
Returns:
|
|
|
1208 |
q = (query or "").strip()
|
1209 |
if not q:
|
1210 |
return "Error: empty query."
|
1211 |
+
|
1212 |
+
# Parse the enhanced query
|
1213 |
+
parsed_query = _parse_search_query(q)
|
1214 |
+
if not parsed_query['tag_terms'] and not parsed_query['text_terms']:
|
1215 |
+
return "Error: no valid search terms found."
|
1216 |
+
|
1217 |
limit = max(1, min(200, limit))
|
1218 |
with _MEMORY_LOCK:
|
1219 |
memories = _load_memories()
|
1220 |
+
|
1221 |
+
# Search with enhanced logic
|
1222 |
+
matches: List[Dict[str, str]] = []
|
1223 |
total_matches = 0
|
1224 |
+
for m in reversed(memories): # newest first
|
1225 |
+
if _match_memory_with_query(m, parsed_query):
|
|
|
1226 |
total_matches += 1
|
1227 |
if len(matches) < limit:
|
1228 |
matches.append(m)
|
|
|
1592 |
action: Annotated[Literal["save","list","search","delete"], "Action to perform: save | list | search | delete"],
|
1593 |
text: Annotated[Optional[str], "Text content (Save only)"] = None,
|
1594 |
tags: Annotated[Optional[str], "Comma-separated tags (Save only)"] = None,
|
1595 |
+
query: Annotated[Optional[str], "Enhanced search with tag:name syntax, AND/OR operators (Search only)"] = None,
|
1596 |
limit: Annotated[int, "Max results (List/Search only)"] = 20,
|
1597 |
memory_id: Annotated[Optional[str], "Full UUID or unique prefix (Delete only)"] = None,
|
1598 |
include_tags: Annotated[bool, "Include tags (List/Search only)"] = True,
|
|
|
1607 |
Supported Actions:
|
1608 |
- save : Store a new memory (requires 'text'; optional 'tags').
|
1609 |
- list : Return the most recent memories (respects 'limit' + 'include_tags').
|
1610 |
+
- search : Enhanced AND match with tag: queries, boolean operators, and text terms (uses 'query', 'limit').
|
1611 |
- delete : Remove one memory by full UUID or unique prefix (uses 'memory_id').
|
1612 |
|
1613 |
Parameter Usage by Action:
|
|
|
1620 |
action (Literal[save|list|search|delete]): Operation selector (case-insensitive).
|
1621 |
text (str): Raw memory content; leading/trailing whitespace trimmed (save only).
|
1622 |
tags (str): Optional comma-separated tags; stored verbatim (save only).
|
1623 |
+
query (str): Enhanced search query supporting:
|
1624 |
+
- tag:name - search for specific tag
|
1625 |
+
- AND/OR operators (case-insensitive, default is AND)
|
1626 |
+
- Regular text terms search in text content and tags
|
1627 |
+
- Examples: 'tag:work', 'tag:work AND tag:project', 'meeting tag:work', 'tag:urgent OR important'
|
1628 |
limit (int): Maximum rows for list/search (clamped internally to 1–200).
|
1629 |
memory_id (str): Full UUID or unique prefix (>=4 chars) (delete only).
|
1630 |
include_tags (bool): When True, show tag column in list/search output.
|
|
|
1678 |
gr.Dropdown(label="Action", choices=["save","list","search","delete"], value="list"),
|
1679 |
gr.Textbox(label="Text", lines=3, placeholder="Memory text (save)"),
|
1680 |
gr.Textbox(label="Tags", placeholder="tag1, tag2"),
|
1681 |
+
gr.Textbox(label="Query", placeholder="tag:work AND tag:project OR meeting"),
|
1682 |
gr.Slider(1, 200, value=20, step=1, label="Limit"),
|
1683 |
gr.Textbox(label="Memory ID / Prefix", placeholder="UUID or prefix (delete)"),
|
1684 |
gr.Checkbox(value=True, label="Include Tags"),
|
|
|
1690 |
),
|
1691 |
api_description=(
|
1692 |
"Manage short text memories with optional tags. Actions: save(text,tags), list(limit,include_tags), "
|
1693 |
+
"search(query,limit,include_tags), delete(memory_id). Enhanced search supports tag:name queries and AND/OR operators. "
|
1694 |
+
"Examples: 'tag:work', 'tag:work AND tag:project', 'meeting tag:work', 'tag:urgent OR important'. "
|
1695 |
+
"Action parameter is always required. Use Memory_Manager whenever you are given information worth remembering about the user, "
|
1696 |
+
"and search for memories when relevant."
|
1697 |
),
|
1698 |
flagging_mode="never",
|
1699 |
)
|