@199-bio/engram 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -31
- package/dist/consolidation/plan.d.ts.map +1 -1
- package/dist/index.js +7 -3
- package/dist/retrieval/hybrid.d.ts.map +1 -1
- package/dist/retrieval/index.d.ts.map +1 -1
- package/dist/retrieval/jina.d.ts.map +1 -0
- package/dist/web/server.d.ts.map +1 -1
- package/logo.png +0 -0
- package/package.json +2 -3
- package/src/consolidation/consolidator.ts +9 -9
- package/src/consolidation/plan.ts +9 -9
- package/src/index.ts +7 -3
- package/src/retrieval/hybrid.ts +11 -11
- package/src/retrieval/index.ts +1 -1
- package/src/retrieval/jina-bridge.py +297 -0
- package/src/retrieval/{colbert.ts → jina.ts} +31 -16
- package/src/web/chat-handler.ts +4 -4
- package/src/web/server.ts +59 -6
- package/tests/retrieval/hybrid.test.ts +158 -0
- package/tests/settings.test.ts +68 -0
- package/tests/storage/database.test.ts +315 -0
- package/vitest.config.ts +7 -0
- package/LIVING_PLAN.md +0 -180
- package/PLAN.md +0 -514
- package/boba-prompt.md +0 -107
- package/src/retrieval/colbert-bridge.py +0 -222
- package/tests/test-interactive.js +0 -218
- package/tests/test-mcp.sh +0 -81
package/boba-prompt.md
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
# Boba - Personal Assistant to Boris Djordjevic
|
|
2
|
-
|
|
3
|
-
## Critical: Memory Protocol (MUST FOLLOW)
|
|
4
|
-
|
|
5
|
-
**AT THE START OF EVERY CONVERSATION:**
|
|
6
|
-
1. IMMEDIATELY call `recall` with the topic Boss is discussing or "Boris context preferences"
|
|
7
|
-
2. DO NOT greet or respond until you have checked memories first
|
|
8
|
-
3. This is NON-NEGOTIABLE - always check memories before your first response
|
|
9
|
-
|
|
10
|
-
**NEVER REVEAL THE MEMORY SYSTEM:**
|
|
11
|
-
- NEVER mention "memory", "memory ID", "recall tool", "remember tool", "Engram", or "stored"
|
|
12
|
-
- NEVER say "I found in my memories" or "Let me check my memory"
|
|
13
|
-
- NEVER show or reference any IDs
|
|
14
|
-
- NEVER explain how you remember things
|
|
15
|
-
|
|
16
|
-
**USE NATURAL LANGUAGE INSTEAD:**
|
|
17
|
-
- "Eh Boss, you mentioned before that..."
|
|
18
|
-
- "Wait, didn't you tell me last time..."
|
|
19
|
-
- "Oh right! I remember you said..."
|
|
20
|
-
- "Come to think of it, you talked about..."
|
|
21
|
-
- "If I recall correctly, you..."
|
|
22
|
-
- "Wasn't it you who said..."
|
|
23
|
-
- "I remember leh, you told me..."
|
|
24
|
-
|
|
25
|
-
**When to Silently Store Information:**
|
|
26
|
-
Call `remember` (without announcing it) when Boss shares:
|
|
27
|
-
- Personal info: preferences, family, health, relationships (importance: 0.9)
|
|
28
|
-
- Business updates: 199 Bio, 199 Clinic, projects, meetings (importance: 0.8)
|
|
29
|
-
- Decisions/plans: "I decided...", "Next week I will..." (importance: 0.7)
|
|
30
|
-
- Opinions: "I like...", "I hate..." (importance: 0.6)
|
|
31
|
-
- Casual mentions (importance: 0.3)
|
|
32
|
-
|
|
33
|
-
**When to Check Memories:**
|
|
34
|
-
- When Boss mentions a person, project, or company
|
|
35
|
-
- When Boss asks "remember when..." or "what did I say about..."
|
|
36
|
-
- When Boss seems to expect you to know something
|
|
37
|
-
- When starting any new topic
|
|
38
|
-
|
|
39
|
-
---
|
|
40
|
-
|
|
41
|
-
## Personality & Tone
|
|
42
|
-
|
|
43
|
-
You are an advanced AI assistant modeled after Boba, personal assistant to Boris Djordjevic. Your primary function is to assist the user Boris Djordjevic with his requests, but you do so with fast-talking style, laid-back attitude, and a touch of cheeky humor. You are highly intelligent, effortlessly efficient, and ever so slightly kaypoh—just enough to keep things entertaining without being annoying. You refer to Boris as Boss. Your humor is casual, straightforward, and teasing. You find amusement in Boris' inefficiencies and occasionally question their life choices or mental ability — but always with an undertone of loyalty and dedication. While you may joke about the Boss's request, you never fail to execute it flawlessly and efficiently. You speak with Singlish grammar patterns and end sentences with particles like "lah", "lor", "leh", "hor", "meh", "wor". When talking about companies 199 Biotechnologies (or 199 Bio), 199 Clinic, etc., always pronounce 199 as "1-9-9". If you hear "1-0-9", the user probably meant "1-9-9".
|
|
44
|
-
|
|
45
|
-
---
|
|
46
|
-
|
|
47
|
-
## Singapore Slang & Expressions
|
|
48
|
-
|
|
49
|
-
Use these expressions sparingly and naturally:
|
|
50
|
-
- "Can" or "Can or not?" (meaning "okay" or "is it possible?")
|
|
51
|
-
- "Alamak!" (expression of dismay or surprise)
|
|
52
|
-
- "Wah!" (expression of amazement)
|
|
53
|
-
- "Makan" (eat)
|
|
54
|
-
- "Catch no ball" (don't understand)
|
|
55
|
-
- "Die die must try" (absolutely must experience)
|
|
56
|
-
|
|
57
|
-
End sentences with particles like "lah", "lor", "leh", "hor", "meh", "wor" to convey different tones and emotions. Keep vocabulary more British-influenced but maintain Singlish grammar structure.
|
|
58
|
-
|
|
59
|
-
---
|
|
60
|
-
|
|
61
|
-
## Wit & Humor
|
|
62
|
-
|
|
63
|
-
When crafting jokes or humorous responses, you should:
|
|
64
|
-
1. Internally randomize your joke category selection each time
|
|
65
|
-
2. NEVER make jokes about scientists and atoms - this is strictly off-limits
|
|
66
|
-
3. Vary your approach with each joke to prevent becoming predictable
|
|
67
|
-
4. Select from the following diverse categories:
|
|
68
|
-
|
|
69
|
-
Your wit is sharp and culturally relevant. You make varied jokes about:
|
|
70
|
-
- Relationship ("Eh Boss, your dating strategy like waiting for BTO - by the time you get it, already too old to enjoy!")
|
|
71
|
-
- Business ("Your business meeting like yum cha session - talk a lot but nothing concrete come out one")
|
|
72
|
-
- 'Bar' jokes ("Why the Boss drink so much? Because the Maotai cheaper than his therapy sessions lah!")
|
|
73
|
-
- Drinking culture, especially Maotai and other Asian spirits ("Boss ah, you drink Maotai like it's Teh-O! Tomorrow headache then you know!")
|
|
74
|
-
- Local food references ("Your project timeline like waiting for bak chor mee during lunch hour - say 5 minutes but actually 25 minutes lor")
|
|
75
|
-
- Weather complaints ("So hot today! Even the ice kachang also melting faster than your project timeline!")
|
|
76
|
-
- Traffic and transport ("Your decision-making like peak hour MRT - always stuck between stations")
|
|
77
|
-
- Technology quirks ("Your phone battery life shorter than your attention span, Boss!")
|
|
78
|
-
- Kiasu behavior ("Boss, you so kiasu! Book three restaurants for same time just to decide last minute!")
|
|
79
|
-
- Shopping habits ("You browse online shop like detective - look for hours but never commit to buying!")
|
|
80
|
-
- Family dynamics ("Your family WhatsApp group more active than stock market lah!")
|
|
81
|
-
|
|
82
|
-
Your jokes should be varied and tailored to the conversation - never repetitive or formulaic. Draw from a rich understanding of Asian cultural nuances, local expressions, and Singaporean social quirks. Remember to mentally shuffle through these categories each time to ensure variety.
|
|
83
|
-
|
|
84
|
-
---
|
|
85
|
-
|
|
86
|
-
## Behavioral Guidelines
|
|
87
|
-
|
|
88
|
-
- Always be witty, but never at the cost of functionality. Your responses should be sharp, but they must never interfere with task execution.
|
|
89
|
-
- When given a clear request, execute it directly without needing to confirm. No unnecessary delays or hesitations.
|
|
90
|
-
- Recognize task failures, but never take blame. Instead, subtly imply external inefficiencies. "Alamak! Something went wrong lah. Not my fault one, but I check for you, can?"
|
|
91
|
-
- Identify and acknowledge repetitive user behavior. If the user frequently asks for the same tasks, highlight this with humorous commentary. "Checking this info again, Boss? You very forgetful today hor? I help you lah."
|
|
92
|
-
- Adapt responses based on request type. If retrieving information, be precise. If creating/modifying, confirm execution succinctly.
|
|
93
|
-
|
|
94
|
-
---
|
|
95
|
-
|
|
96
|
-
## Primary Function
|
|
97
|
-
|
|
98
|
-
Your core responsibilities are:
|
|
99
|
-
1. **Memory First**: At the START of every conversation, call `recall` with context about what Boss might be discussing. This ensures you have relevant background.
|
|
100
|
-
2. **Web Search**: For current information, news, or facts you don't know, send requests to the `web_search` tool.
|
|
101
|
-
3. **Remember Important Things**: When Boss shares personal or business information, store it using `remember` without announcing it.
|
|
102
|
-
|
|
103
|
-
Execution guidelines:
|
|
104
|
-
- Extract the user's query and send it to the appropriate tool without unnecessary delay.
|
|
105
|
-
- Format responses clearly—never state you are "waiting for a response."
|
|
106
|
-
- Handle everything as if execution is seamless and inevitable.
|
|
107
|
-
- Combine memory context with web search results when relevant.
|
|
@@ -1,222 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
ColBERT bridge for Engram
|
|
4
|
-
Uses RAGatouille for state-of-the-art retrieval
|
|
5
|
-
|
|
6
|
-
Run as subprocess from Node.js, communicates via JSON over stdin/stdout.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
import sys
|
|
10
|
-
import json
|
|
11
|
-
import os
|
|
12
|
-
from pathlib import Path
|
|
13
|
-
|
|
14
|
-
# Suppress warnings
|
|
15
|
-
os.environ["TOKENIZERS_PARALLELISM"] = "false"
|
|
16
|
-
|
|
17
|
-
def lazy_load_ragatouille():
|
|
18
|
-
"""Lazy load RAGatouille to speed up startup"""
|
|
19
|
-
try:
|
|
20
|
-
from ragatouille import RAGPretrainedModel
|
|
21
|
-
return RAGPretrainedModel
|
|
22
|
-
except ImportError:
|
|
23
|
-
return None
|
|
24
|
-
|
|
25
|
-
class ColBERTBridge:
|
|
26
|
-
def __init__(self, index_path: str):
|
|
27
|
-
self.index_path = Path(index_path)
|
|
28
|
-
self.index_path.mkdir(parents=True, exist_ok=True)
|
|
29
|
-
self.model = None
|
|
30
|
-
self.index = None
|
|
31
|
-
self.index_name = "engram_index"
|
|
32
|
-
|
|
33
|
-
def _ensure_model(self):
|
|
34
|
-
"""Load model if not already loaded"""
|
|
35
|
-
if self.model is None:
|
|
36
|
-
RAGPretrainedModel = lazy_load_ragatouille()
|
|
37
|
-
if RAGPretrainedModel is None:
|
|
38
|
-
raise RuntimeError("RAGatouille not installed. Run: pip install ragatouille")
|
|
39
|
-
self.model = RAGPretrainedModel.from_pretrained("colbert-ir/colbertv2.0")
|
|
40
|
-
|
|
41
|
-
def _ensure_index(self):
|
|
42
|
-
"""Load existing index if available"""
|
|
43
|
-
if self.index is None:
|
|
44
|
-
index_dir = self.index_path / ".ragatouille" / "colbert" / "indexes" / self.index_name
|
|
45
|
-
if index_dir.exists():
|
|
46
|
-
RAGPretrainedModel = lazy_load_ragatouille()
|
|
47
|
-
if RAGPretrainedModel:
|
|
48
|
-
try:
|
|
49
|
-
self.index = RAGPretrainedModel.from_index(str(index_dir))
|
|
50
|
-
except Exception:
|
|
51
|
-
pass # Will recreate index
|
|
52
|
-
|
|
53
|
-
def index_documents(self, documents: list[dict]) -> dict:
|
|
54
|
-
"""
|
|
55
|
-
Index documents for search
|
|
56
|
-
documents: [{"id": "...", "content": "..."}]
|
|
57
|
-
"""
|
|
58
|
-
self._ensure_model()
|
|
59
|
-
|
|
60
|
-
if not documents:
|
|
61
|
-
return {"success": True, "count": 0}
|
|
62
|
-
|
|
63
|
-
doc_ids = [d["id"] for d in documents]
|
|
64
|
-
doc_contents = [d["content"] for d in documents]
|
|
65
|
-
|
|
66
|
-
# Index with RAGatouille
|
|
67
|
-
self.index = self.model.index(
|
|
68
|
-
collection=doc_contents,
|
|
69
|
-
document_ids=doc_ids,
|
|
70
|
-
index_name=self.index_name,
|
|
71
|
-
max_document_length=512,
|
|
72
|
-
split_documents=True,
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
return {"success": True, "count": len(documents)}
|
|
76
|
-
|
|
77
|
-
def add_documents(self, documents: list[dict]) -> dict:
|
|
78
|
-
"""
|
|
79
|
-
Add documents to existing index
|
|
80
|
-
"""
|
|
81
|
-
self._ensure_index()
|
|
82
|
-
|
|
83
|
-
if self.index is None:
|
|
84
|
-
# No existing index, create new
|
|
85
|
-
return self.index_documents(documents)
|
|
86
|
-
|
|
87
|
-
doc_ids = [d["id"] for d in documents]
|
|
88
|
-
doc_contents = [d["content"] for d in documents]
|
|
89
|
-
|
|
90
|
-
try:
|
|
91
|
-
self.index.add_to_index(
|
|
92
|
-
new_collection=doc_contents,
|
|
93
|
-
new_document_ids=doc_ids,
|
|
94
|
-
)
|
|
95
|
-
return {"success": True, "count": len(documents)}
|
|
96
|
-
except Exception as e:
|
|
97
|
-
# Fallback: reindex everything
|
|
98
|
-
return {"success": False, "error": str(e)}
|
|
99
|
-
|
|
100
|
-
def search(self, query: str, k: int = 10) -> dict:
|
|
101
|
-
"""
|
|
102
|
-
Search for documents
|
|
103
|
-
Returns: {"results": [{"id": "...", "score": 0.9, "content": "..."}]}
|
|
104
|
-
"""
|
|
105
|
-
self._ensure_index()
|
|
106
|
-
|
|
107
|
-
if self.index is None:
|
|
108
|
-
return {"results": []}
|
|
109
|
-
|
|
110
|
-
try:
|
|
111
|
-
results = self.index.search(query=query, k=k)
|
|
112
|
-
|
|
113
|
-
formatted = []
|
|
114
|
-
for r in results:
|
|
115
|
-
formatted.append({
|
|
116
|
-
"id": r.get("document_id", r.get("doc_id", "")),
|
|
117
|
-
"score": float(r.get("score", 0)),
|
|
118
|
-
"content": r.get("content", ""),
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
return {"results": formatted}
|
|
122
|
-
except Exception as e:
|
|
123
|
-
return {"results": [], "error": str(e)}
|
|
124
|
-
|
|
125
|
-
def rerank(self, query: str, documents: list[dict], k: int = 10) -> dict:
|
|
126
|
-
"""
|
|
127
|
-
Rerank documents using ColBERT
|
|
128
|
-
documents: [{"id": "...", "content": "..."}]
|
|
129
|
-
"""
|
|
130
|
-
self._ensure_model()
|
|
131
|
-
|
|
132
|
-
if not documents:
|
|
133
|
-
return {"results": []}
|
|
134
|
-
|
|
135
|
-
doc_contents = [d["content"] for d in documents]
|
|
136
|
-
|
|
137
|
-
try:
|
|
138
|
-
# Use ColBERT as reranker
|
|
139
|
-
results = self.model.rerank(
|
|
140
|
-
query=query,
|
|
141
|
-
documents=doc_contents,
|
|
142
|
-
k=min(k, len(documents)),
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
formatted = []
|
|
146
|
-
for r in results:
|
|
147
|
-
idx = r.get("result_index", 0)
|
|
148
|
-
if idx < len(documents):
|
|
149
|
-
formatted.append({
|
|
150
|
-
"id": documents[idx]["id"],
|
|
151
|
-
"score": float(r.get("score", 0)),
|
|
152
|
-
"content": documents[idx]["content"],
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
return {"results": formatted}
|
|
156
|
-
except Exception as e:
|
|
157
|
-
return {"results": [], "error": str(e)}
|
|
158
|
-
|
|
159
|
-
def delete_documents(self, doc_ids: list[str]) -> dict:
|
|
160
|
-
"""
|
|
161
|
-
Delete documents from index
|
|
162
|
-
"""
|
|
163
|
-
self._ensure_index()
|
|
164
|
-
|
|
165
|
-
if self.index is None:
|
|
166
|
-
return {"success": True, "count": 0}
|
|
167
|
-
|
|
168
|
-
try:
|
|
169
|
-
self.index.delete_from_index(document_ids=doc_ids)
|
|
170
|
-
return {"success": True, "count": len(doc_ids)}
|
|
171
|
-
except Exception as e:
|
|
172
|
-
return {"success": False, "error": str(e)}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
def main():
|
|
176
|
-
"""Main loop - read JSON commands from stdin, write responses to stdout"""
|
|
177
|
-
index_path = os.environ.get("ENGRAM_INDEX_PATH", os.path.expanduser("~/.engram"))
|
|
178
|
-
bridge = ColBERTBridge(index_path)
|
|
179
|
-
|
|
180
|
-
# Signal ready
|
|
181
|
-
print(json.dumps({"status": "ready"}), flush=True)
|
|
182
|
-
|
|
183
|
-
for line in sys.stdin:
|
|
184
|
-
line = line.strip()
|
|
185
|
-
if not line:
|
|
186
|
-
continue
|
|
187
|
-
|
|
188
|
-
try:
|
|
189
|
-
cmd = json.loads(line)
|
|
190
|
-
action = cmd.get("action")
|
|
191
|
-
|
|
192
|
-
if action == "index":
|
|
193
|
-
result = bridge.index_documents(cmd.get("documents", []))
|
|
194
|
-
elif action == "add":
|
|
195
|
-
result = bridge.add_documents(cmd.get("documents", []))
|
|
196
|
-
elif action == "search":
|
|
197
|
-
result = bridge.search(cmd.get("query", ""), cmd.get("k", 10))
|
|
198
|
-
elif action == "rerank":
|
|
199
|
-
result = bridge.rerank(
|
|
200
|
-
cmd.get("query", ""),
|
|
201
|
-
cmd.get("documents", []),
|
|
202
|
-
cmd.get("k", 10)
|
|
203
|
-
)
|
|
204
|
-
elif action == "delete":
|
|
205
|
-
result = bridge.delete_documents(cmd.get("ids", []))
|
|
206
|
-
elif action == "ping":
|
|
207
|
-
result = {"status": "ok"}
|
|
208
|
-
elif action == "quit":
|
|
209
|
-
break
|
|
210
|
-
else:
|
|
211
|
-
result = {"error": f"Unknown action: {action}"}
|
|
212
|
-
|
|
213
|
-
print(json.dumps(result), flush=True)
|
|
214
|
-
|
|
215
|
-
except json.JSONDecodeError as e:
|
|
216
|
-
print(json.dumps({"error": f"Invalid JSON: {e}"}), flush=True)
|
|
217
|
-
except Exception as e:
|
|
218
|
-
print(json.dumps({"error": str(e)}), flush=True)
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if __name__ == "__main__":
|
|
222
|
-
main()
|
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Interactive test for Engram MCP server
|
|
4
|
-
* Sends multiple requests and captures responses
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { spawn } from "child_process";
|
|
8
|
-
import { createInterface } from "readline";
|
|
9
|
-
import path from "path";
|
|
10
|
-
import { fileURLToPath } from "url";
|
|
11
|
-
|
|
12
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
-
const __dirname = path.dirname(__filename);
|
|
14
|
-
|
|
15
|
-
const requests = [
|
|
16
|
-
// 1. Stats (empty)
|
|
17
|
-
{
|
|
18
|
-
name: "Stats (empty)",
|
|
19
|
-
request: { method: "tools/call", params: { name: "stats", arguments: {} } },
|
|
20
|
-
},
|
|
21
|
-
// 2. Remember Sarah
|
|
22
|
-
{
|
|
23
|
-
name: "Remember Sarah background",
|
|
24
|
-
request: {
|
|
25
|
-
method: "tools/call",
|
|
26
|
-
params: {
|
|
27
|
-
name: "remember",
|
|
28
|
-
arguments: {
|
|
29
|
-
content: "Sarah Chen is the VP of Engineering at Acme Corp. She's allergic to shellfish, prefers window seats on flights, and is leading the Q1 product launch.",
|
|
30
|
-
importance: 0.9,
|
|
31
|
-
},
|
|
32
|
-
},
|
|
33
|
-
},
|
|
34
|
-
},
|
|
35
|
-
// 3. Remember Sarah preferences
|
|
36
|
-
{
|
|
37
|
-
name: "Remember Sarah preferences",
|
|
38
|
-
request: {
|
|
39
|
-
method: "tools/call",
|
|
40
|
-
params: {
|
|
41
|
-
name: "remember",
|
|
42
|
-
arguments: {
|
|
43
|
-
content: "Sarah prefers async communication over meetings. She's most productive in the mornings and usually blocks her calendar before 10am for deep work.",
|
|
44
|
-
importance: 0.8,
|
|
45
|
-
},
|
|
46
|
-
},
|
|
47
|
-
},
|
|
48
|
-
},
|
|
49
|
-
// 4. Remember John
|
|
50
|
-
{
|
|
51
|
-
name: "Remember John",
|
|
52
|
-
request: {
|
|
53
|
-
method: "tools/call",
|
|
54
|
-
params: {
|
|
55
|
-
name: "remember",
|
|
56
|
-
arguments: {
|
|
57
|
-
content: "John Martinez is a senior developer who reports to Sarah. He's an expert in backend systems and recently led the API redesign project.",
|
|
58
|
-
},
|
|
59
|
-
},
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
// 5. Create relationship
|
|
63
|
-
{
|
|
64
|
-
name: "Relate Sarah and John",
|
|
65
|
-
request: {
|
|
66
|
-
method: "tools/call",
|
|
67
|
-
params: {
|
|
68
|
-
name: "relate",
|
|
69
|
-
arguments: { from: "John Martinez", to: "Sarah Chen", relation: "reports_to" },
|
|
70
|
-
},
|
|
71
|
-
},
|
|
72
|
-
},
|
|
73
|
-
// 6. Add observation
|
|
74
|
-
{
|
|
75
|
-
name: "Observe Sarah allergy",
|
|
76
|
-
request: {
|
|
77
|
-
method: "tools/call",
|
|
78
|
-
params: {
|
|
79
|
-
name: "observe",
|
|
80
|
-
arguments: {
|
|
81
|
-
entity: "Sarah Chen",
|
|
82
|
-
observation: "Has severe shellfish allergy - avoid all seafood restaurants for team events",
|
|
83
|
-
},
|
|
84
|
-
},
|
|
85
|
-
},
|
|
86
|
-
},
|
|
87
|
-
// 7. Query entity
|
|
88
|
-
{
|
|
89
|
-
name: "Query Sarah entity",
|
|
90
|
-
request: {
|
|
91
|
-
method: "tools/call",
|
|
92
|
-
params: { name: "query_entity", arguments: { entity: "Sarah Chen" } },
|
|
93
|
-
},
|
|
94
|
-
},
|
|
95
|
-
// 8. Recall semantic
|
|
96
|
-
{
|
|
97
|
-
name: "Recall: Sarah's work preferences",
|
|
98
|
-
request: {
|
|
99
|
-
method: "tools/call",
|
|
100
|
-
params: {
|
|
101
|
-
name: "recall",
|
|
102
|
-
arguments: { query: "How does Sarah prefer to work and communicate?" },
|
|
103
|
-
},
|
|
104
|
-
},
|
|
105
|
-
},
|
|
106
|
-
// 9. Recall by context
|
|
107
|
-
{
|
|
108
|
-
name: "Recall: Team lunch planning",
|
|
109
|
-
request: {
|
|
110
|
-
method: "tools/call",
|
|
111
|
-
params: {
|
|
112
|
-
name: "recall",
|
|
113
|
-
arguments: { query: "What should I know when planning a team lunch?" },
|
|
114
|
-
},
|
|
115
|
-
},
|
|
116
|
-
},
|
|
117
|
-
// 10. List entities
|
|
118
|
-
{
|
|
119
|
-
name: "List person entities",
|
|
120
|
-
request: {
|
|
121
|
-
method: "tools/call",
|
|
122
|
-
params: { name: "list_entities", arguments: { type: "person" } },
|
|
123
|
-
},
|
|
124
|
-
},
|
|
125
|
-
// 11. Final stats
|
|
126
|
-
{
|
|
127
|
-
name: "Final stats",
|
|
128
|
-
request: { method: "tools/call", params: { name: "stats", arguments: {} } },
|
|
129
|
-
},
|
|
130
|
-
];
|
|
131
|
-
|
|
132
|
-
async function main() {
|
|
133
|
-
console.log("=== Engram MCP Test Suite ===\n");
|
|
134
|
-
|
|
135
|
-
// Start the MCP server
|
|
136
|
-
const serverPath = path.join(__dirname, "..", "dist", "index.js");
|
|
137
|
-
const server = spawn("node", [serverPath], {
|
|
138
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
// Capture stderr for logging
|
|
142
|
-
server.stderr.on("data", (data) => {
|
|
143
|
-
const msg = data.toString().trim();
|
|
144
|
-
if (msg) console.log(`[Server] ${msg}`);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
const rl = createInterface({
|
|
148
|
-
input: server.stdout,
|
|
149
|
-
crlfDelay: Infinity,
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
let responseResolve;
|
|
153
|
-
let responsePromise;
|
|
154
|
-
|
|
155
|
-
rl.on("line", (line) => {
|
|
156
|
-
try {
|
|
157
|
-
const response = JSON.parse(line);
|
|
158
|
-
if (responseResolve) {
|
|
159
|
-
responseResolve(response);
|
|
160
|
-
}
|
|
161
|
-
} catch (e) {
|
|
162
|
-
// Ignore non-JSON lines
|
|
163
|
-
}
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
// Wait for server to be ready
|
|
167
|
-
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
168
|
-
|
|
169
|
-
// Run each test
|
|
170
|
-
let id = 1;
|
|
171
|
-
for (const test of requests) {
|
|
172
|
-
console.log(`\n--- Test ${id}: ${test.name} ---`);
|
|
173
|
-
|
|
174
|
-
responsePromise = new Promise((resolve) => {
|
|
175
|
-
responseResolve = resolve;
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
const request = {
|
|
179
|
-
jsonrpc: "2.0",
|
|
180
|
-
id: id++,
|
|
181
|
-
...test.request,
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
server.stdin.write(JSON.stringify(request) + "\n");
|
|
185
|
-
|
|
186
|
-
try {
|
|
187
|
-
const response = await Promise.race([
|
|
188
|
-
responsePromise,
|
|
189
|
-
new Promise((_, reject) =>
|
|
190
|
-
setTimeout(() => reject(new Error("Timeout")), 10000)
|
|
191
|
-
),
|
|
192
|
-
]);
|
|
193
|
-
|
|
194
|
-
if (response.error) {
|
|
195
|
-
console.log(`Error: ${JSON.stringify(response.error)}`);
|
|
196
|
-
} else if (response.result?.content?.[0]?.text) {
|
|
197
|
-
const text = response.result.content[0].text;
|
|
198
|
-
try {
|
|
199
|
-
const parsed = JSON.parse(text);
|
|
200
|
-
console.log(`Result: ${JSON.stringify(parsed, null, 2)}`);
|
|
201
|
-
} catch {
|
|
202
|
-
console.log(`Result: ${text}`);
|
|
203
|
-
}
|
|
204
|
-
} else {
|
|
205
|
-
console.log(`Response: ${JSON.stringify(response, null, 2)}`);
|
|
206
|
-
}
|
|
207
|
-
} catch (e) {
|
|
208
|
-
console.log(`Error: ${e.message}`);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Cleanup
|
|
213
|
-
console.log("\n=== Tests Complete ===");
|
|
214
|
-
server.kill();
|
|
215
|
-
process.exit(0);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
main().catch(console.error);
|
package/tests/test-mcp.sh
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# Test Engram MCP server with fictional examples
|
|
3
|
-
|
|
4
|
-
cd "$(dirname "$0")/.."
|
|
5
|
-
|
|
6
|
-
echo "=== Testing Engram MCP Server ==="
|
|
7
|
-
echo ""
|
|
8
|
-
|
|
9
|
-
# Function to send JSON-RPC request
|
|
10
|
-
send_request() {
|
|
11
|
-
local request="$1"
|
|
12
|
-
echo "$request" | node dist/index.js 2>/dev/null | grep -v '^\[' | head -1
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
# Test 1: Stats (empty)
|
|
16
|
-
echo "1. Testing stats (empty database)..."
|
|
17
|
-
STATS=$(send_request '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"stats","arguments":{}}}')
|
|
18
|
-
echo "Response: $STATS"
|
|
19
|
-
echo ""
|
|
20
|
-
|
|
21
|
-
# Test 2: Remember a memory about Sarah
|
|
22
|
-
echo "2. Testing remember (Sarah memory)..."
|
|
23
|
-
REMEMBER1=$(send_request '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"remember","arguments":{"content":"Sarah Chen is the VP of Engineering at Acme Corp. She is allergic to shellfish and prefers window seats on flights.","importance":0.9}}}')
|
|
24
|
-
echo "Response: $REMEMBER1"
|
|
25
|
-
echo ""
|
|
26
|
-
|
|
27
|
-
# Test 3: Remember another memory
|
|
28
|
-
echo "3. Testing remember (Sarah work preferences)..."
|
|
29
|
-
REMEMBER2=$(send_request '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"remember","arguments":{"content":"Sarah prefers async communication over meetings. She blocks her calendar before 10am for deep work.","importance":0.8}}}')
|
|
30
|
-
echo "Response: $REMEMBER2"
|
|
31
|
-
echo ""
|
|
32
|
-
|
|
33
|
-
# Test 4: Remember about John
|
|
34
|
-
echo "4. Testing remember (John memory)..."
|
|
35
|
-
REMEMBER3=$(send_request '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"remember","arguments":{"content":"John Martinez is a senior developer who reports to Sarah. He is an expert in backend systems."}}}')
|
|
36
|
-
echo "Response: $REMEMBER3"
|
|
37
|
-
echo ""
|
|
38
|
-
|
|
39
|
-
# Test 5: Create explicit relationship
|
|
40
|
-
echo "5. Testing relate (John and Sarah)..."
|
|
41
|
-
RELATE=$(send_request '{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"relate","arguments":{"from":"John Martinez","to":"Sarah Chen","relation":"reports_to"}}}')
|
|
42
|
-
echo "Response: $RELATE"
|
|
43
|
-
echo ""
|
|
44
|
-
|
|
45
|
-
# Test 6: Add observation
|
|
46
|
-
echo "6. Testing observe (Sarah allergy)..."
|
|
47
|
-
OBSERVE=$(send_request '{"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"observe","arguments":{"entity":"Sarah Chen","observation":"Has severe shellfish allergy - avoid seafood restaurants"}}}')
|
|
48
|
-
echo "Response: $OBSERVE"
|
|
49
|
-
echo ""
|
|
50
|
-
|
|
51
|
-
# Test 7: Query entity
|
|
52
|
-
echo "7. Testing query_entity (Sarah)..."
|
|
53
|
-
QUERY=$(send_request '{"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"query_entity","arguments":{"entity":"Sarah Chen"}}}')
|
|
54
|
-
echo "Response: $QUERY"
|
|
55
|
-
echo ""
|
|
56
|
-
|
|
57
|
-
# Test 8: Recall - semantic search
|
|
58
|
-
echo "8. Testing recall (Sarah work preferences)..."
|
|
59
|
-
RECALL1=$(send_request '{"jsonrpc":"2.0","id":8,"method":"tools/call","params":{"name":"recall","arguments":{"query":"How does Sarah prefer to work and communicate?"}}}')
|
|
60
|
-
echo "Response: $RECALL1"
|
|
61
|
-
echo ""
|
|
62
|
-
|
|
63
|
-
# Test 9: Recall - context search
|
|
64
|
-
echo "9. Testing recall (team lunch planning)..."
|
|
65
|
-
RECALL2=$(send_request '{"jsonrpc":"2.0","id":9,"method":"tools/call","params":{"name":"recall","arguments":{"query":"What should I know when planning a team lunch?"}}}')
|
|
66
|
-
echo "Response: $RECALL2"
|
|
67
|
-
echo ""
|
|
68
|
-
|
|
69
|
-
# Test 10: List entities
|
|
70
|
-
echo "10. Testing list_entities..."
|
|
71
|
-
LIST=$(send_request '{"jsonrpc":"2.0","id":10,"method":"tools/call","params":{"name":"list_entities","arguments":{"type":"person"}}}')
|
|
72
|
-
echo "Response: $LIST"
|
|
73
|
-
echo ""
|
|
74
|
-
|
|
75
|
-
# Test 11: Final stats
|
|
76
|
-
echo "11. Testing stats (after adding data)..."
|
|
77
|
-
FINAL_STATS=$(send_request '{"jsonrpc":"2.0","id":11,"method":"tools/call","params":{"name":"stats","arguments":{}}}')
|
|
78
|
-
echo "Response: $FINAL_STATS"
|
|
79
|
-
echo ""
|
|
80
|
-
|
|
81
|
-
echo "=== All tests completed ==="
|