llmemory 0.1.10 → 0.1.11
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.
- checksums.yaml +4 -4
- data/README.md +183 -0
- data/exe/llmemory-mcp +44 -0
- data/lib/llmemory/cli/commands/mcp.rb +129 -0
- data/lib/llmemory/cli.rb +4 -1
- data/lib/llmemory/long_term/file_based/storages/active_record_storage.rb +32 -0
- data/lib/llmemory/long_term/file_based/storages/base.rb +8 -0
- data/lib/llmemory/long_term/file_based/storages/database_storage.rb +34 -0
- data/lib/llmemory/long_term/file_based/storages/memory_storage.rb +36 -0
- data/lib/llmemory/long_term/graph_based/storages/active_record_storage.rb +26 -0
- data/lib/llmemory/long_term/graph_based/storages/base.rb +4 -0
- data/lib/llmemory/long_term/graph_based/storages/memory_storage.rb +23 -0
- data/lib/llmemory/mcp/authentication.rb +70 -0
- data/lib/llmemory/mcp/server.rb +185 -0
- data/lib/llmemory/mcp/tools/memory_add_message.rb +47 -0
- data/lib/llmemory/mcp/tools/memory_consolidate.rb +57 -0
- data/lib/llmemory/mcp/tools/memory_info.rb +79 -0
- data/lib/llmemory/mcp/tools/memory_retrieve.rb +122 -0
- data/lib/llmemory/mcp/tools/memory_save.rb +58 -0
- data/lib/llmemory/mcp/tools/memory_search.rb +134 -0
- data/lib/llmemory/mcp/tools/memory_stats.rb +111 -0
- data/lib/llmemory/mcp/tools/memory_timeline.rb +120 -0
- data/lib/llmemory/mcp/tools/memory_timeline_context.rb +140 -0
- data/lib/llmemory/mcp.rb +8 -0
- data/lib/llmemory/version.rb +1 -1
- metadata +30 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2016468aa862476d2bfc4fafef85e340433b0bf940519351a9fb72f3bda5e796
|
|
4
|
+
data.tar.gz: 31d1d72a3ef8b94b6cef5e696ed29f72357377670d484897350cddcdf17a20ae
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f360315f571e5641f53af4006f661a9e432f80280d53a2a7f99d7563f318f23b04950925d2bb2e0716bcad6088b5f929fa15d2b25523f7d83af04b0ac269db3b
|
|
7
|
+
data.tar.gz: aef3ce481555a0f84630207ff0333fe749d841656dd41c1c6449b4e3797cff73ecd033ce1817ff50d8e87913e783dfed3198eb85ccadfd6683388f9a4e5a8780
|
data/README.md
CHANGED
|
@@ -259,6 +259,189 @@ end
|
|
|
259
259
|
|
|
260
260
|
The dashboard uses your existing `Llmemory.configuration` (short-term store, long-term store/type, etc.) and does not add any gem dependency; it only runs when Rails is present and you require `llmemory/dashboard`.
|
|
261
261
|
|
|
262
|
+
## MCP Server (Model Context Protocol)
|
|
263
|
+
|
|
264
|
+
llmemory includes an MCP server that allows LLM agents (like Claude Code) to interact directly with the memory system. This gives agents "agency" over their own memory—they can search, save, and retrieve memories autonomously.
|
|
265
|
+
|
|
266
|
+
### Starting the Server
|
|
267
|
+
|
|
268
|
+
**Stdio mode** (default, for local use with Claude Code):
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
# Via CLI
|
|
272
|
+
llmemory mcp serve
|
|
273
|
+
|
|
274
|
+
# Or via standalone executable
|
|
275
|
+
llmemory-mcp
|
|
276
|
+
|
|
277
|
+
# With custom server name
|
|
278
|
+
llmemory mcp serve --name my-memory
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
**HTTP mode** (for remote access or web integrations):
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
# Start HTTP server on default port 3100
|
|
285
|
+
llmemory mcp serve --http
|
|
286
|
+
|
|
287
|
+
# Custom port and host
|
|
288
|
+
llmemory mcp serve --http --port 8080 --host 127.0.0.1
|
|
289
|
+
|
|
290
|
+
# With authentication (recommended for HTTP/HTTPS)
|
|
291
|
+
MCP_TOKEN=your-secret-token llmemory mcp serve --http
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
**HTTPS mode** (secure remote access):
|
|
295
|
+
|
|
296
|
+
```bash
|
|
297
|
+
# Start HTTPS server with SSL certificates
|
|
298
|
+
llmemory mcp serve --http --port 443 \
|
|
299
|
+
--ssl-cert /path/to/cert.pem \
|
|
300
|
+
--ssl-key /path/to/key.pem
|
|
301
|
+
|
|
302
|
+
# With authentication (strongly recommended)
|
|
303
|
+
MCP_TOKEN=your-secret-token llmemory mcp serve --http --port 443 \
|
|
304
|
+
--ssl-cert /path/to/cert.pem \
|
|
305
|
+
--ssl-key /path/to/key.pem
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### Available Tools
|
|
309
|
+
|
|
310
|
+
| Tool | Description |
|
|
311
|
+
|------|-------------|
|
|
312
|
+
| `memory_search` | Search memories by semantic query |
|
|
313
|
+
| `memory_save` | Save new observations/facts to long-term memory |
|
|
314
|
+
| `memory_retrieve` | Get context optimized for LLM inference (supports timeline context) |
|
|
315
|
+
| `memory_timeline` | Get chronological timeline of recent memories |
|
|
316
|
+
| `memory_timeline_context` | Get N items before/after a specific memory |
|
|
317
|
+
| `memory_add_message` | Add message to short-term conversation |
|
|
318
|
+
| `memory_consolidate` | Extract facts from conversation to long-term |
|
|
319
|
+
| `memory_stats` | Get memory statistics for a user |
|
|
320
|
+
| `memory_info` | Documentation on how to use the tools |
|
|
321
|
+
|
|
322
|
+
### Configuration for Claude Code
|
|
323
|
+
|
|
324
|
+
Add to `~/.claude/claude_code_config.json`:
|
|
325
|
+
|
|
326
|
+
```json
|
|
327
|
+
{
|
|
328
|
+
"mcpServers": {
|
|
329
|
+
"llmemory": {
|
|
330
|
+
"command": "llmemory",
|
|
331
|
+
"args": ["mcp", "serve"],
|
|
332
|
+
"env": {
|
|
333
|
+
"OPENAI_API_KEY": "sk-..."
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
Or with the standalone executable:
|
|
341
|
+
|
|
342
|
+
```json
|
|
343
|
+
{
|
|
344
|
+
"mcpServers": {
|
|
345
|
+
"llmemory": {
|
|
346
|
+
"command": "llmemory-mcp"
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### Environment Variables
|
|
353
|
+
|
|
354
|
+
| Variable | Description |
|
|
355
|
+
|----------|-------------|
|
|
356
|
+
| `MCP_TOKEN` | Token for HTTP authentication (if set, requests must include valid token) |
|
|
357
|
+
| `LLMEMORY_DEBUG` | Set to `1` to enable debug output on stderr |
|
|
358
|
+
| `OPENAI_API_KEY` | API key for LLM/embeddings |
|
|
359
|
+
| `REDIS_URL` | Redis URL for short-term store |
|
|
360
|
+
| `DATABASE_URL` | Database URL for persistence |
|
|
361
|
+
|
|
362
|
+
### HTTP Authentication
|
|
363
|
+
|
|
364
|
+
When `MCP_TOKEN` is set, the HTTP server requires authentication. Requests must include the token via:
|
|
365
|
+
|
|
366
|
+
- **Authorization header**: `Authorization: Bearer <token>` or `Authorization: <token>`
|
|
367
|
+
- **Query parameter**: `?token=<token>`
|
|
368
|
+
|
|
369
|
+
Example with curl:
|
|
370
|
+
|
|
371
|
+
```bash
|
|
372
|
+
# Using Authorization header
|
|
373
|
+
curl -H "Authorization: Bearer your-secret-token" \
|
|
374
|
+
-H "Accept: application/json, text/event-stream" \
|
|
375
|
+
-H "Content-Type: application/json" \
|
|
376
|
+
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' \
|
|
377
|
+
http://localhost:3100/
|
|
378
|
+
|
|
379
|
+
# Using query parameter
|
|
380
|
+
curl "http://localhost:3100/?token=your-secret-token" \
|
|
381
|
+
-H "Accept: application/json, text/event-stream" \
|
|
382
|
+
-H "Content-Type: application/json" \
|
|
383
|
+
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}'
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
### Recommended Workflow
|
|
387
|
+
|
|
388
|
+
1. **Start of conversation**: Use `memory_retrieve` to get relevant context
|
|
389
|
+
2. **During conversation**: Use `memory_save` for important observations
|
|
390
|
+
3. **End of conversation**: Use `memory_consolidate` to persist facts
|
|
391
|
+
|
|
392
|
+
### Timeline Context
|
|
393
|
+
|
|
394
|
+
The `memory_retrieve` tool supports **timeline context** - showing N events before and after matched memories. This provides situational context around relevant memories:
|
|
395
|
+
|
|
396
|
+
```json
|
|
397
|
+
{
|
|
398
|
+
"name": "memory_retrieve",
|
|
399
|
+
"arguments": {
|
|
400
|
+
"query": "trabajo",
|
|
401
|
+
"user_id": "user123",
|
|
402
|
+
"include_timeline_context": true,
|
|
403
|
+
"timeline_window": 3
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
This returns:
|
|
409
|
+
- Recent conversation (short-term)
|
|
410
|
+
- Relevant memories (long-term)
|
|
411
|
+
- **Timeline context**: 3 events before and after each match
|
|
412
|
+
|
|
413
|
+
You can also use `memory_timeline_context` directly to explore temporal context around a specific memory:
|
|
414
|
+
|
|
415
|
+
```json
|
|
416
|
+
{
|
|
417
|
+
"name": "memory_timeline_context",
|
|
418
|
+
"arguments": {
|
|
419
|
+
"user_id": "user123",
|
|
420
|
+
"item_id": "item_42",
|
|
421
|
+
"before": 5,
|
|
422
|
+
"after": 5
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
Example output:
|
|
428
|
+
```
|
|
429
|
+
Timeline Context around 'item_42':
|
|
430
|
+
|
|
431
|
+
BEFORE (3 items):
|
|
432
|
+
- [2024-01-14] [personal] Usuario vive en Madrid
|
|
433
|
+
- [2024-01-15] [technical] Usuario programa en Python
|
|
434
|
+
- [2024-01-16] [preferences] Usuario usa VS Code
|
|
435
|
+
|
|
436
|
+
TARGET:
|
|
437
|
+
>>> [2024-01-17] [work] Usuario trabaja en Acme Corp
|
|
438
|
+
|
|
439
|
+
AFTER (3 items):
|
|
440
|
+
- [2024-01-18] [personal] Usuario tiene un gato
|
|
441
|
+
- [2024-01-19] [work] Usuario lidera equipo backend
|
|
442
|
+
- [2024-01-20] [preferences] Usuario prefiere café
|
|
443
|
+
```
|
|
444
|
+
|
|
262
445
|
## License
|
|
263
446
|
|
|
264
447
|
MIT. See [LICENSE.txt](LICENSE.txt).
|
data/exe/llmemory-mcp
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__)) unless $LOAD_PATH.include?(File.expand_path("../lib", __dir__))
|
|
5
|
+
|
|
6
|
+
require "llmemory"
|
|
7
|
+
require "llmemory/mcp"
|
|
8
|
+
|
|
9
|
+
# Parse minimal arguments
|
|
10
|
+
name = "llmemory"
|
|
11
|
+
ARGV.each_with_index do |arg, i|
|
|
12
|
+
if arg == "--name" && ARGV[i + 1]
|
|
13
|
+
name = ARGV[i + 1]
|
|
14
|
+
elsif arg == "--help" || arg == "-h"
|
|
15
|
+
puts "Usage: llmemory-mcp [--name NAME]"
|
|
16
|
+
puts ""
|
|
17
|
+
puts "Starts an MCP (Model Context Protocol) server for llmemory."
|
|
18
|
+
puts ""
|
|
19
|
+
puts "Options:"
|
|
20
|
+
puts " --name NAME Server name (default: llmemory)"
|
|
21
|
+
puts " --help, -h Show this help"
|
|
22
|
+
puts ""
|
|
23
|
+
puts "Environment variables:"
|
|
24
|
+
puts " LLMEMORY_DEBUG Set to 1 to enable debug output on stderr"
|
|
25
|
+
puts ""
|
|
26
|
+
puts "Example configuration for Claude Code (~/.claude/claude_code_config.json):"
|
|
27
|
+
puts ' {'
|
|
28
|
+
puts ' "mcpServers": {'
|
|
29
|
+
puts ' "llmemory": {'
|
|
30
|
+
puts ' "command": "llmemory-mcp"'
|
|
31
|
+
puts ' }'
|
|
32
|
+
puts ' }'
|
|
33
|
+
puts ' }'
|
|
34
|
+
exit 0
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Silence stderr for clean protocol unless debugging
|
|
39
|
+
unless ENV["LLMEMORY_DEBUG"]
|
|
40
|
+
$stderr.reopen(File::NULL, "w")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
server = Llmemory::MCP::Server.new(name: name)
|
|
44
|
+
server.run_stdio
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Llmemory
|
|
6
|
+
module Cli
|
|
7
|
+
module Commands
|
|
8
|
+
class Mcp < Base
|
|
9
|
+
def option_parser(parser)
|
|
10
|
+
@server_name = "llmemory"
|
|
11
|
+
@http_mode = false
|
|
12
|
+
@port = 3100
|
|
13
|
+
@host = "0.0.0.0"
|
|
14
|
+
@ssl_cert = nil
|
|
15
|
+
@ssl_key = nil
|
|
16
|
+
|
|
17
|
+
parser.banner = "Usage: llmemory mcp [serve] [options]"
|
|
18
|
+
parser.on("--name NAME", "Server name (default: llmemory)") { |v| @server_name = v }
|
|
19
|
+
parser.on("--http", "Run as HTTP server instead of stdio") { @http_mode = true }
|
|
20
|
+
parser.on("--port PORT", Integer, "HTTP port (default: 3100)") { |v| @port = v }
|
|
21
|
+
parser.on("--host HOST", "HTTP host (default: 0.0.0.0)") { |v| @host = v }
|
|
22
|
+
parser.on("--ssl-cert FILE", "SSL certificate file for HTTPS") { |v| @ssl_cert = v }
|
|
23
|
+
parser.on("--ssl-key FILE", "SSL private key file for HTTPS") { |v| @ssl_key = v }
|
|
24
|
+
parser.on("-h", "--help", "Show this help") do
|
|
25
|
+
puts parser
|
|
26
|
+
exit
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def execute(argv, _opts)
|
|
31
|
+
action = argv.shift || "serve"
|
|
32
|
+
|
|
33
|
+
case action
|
|
34
|
+
when "serve"
|
|
35
|
+
run_server
|
|
36
|
+
when "help", "--help", "-h"
|
|
37
|
+
show_help
|
|
38
|
+
else
|
|
39
|
+
$stderr.puts "Unknown MCP action: #{action}"
|
|
40
|
+
$stderr.puts "Usage: llmemory mcp [serve] [options]"
|
|
41
|
+
exit 1
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def show_help
|
|
48
|
+
puts "Usage: llmemory mcp [serve] [options]"
|
|
49
|
+
puts ""
|
|
50
|
+
puts "Options:"
|
|
51
|
+
puts " --name NAME Server name (default: llmemory)"
|
|
52
|
+
puts " --http Run as HTTP server instead of stdio"
|
|
53
|
+
puts " --port PORT HTTP port (default: 3100)"
|
|
54
|
+
puts " --host HOST HTTP host (default: 0.0.0.0)"
|
|
55
|
+
puts " --ssl-cert FILE SSL certificate file for HTTPS"
|
|
56
|
+
puts " --ssl-key FILE SSL private key file for HTTPS"
|
|
57
|
+
puts ""
|
|
58
|
+
puts "Environment Variables:"
|
|
59
|
+
puts " MCP_TOKEN If set, enables token authentication for HTTP mode"
|
|
60
|
+
puts " LLMEMORY_DEBUG Set to 1 to enable debug output"
|
|
61
|
+
puts ""
|
|
62
|
+
puts "Authentication (HTTP/HTTPS mode only):"
|
|
63
|
+
puts " When MCP_TOKEN is set, requests must include either:"
|
|
64
|
+
puts " - Authorization header: 'Bearer <token>' or '<token>'"
|
|
65
|
+
puts " - Query parameter: '?token=<token>'"
|
|
66
|
+
puts ""
|
|
67
|
+
puts "Starts an MCP (Model Context Protocol) server that exposes llmemory"
|
|
68
|
+
puts "tools for use with LLM agents like Claude Code."
|
|
69
|
+
puts ""
|
|
70
|
+
puts "Example configuration for Claude Code (~/.claude/claude_code_config.json):"
|
|
71
|
+
puts ' {'
|
|
72
|
+
puts ' "mcpServers": {'
|
|
73
|
+
puts ' "llmemory": {'
|
|
74
|
+
puts ' "command": "llmemory",'
|
|
75
|
+
puts ' "args": ["mcp", "serve"]'
|
|
76
|
+
puts ' }'
|
|
77
|
+
puts ' }'
|
|
78
|
+
puts ' }'
|
|
79
|
+
puts ""
|
|
80
|
+
puts "HTTP mode example:"
|
|
81
|
+
puts " MCP_TOKEN=secret123 llmemory mcp serve --http --port 3100"
|
|
82
|
+
puts ""
|
|
83
|
+
puts "HTTPS mode example:"
|
|
84
|
+
puts " MCP_TOKEN=secret123 llmemory mcp serve --http --port 443 \\"
|
|
85
|
+
puts " --ssl-cert /path/to/cert.pem --ssl-key /path/to/key.pem"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def run_server
|
|
89
|
+
require_relative "../../mcp"
|
|
90
|
+
|
|
91
|
+
server = Llmemory::MCP::Server.new(name: @server_name)
|
|
92
|
+
|
|
93
|
+
if @http_mode
|
|
94
|
+
validate_ssl_options!
|
|
95
|
+
server.run_http(port: @port, host: @host, ssl_cert: @ssl_cert, ssl_key: @ssl_key)
|
|
96
|
+
else
|
|
97
|
+
# Silence stderr to avoid interference with MCP protocol
|
|
98
|
+
# unless debugging is enabled
|
|
99
|
+
unless ENV["LLMEMORY_DEBUG"]
|
|
100
|
+
$stderr.reopen(File::NULL, "w")
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
server.run_stdio
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def validate_ssl_options!
|
|
108
|
+
# If one SSL option is provided, both must be provided
|
|
109
|
+
if (@ssl_cert && !@ssl_key) || (!@ssl_cert && @ssl_key)
|
|
110
|
+
$stderr.puts "Error: Both --ssl-cert and --ssl-key must be provided for HTTPS"
|
|
111
|
+
exit 1
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Validate files exist if SSL is enabled
|
|
115
|
+
if @ssl_cert && @ssl_key
|
|
116
|
+
unless File.exist?(@ssl_cert)
|
|
117
|
+
$stderr.puts "Error: SSL certificate file not found: #{@ssl_cert}"
|
|
118
|
+
exit 1
|
|
119
|
+
end
|
|
120
|
+
unless File.exist?(@ssl_key)
|
|
121
|
+
$stderr.puts "Error: SSL key file not found: #{@ssl_key}"
|
|
122
|
+
exit 1
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
data/lib/llmemory/cli.rb
CHANGED
|
@@ -7,6 +7,7 @@ require_relative "cli/commands/short_term"
|
|
|
7
7
|
require_relative "cli/commands/long_term"
|
|
8
8
|
require_relative "cli/commands/stats"
|
|
9
9
|
require_relative "cli/commands/search"
|
|
10
|
+
require_relative "cli/commands/mcp"
|
|
10
11
|
|
|
11
12
|
module Llmemory
|
|
12
13
|
class CLI
|
|
@@ -47,7 +48,8 @@ module Llmemory
|
|
|
47
48
|
"edges" => Cli::Commands::LongTerm::Edges,
|
|
48
49
|
"graph" => Cli::Commands::LongTerm::Graph,
|
|
49
50
|
"search" => Cli::Commands::Search,
|
|
50
|
-
"stats" => Cli::Commands::Stats
|
|
51
|
+
"stats" => Cli::Commands::Stats,
|
|
52
|
+
"mcp" => Cli::Commands::Mcp
|
|
51
53
|
}[normalized]
|
|
52
54
|
end
|
|
53
55
|
|
|
@@ -68,6 +70,7 @@ module Llmemory
|
|
|
68
70
|
graph USER_ID Export graph (--format dot|json)
|
|
69
71
|
search USER_ID "query" Search in memory
|
|
70
72
|
stats [USER_ID] Show statistics
|
|
73
|
+
mcp [serve] Start MCP server for LLM agents
|
|
71
74
|
|
|
72
75
|
Run 'llmemory <command> --help' for command-specific options.
|
|
73
76
|
HELP
|
|
@@ -138,8 +138,40 @@ module Llmemory
|
|
|
138
138
|
LlmemoryItem.where(user_id: user_id).count
|
|
139
139
|
end
|
|
140
140
|
|
|
141
|
+
def get_items_around(user_id, reference, before: 5, after: 5)
|
|
142
|
+
items = get_all_items(user_id)
|
|
143
|
+
find_around(items, reference, before, after)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def get_resources_around(user_id, reference, before: 5, after: 5)
|
|
147
|
+
resources = get_all_resources(user_id)
|
|
148
|
+
find_around(resources, reference, before, after)
|
|
149
|
+
end
|
|
150
|
+
|
|
141
151
|
private
|
|
142
152
|
|
|
153
|
+
def find_around(items, reference, before, after)
|
|
154
|
+
return { before: [], target: nil, after: [] } if items.empty?
|
|
155
|
+
|
|
156
|
+
idx = if reference.is_a?(String) && reference.match?(/^\d{4}-/)
|
|
157
|
+
target_time = Time.parse(reference)
|
|
158
|
+
items.index { |i| i[:created_at] >= target_time } || items.size
|
|
159
|
+
else
|
|
160
|
+
items.index { |i| i[:id] == reference }
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
return { before: [], target: nil, after: [] } unless idx
|
|
164
|
+
|
|
165
|
+
start_idx = [idx - before, 0].max
|
|
166
|
+
end_idx = [idx + after, items.size - 1].min
|
|
167
|
+
|
|
168
|
+
{
|
|
169
|
+
before: items[start_idx...idx] || [],
|
|
170
|
+
target: items[idx],
|
|
171
|
+
after: items[(idx + 1)..end_idx] || []
|
|
172
|
+
}
|
|
173
|
+
end
|
|
174
|
+
|
|
143
175
|
def sanitize_like(str)
|
|
144
176
|
(str || "").to_s.gsub(/[%_\\]/) { |c| "\\#{c}" }
|
|
145
177
|
end
|
|
@@ -80,6 +80,14 @@ module Llmemory
|
|
|
80
80
|
def count_items(user_id:)
|
|
81
81
|
raise NotImplementedError, "#{self.class}#count_items must be implemented"
|
|
82
82
|
end
|
|
83
|
+
|
|
84
|
+
def get_items_around(user_id, reference, before: 5, after: 5)
|
|
85
|
+
raise NotImplementedError, "#{self.class}#get_items_around must be implemented"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def get_resources_around(user_id, reference, before: 5, after: 5)
|
|
89
|
+
raise NotImplementedError, "#{self.class}#get_resources_around must be implemented"
|
|
90
|
+
end
|
|
83
91
|
end
|
|
84
92
|
end
|
|
85
93
|
end
|
|
@@ -197,8 +197,42 @@ module Llmemory
|
|
|
197
197
|
result.first["c"].to_i
|
|
198
198
|
end
|
|
199
199
|
|
|
200
|
+
def get_items_around(user_id, reference, before: 5, after: 5)
|
|
201
|
+
ensure_tables!
|
|
202
|
+
items = get_all_items(user_id)
|
|
203
|
+
find_around(items, reference, before, after)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def get_resources_around(user_id, reference, before: 5, after: 5)
|
|
207
|
+
ensure_tables!
|
|
208
|
+
resources = get_all_resources(user_id)
|
|
209
|
+
find_around(resources, reference, before, after)
|
|
210
|
+
end
|
|
211
|
+
|
|
200
212
|
private
|
|
201
213
|
|
|
214
|
+
def find_around(items, reference, before, after)
|
|
215
|
+
return { before: [], target: nil, after: [] } if items.empty?
|
|
216
|
+
|
|
217
|
+
idx = if reference.is_a?(String) && reference.match?(/^\d{4}-/)
|
|
218
|
+
target_time = Time.parse(reference)
|
|
219
|
+
items.index { |i| i[:created_at] >= target_time } || items.size
|
|
220
|
+
else
|
|
221
|
+
items.index { |i| i[:id] == reference }
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
return { before: [], target: nil, after: [] } unless idx
|
|
225
|
+
|
|
226
|
+
start_idx = [idx - before, 0].max
|
|
227
|
+
end_idx = [idx + after, items.size - 1].min
|
|
228
|
+
|
|
229
|
+
{
|
|
230
|
+
before: items[start_idx...idx] || [],
|
|
231
|
+
target: items[idx],
|
|
232
|
+
after: items[(idx + 1)..end_idx] || []
|
|
233
|
+
}
|
|
234
|
+
end
|
|
235
|
+
|
|
202
236
|
def conn
|
|
203
237
|
@connection ||= begin
|
|
204
238
|
require "pg"
|
|
@@ -116,6 +116,42 @@ module Llmemory
|
|
|
116
116
|
def count_items(user_id:)
|
|
117
117
|
@items[user_id].size
|
|
118
118
|
end
|
|
119
|
+
|
|
120
|
+
def get_items_around(user_id, reference, before: 5, after: 5)
|
|
121
|
+
items = @items[user_id].sort_by { |i| i[:created_at] }
|
|
122
|
+
find_around(items, reference, :id, before, after)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def get_resources_around(user_id, reference, before: 5, after: 5)
|
|
126
|
+
resources = @resources[user_id].sort_by { |r| r[:created_at] }
|
|
127
|
+
find_around(resources, reference, :id, before, after)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
private
|
|
131
|
+
|
|
132
|
+
def find_around(items, reference, id_key, before, after)
|
|
133
|
+
return { before: [], target: nil, after: [] } if items.empty?
|
|
134
|
+
|
|
135
|
+
idx = if reference.is_a?(String) && reference.match?(/^\d{4}-/)
|
|
136
|
+
# ISO timestamp - find closest item at or after this time
|
|
137
|
+
target_time = Time.parse(reference)
|
|
138
|
+
items.index { |i| i[:created_at] >= target_time } || items.size
|
|
139
|
+
else
|
|
140
|
+
# Item ID
|
|
141
|
+
items.index { |i| i[id_key] == reference }
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
return { before: [], target: nil, after: [] } unless idx
|
|
145
|
+
|
|
146
|
+
start_idx = [idx - before, 0].max
|
|
147
|
+
end_idx = [idx + after, items.size - 1].min
|
|
148
|
+
|
|
149
|
+
{
|
|
150
|
+
before: items[start_idx...idx] || [],
|
|
151
|
+
target: items[idx],
|
|
152
|
+
after: items[(idx + 1)..end_idx] || []
|
|
153
|
+
}
|
|
154
|
+
end
|
|
119
155
|
end
|
|
120
156
|
end
|
|
121
157
|
end
|
|
@@ -123,6 +123,32 @@ module Llmemory
|
|
|
123
123
|
LlmemoryGraphEdge.where(user_id: user_id, archived_at: nil).count
|
|
124
124
|
end
|
|
125
125
|
|
|
126
|
+
def get_edges_around(user_id, reference, before: 5, after: 5)
|
|
127
|
+
edges = LlmemoryGraphEdge.where(user_id: user_id, archived_at: nil)
|
|
128
|
+
.order(:created_at)
|
|
129
|
+
.map { |r| record_to_edge(r) }
|
|
130
|
+
|
|
131
|
+
return { before: [], target: nil, after: [] } if edges.empty?
|
|
132
|
+
|
|
133
|
+
idx = if reference.is_a?(String) && reference.match?(/^\d{4}-/)
|
|
134
|
+
target_time = Time.parse(reference)
|
|
135
|
+
edges.index { |e| e.created_at >= target_time } || edges.size
|
|
136
|
+
else
|
|
137
|
+
edges.index { |e| e.id == reference || e.id.to_s == reference.to_s }
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
return { before: [], target: nil, after: [] } unless idx
|
|
141
|
+
|
|
142
|
+
start_idx = [idx - before, 0].max
|
|
143
|
+
end_idx = [idx + after, edges.size - 1].min
|
|
144
|
+
|
|
145
|
+
{
|
|
146
|
+
before: edges[start_idx...idx] || [],
|
|
147
|
+
target: edges[idx],
|
|
148
|
+
after: edges[(idx + 1)..end_idx] || []
|
|
149
|
+
}
|
|
150
|
+
end
|
|
151
|
+
|
|
126
152
|
private
|
|
127
153
|
|
|
128
154
|
def record_to_node(r)
|
|
@@ -48,6 +48,10 @@ module Llmemory
|
|
|
48
48
|
def count_edges(user_id)
|
|
49
49
|
raise NotImplementedError, "#{self.class}#count_edges must be implemented"
|
|
50
50
|
end
|
|
51
|
+
|
|
52
|
+
def get_edges_around(user_id, reference, before: 5, after: 5)
|
|
53
|
+
raise NotImplementedError, "#{self.class}#get_edges_around must be implemented"
|
|
54
|
+
end
|
|
51
55
|
end
|
|
52
56
|
end
|
|
53
57
|
end
|
|
@@ -118,6 +118,29 @@ module Llmemory
|
|
|
118
118
|
def count_edges(user_id)
|
|
119
119
|
@edges[user_id].count { |e| !e.archived? }
|
|
120
120
|
end
|
|
121
|
+
|
|
122
|
+
def get_edges_around(user_id, reference, before: 5, after: 5)
|
|
123
|
+
edges = @edges[user_id].reject(&:archived?).sort_by(&:created_at)
|
|
124
|
+
return { before: [], target: nil, after: [] } if edges.empty?
|
|
125
|
+
|
|
126
|
+
idx = if reference.is_a?(String) && reference.match?(/^\d{4}-/)
|
|
127
|
+
target_time = Time.parse(reference)
|
|
128
|
+
edges.index { |e| e.created_at >= target_time } || edges.size
|
|
129
|
+
else
|
|
130
|
+
edges.index { |e| e.id == reference }
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
return { before: [], target: nil, after: [] } unless idx
|
|
134
|
+
|
|
135
|
+
start_idx = [idx - before, 0].max
|
|
136
|
+
end_idx = [idx + after, edges.size - 1].min
|
|
137
|
+
|
|
138
|
+
{
|
|
139
|
+
before: edges[start_idx...idx] || [],
|
|
140
|
+
target: edges[idx],
|
|
141
|
+
after: edges[(idx + 1)..end_idx] || []
|
|
142
|
+
}
|
|
143
|
+
end
|
|
121
144
|
end
|
|
122
145
|
end
|
|
123
146
|
end
|