Nymbo commited on
Commit
9e2a5dd
·
verified ·
1 Parent(s): 5c66cbb

allowing Memory_Manager to search by tags with AND/OR operators

Browse files
Files changed (1) hide show
  1. app.py +141 -22
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, "Case-insensitive substring search; space-separated terms are ANDed."],
1082
  limit: Annotated[int, "Maximum number of matches (1–200)."] = 20,
1083
  ) -> str:
1084
- """(Internal) Full-text style AND search across text and tags.
1085
-
1086
- Search Semantics:
1087
- - Split query on whitespace into individual terms.
1088
- - A memory matches only if EVERY term appears (case-insensitive) in the text OR tags field.
1089
- - Results are ordered newest-first (descending timestamp).
 
 
 
 
 
 
1090
 
1091
  Parameters:
1092
- query (str): Raw user query string; must contain at least one non-space character.
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
- terms = [t.lower() for t in q.split() if t.strip()]
1102
- if not terms:
1103
- return "Error: no valid search terms."
 
 
 
1104
  limit = max(1, min(200, limit))
1105
  with _MEMORY_LOCK:
1106
  memories = _load_memories()
1107
- # Newest first iteration for early cutoff
1108
- matches: List[Dict[str, str]] = [] # collected (capped at limit)
 
1109
  total_matches = 0
1110
- for m in reversed(memories): # newest backward
1111
- hay = (m.get("text", "") + " " + m.get("tags", "")).lower()
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], "Search query terms (Search only)"] = None,
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 space‑separated terms across text and tags (uses 'query', 'limit').
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): Space-separated terms (AND logic, case-insensitive) across text+tags (search only).
 
 
 
 
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="Search terms (search)"),
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). Returns plaintext JSON. Action parameter is always required. "
1577
- "Use Memory_Manager whenever you are given information worth remembering about the user, and search for memories when relevant."
 
 
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
  )