-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtools.py
More file actions
151 lines (130 loc) · 5.56 KB
/
Copy pathtools.py
File metadata and controls
151 lines (130 loc) · 5.56 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
import json
import logging
import sys
from embeddings import _embed_text_to_floats, serialize_f32
from store import MemoryStore
logging.basicConfig(level=logging.INFO, stream=sys.stderr)
logger = logging.getLogger(__name__)
TOOL_SCHEMAS = [
{
"type": "function",
"function": {
"name": "memory_search",
"description": "Search stored memories by keyword. Call BEFORE "
"answering any question about the user's preferences, history, "
"or personal details, and before giving personalized "
"recommendations. Input: 1-3 content keywords.",
"parameters": {
"type": "object",
"properties": {
"keyword": {
"type": "string",
"description": "1-3 content keywords to search. e.g. 'hiking outdoor'"
}
},
"required": ["keyword"],
},
},
},
{
"type": "function",
"function": {
"name": "memory_list",
"description": "Return all stored memories about the user. Call for "
"broad questions ('what do you remember about me?'), open-ended "
"personalization spanning many topics, or as a fallback when "
"memory_search returns nothing but the fact likely exists under "
"different wording. Takes no input.",
"parameters": {
"type": "object",
},
},
}
]
def make_tools(store: MemoryStore, user_id: str):
# each tool closes over `store` and `user_id`
def memory_search(keyword: str, *, top_k: int = 5, use_vector: bool = True) -> str:
"""
Search memories for given keyword.
- if `use_vector` is True and the sqlite-vec extension is available,
we perform a nearest-neighbor vector search on the `embedding` column.
- Regardless of the vector result we also run a simple LIKE-search on the raw
`content` column - this guarantees you still get the hits when the vector
index is missing or the query is very short.
- The two result sets are de-duplicated (by `id`) and limited to `top_k`
rows, preserving the vector-score order first, then the keyword matches.
"""
# try the vector path (if requested and extension is loadable)
vec_rows = []
if use_vector:
try:
# Load the sqlite-vec extension on the store's connection
conn = store.conn
# embed the keyword
keyword_embedding = _embed_text_to_floats(keyword)
keyword_blob = serialize_f32(keyword_embedding)
# perform brute-force cosine-distance search.
# For L2-normalized vectors, cosine distance = 1 - dot_product.
# sqlite-vec provides `vec_distance_cosine(blob1, blob2)`.
vec_sql = """
SELECT id, content, vec_distance_cosine(embedding, ?) AS distance
FROM memories
WHERE user_id = ?
ORDER BY distance
LIMIT ?
"""
vec_rows = conn.execute(
vec_sql,
(keyword_blob, user_id, top_k),
).fetchall()
except Exception as e:
# if anything goes wrong (extension missing, error, etc) we just
# fall back to the keyword search below
logger.error(f"Vector search failed: {e}")
vec_rows = []
# keyword search (using the existing method in the store)
kw_rows = store.search_by_keyword(user_id=user_id, keyword=keyword)
# Note: store.search_by_keyword returns a list of dicts (because of the dict(row))
seen_ids = set()
merged_contents = []
# process vector results first
for row in vec_rows:
# row is a sqlite3.Row (because we set row_factory to sqlite3.Row in the store's __init__)
mem_id = row['id']
if mem_id not in seen_ids:
seen_ids.add(mem_id)
merged_contents.append(row['content'])
if len(merged_contents) >= top_k:
break
# If we still need more results, add from keyword results
if len(merged_contents) < top_k:
for row in kw_rows:
mem_id = row['id']
if mem_id not in seen_ids:
seen_ids.add(mem_id)
merged_contents.append(row['content'])
if len(merged_contents) >= top_k:
break
return "\n".join(merged_contents) if merged_contents else "No matching memories"
def memory_list() -> str:
rows = store.list_memories(user_id=user_id)
hits = [row['content'] for row in rows]
return "\n".join(hits) if hits else "No memories stored yet."
registry = {
"memory_search": memory_search,
"memory_list": memory_list,
}
return registry
def dispatch(name: str, arguments: str, registry: dict) -> str:
"""
Look up the tool, parse its JSON args, run it, return a string result
"""
logger.info(f"Tool calls: {name}({arguments})")
fn = registry.get(name)
if fn is None:
return f"Error: Unknown tool '{name}'"
try:
args = json.loads(arguments)
return fn(**args)
except (json.JSONDecodeError, TypeError) as e:
return f"Error calling {name}: {e}"