supermemory 0.1.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +24 -0
- data/LICENSE +21 -0
- data/README.md +503 -0
- data/lib/supermemory/client.rb +193 -0
- data/lib/supermemory/configuration.rb +25 -0
- data/lib/supermemory/errors.rb +84 -0
- data/lib/supermemory/integrations/graph_agent.rb +222 -0
- data/lib/supermemory/integrations/langchain.rb +235 -0
- data/lib/supermemory/integrations/openai.rb +294 -0
- data/lib/supermemory/resources/base.rb +15 -0
- data/lib/supermemory/resources/connections.rb +104 -0
- data/lib/supermemory/resources/documents.rb +115 -0
- data/lib/supermemory/resources/memories.rb +36 -0
- data/lib/supermemory/resources/search.rb +68 -0
- data/lib/supermemory/resources/settings.rb +59 -0
- data/lib/supermemory/version.rb +5 -0
- data/lib/supermemory.rb +23 -0
- metadata +173 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
require "faraday/multipart"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module Supermemory
|
|
8
|
+
class Client
|
|
9
|
+
RETRYABLE_STATUS_CODES = [408, 409, 429, 500, 502, 503, 504].freeze
|
|
10
|
+
INITIAL_RETRY_DELAY = 0.5
|
|
11
|
+
MAX_RETRY_DELAY = 8.0
|
|
12
|
+
|
|
13
|
+
attr_reader :api_key, :base_url, :timeout, :max_retries
|
|
14
|
+
|
|
15
|
+
# @param api_key [String, nil] API key (defaults to SUPERMEMORY_API_KEY env var)
|
|
16
|
+
# @param base_url [String, nil] Base URL (defaults to https://api.supermemory.ai)
|
|
17
|
+
# @param timeout [Integer] Request timeout in seconds (default: 60)
|
|
18
|
+
# @param max_retries [Integer] Max retry attempts (default: 2)
|
|
19
|
+
# @param extra_headers [Hash] Additional headers to send with every request
|
|
20
|
+
def initialize(api_key: nil, base_url: nil, timeout: nil, max_retries: nil, extra_headers: {})
|
|
21
|
+
config = Supermemory.configuration
|
|
22
|
+
@api_key = api_key || config.api_key
|
|
23
|
+
@base_url = base_url || config.base_url
|
|
24
|
+
@timeout = timeout || config.timeout
|
|
25
|
+
@max_retries = max_retries || config.max_retries
|
|
26
|
+
@extra_headers = extra_headers.merge(config.extra_headers)
|
|
27
|
+
|
|
28
|
+
unless @api_key
|
|
29
|
+
raise Supermemory::Error.new("API key is required. Set via Supermemory.configure or pass api_key:")
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Top-level convenience: add a document
|
|
34
|
+
# @param content [String] Text, URL, or file content
|
|
35
|
+
# @param options [Hash] Additional parameters (container_tag, custom_id, metadata, entity_context)
|
|
36
|
+
# @return [Hash] { "id" => "...", "status" => "..." }
|
|
37
|
+
def add(content:, **options)
|
|
38
|
+
documents.add(content: content, **options)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Top-level convenience: get user profile
|
|
42
|
+
# @param container_tag [String] User/project identifier
|
|
43
|
+
# @param q [String, nil] Optional search query
|
|
44
|
+
# @param threshold [Float, nil] Score threshold for search results
|
|
45
|
+
# @return [Hash]
|
|
46
|
+
def profile(container_tag:, q: nil, threshold: nil)
|
|
47
|
+
body = { container_tag: container_tag }
|
|
48
|
+
body[:q] = q if q
|
|
49
|
+
body[:threshold] = threshold if threshold
|
|
50
|
+
post("/v4/profile", body)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @return [Supermemory::Resources::Documents]
|
|
54
|
+
def documents
|
|
55
|
+
@documents ||= Resources::Documents.new(self)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @return [Supermemory::Resources::Search]
|
|
59
|
+
def search
|
|
60
|
+
@search ||= Resources::Search.new(self)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# @return [Supermemory::Resources::Memories]
|
|
64
|
+
def memories
|
|
65
|
+
@memories ||= Resources::Memories.new(self)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @return [Supermemory::Resources::Settings]
|
|
69
|
+
def settings
|
|
70
|
+
@settings ||= Resources::Settings.new(self)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# @return [Supermemory::Resources::Connections]
|
|
74
|
+
def connections
|
|
75
|
+
@connections ||= Resources::Connections.new(self)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Low-level HTTP methods
|
|
79
|
+
|
|
80
|
+
# @param path [String]
|
|
81
|
+
# @param params [Hash, nil]
|
|
82
|
+
# @return [Hash, nil]
|
|
83
|
+
def get(path, params = nil)
|
|
84
|
+
request(:get, path, params: params)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# @param path [String]
|
|
88
|
+
# @param body [Hash, nil]
|
|
89
|
+
# @return [Hash, nil]
|
|
90
|
+
def post(path, body = nil)
|
|
91
|
+
request(:post, path, body: body)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# @param path [String]
|
|
95
|
+
# @param body [Hash, nil]
|
|
96
|
+
# @return [Hash, nil]
|
|
97
|
+
def patch(path, body = nil)
|
|
98
|
+
request(:patch, path, body: body)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# @param path [String]
|
|
102
|
+
# @param body [Hash, nil]
|
|
103
|
+
# @return [Hash, nil]
|
|
104
|
+
def delete(path, body = nil)
|
|
105
|
+
request(:delete, path, body: body)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# @param path [String]
|
|
109
|
+
# @param body [Hash]
|
|
110
|
+
# @return [Hash, nil]
|
|
111
|
+
def multipart_post(path, body)
|
|
112
|
+
request(:post, path, body: body, multipart: true)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
def request(method, path, body: nil, params: nil, multipart: false)
|
|
118
|
+
attempts = 0
|
|
119
|
+
|
|
120
|
+
begin
|
|
121
|
+
response = connection(multipart: multipart).run_request(method, path, nil, nil) do |req|
|
|
122
|
+
req.params = params if params
|
|
123
|
+
if multipart
|
|
124
|
+
body.each { |k, v| req.body[k] = v }
|
|
125
|
+
elsif body
|
|
126
|
+
req.body = body.to_json
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
handle_response(response)
|
|
131
|
+
rescue Faraday::TimeoutError => e
|
|
132
|
+
raise Supermemory::APITimeoutError.new("Request timed out: #{e.message}")
|
|
133
|
+
rescue Faraday::ConnectionFailed => e
|
|
134
|
+
raise Supermemory::APIConnectionError.new("Connection failed: #{e.message}")
|
|
135
|
+
rescue Supermemory::APIError => e
|
|
136
|
+
attempts += 1
|
|
137
|
+
if attempts <= @max_retries && retryable?(e.status)
|
|
138
|
+
sleep(retry_delay(attempts))
|
|
139
|
+
retry
|
|
140
|
+
end
|
|
141
|
+
raise
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def handle_response(response)
|
|
146
|
+
status = response.status
|
|
147
|
+
body = parse_body(response.body)
|
|
148
|
+
headers = response.headers
|
|
149
|
+
|
|
150
|
+
return body if status >= 200 && status < 300
|
|
151
|
+
|
|
152
|
+
error_class = ERROR_MAP[status] || (status >= 500 ? InternalServerError : APIError)
|
|
153
|
+
message = body.is_a?(Hash) ? (body["error"] || body["message"] || body.to_s) : body.to_s
|
|
154
|
+
raise error_class.new(message, status: status, body: body, headers: headers)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def parse_body(body)
|
|
158
|
+
return nil if body.nil? || body.empty?
|
|
159
|
+
|
|
160
|
+
JSON.parse(body)
|
|
161
|
+
rescue JSON::ParserError
|
|
162
|
+
body
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def connection(multipart: false)
|
|
166
|
+
@connections_cache ||= {}
|
|
167
|
+
cache_key = multipart ? :multipart : :json
|
|
168
|
+
@connections_cache[cache_key] ||= Faraday.new(url: @base_url) do |f|
|
|
169
|
+
f.options.timeout = @timeout
|
|
170
|
+
f.options.open_timeout = 5
|
|
171
|
+
if multipart
|
|
172
|
+
f.request :multipart
|
|
173
|
+
else
|
|
174
|
+
f.headers["Content-Type"] = "application/json"
|
|
175
|
+
end
|
|
176
|
+
f.headers["Authorization"] = "Bearer #{@api_key}"
|
|
177
|
+
@extra_headers.each { |k, v| f.headers[k.to_s] = v.to_s }
|
|
178
|
+
f.adapter Faraday.default_adapter
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def retryable?(status)
|
|
183
|
+
RETRYABLE_STATUS_CODES.include?(status)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def retry_delay(attempt)
|
|
187
|
+
delay = INITIAL_RETRY_DELAY * (2**(attempt - 1))
|
|
188
|
+
delay = [delay, MAX_RETRY_DELAY].min
|
|
189
|
+
jitter = delay * 0.25 * ((rand * 2) - 1)
|
|
190
|
+
delay + jitter
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Supermemory
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_accessor :api_key, :base_url, :timeout, :max_retries, :extra_headers
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@api_key = ENV.fetch("SUPERMEMORY_API_KEY", nil)
|
|
9
|
+
@base_url = ENV.fetch("SUPERMEMORY_BASE_URL", "https://api.supermemory.ai")
|
|
10
|
+
@timeout = 60
|
|
11
|
+
@max_retries = 2
|
|
12
|
+
@extra_headers = {}
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
def configuration
|
|
18
|
+
@configuration ||= Configuration.new
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def configure
|
|
22
|
+
yield(configuration)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Supermemory
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
class APIError < Error
|
|
7
|
+
attr_reader :status, :body, :headers
|
|
8
|
+
|
|
9
|
+
def initialize(message = nil, status: nil, body: nil, headers: nil)
|
|
10
|
+
@status = status
|
|
11
|
+
@body = body
|
|
12
|
+
@headers = headers
|
|
13
|
+
super(message || default_message)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def default_message
|
|
19
|
+
"API error (status: #{status})"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class BadRequestError < APIError
|
|
24
|
+
def initialize(message = nil, **kwargs)
|
|
25
|
+
super(message, status: 400, **kwargs)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class AuthenticationError < APIError
|
|
30
|
+
def initialize(message = nil, **kwargs)
|
|
31
|
+
super(message, status: 401, **kwargs)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
class PermissionDeniedError < APIError
|
|
36
|
+
def initialize(message = nil, **kwargs)
|
|
37
|
+
super(message, status: 403, **kwargs)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
class NotFoundError < APIError
|
|
42
|
+
def initialize(message = nil, **kwargs)
|
|
43
|
+
super(message, status: 404, **kwargs)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class ConflictError < APIError
|
|
48
|
+
def initialize(message = nil, **kwargs)
|
|
49
|
+
super(message, status: 409, **kwargs)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
class UnprocessableEntityError < APIError
|
|
54
|
+
def initialize(message = nil, **kwargs)
|
|
55
|
+
super(message, status: 422, **kwargs)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
class RateLimitError < APIError
|
|
60
|
+
def initialize(message = nil, **kwargs)
|
|
61
|
+
super(message, status: 429, **kwargs)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
class InternalServerError < APIError
|
|
66
|
+
def initialize(message = nil, **kwargs)
|
|
67
|
+
super(message, status: 500, **kwargs)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
class APIConnectionError < Error; end
|
|
72
|
+
class APITimeoutError < APIConnectionError; end
|
|
73
|
+
|
|
74
|
+
# @private
|
|
75
|
+
ERROR_MAP = {
|
|
76
|
+
400 => BadRequestError,
|
|
77
|
+
401 => AuthenticationError,
|
|
78
|
+
403 => PermissionDeniedError,
|
|
79
|
+
404 => NotFoundError,
|
|
80
|
+
409 => ConflictError,
|
|
81
|
+
422 => UnprocessableEntityError,
|
|
82
|
+
429 => RateLimitError
|
|
83
|
+
}.freeze
|
|
84
|
+
end
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "supermemory"
|
|
4
|
+
|
|
5
|
+
module Supermemory
|
|
6
|
+
module Integrations
|
|
7
|
+
# Integration with graph-agent (https://github.com/ai-firstly/graph-agent).
|
|
8
|
+
#
|
|
9
|
+
# Provides reusable node functions and a pre-built memory-augmented graph
|
|
10
|
+
# for adding persistent memory to graph-agent workflows.
|
|
11
|
+
#
|
|
12
|
+
# @example Basic usage with pre-built nodes
|
|
13
|
+
# require "supermemory/integrations/graph_agent"
|
|
14
|
+
#
|
|
15
|
+
# nodes = Supermemory::Integrations::GraphAgent::Nodes.new(
|
|
16
|
+
# api_key: ENV["SUPERMEMORY_API_KEY"]
|
|
17
|
+
# )
|
|
18
|
+
#
|
|
19
|
+
# graph = GraphAgent::Graph::StateGraph.new(
|
|
20
|
+
# Supermemory::Integrations::GraphAgent.memory_schema
|
|
21
|
+
# )
|
|
22
|
+
# graph.add_node("recall", nodes.method(:recall_memories))
|
|
23
|
+
# graph.add_node("store", nodes.method(:store_memory))
|
|
24
|
+
module GraphAgent
|
|
25
|
+
# Build a state schema with memory fields
|
|
26
|
+
# @param extra_fields [Hash] Additional field definitions to merge
|
|
27
|
+
# @return [Hash] Schema hash compatible with GraphAgent::Graph::StateGraph
|
|
28
|
+
def self.memory_schema(extra_fields: {})
|
|
29
|
+
{
|
|
30
|
+
messages: { type: Array, reducer: ->(a, b) { a + Array(b) }, default: [] },
|
|
31
|
+
memories: { type: Array, reducer: ->(a, b) { a + Array(b) }, default: [] },
|
|
32
|
+
memory_context: { type: String, default: "" },
|
|
33
|
+
user_id: { type: String, default: "" }
|
|
34
|
+
}.merge(extra_fields)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Pre-built node functions for memory operations
|
|
38
|
+
class Nodes
|
|
39
|
+
attr_reader :client
|
|
40
|
+
|
|
41
|
+
# @param api_key [String] Supermemory API key
|
|
42
|
+
# @param base_url [String, nil] Custom API endpoint
|
|
43
|
+
def initialize(api_key:, base_url: nil)
|
|
44
|
+
@client = Supermemory::Client.new(api_key: api_key, base_url: base_url)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Node: Recall memories based on the last user message.
|
|
48
|
+
# Fetches user profile and relevant memories, stores them in state.
|
|
49
|
+
#
|
|
50
|
+
# @param state [Hash] Current graph state (must contain :messages and :user_id)
|
|
51
|
+
# @param _config [Hash] Graph config (unused but accepted for arity compatibility)
|
|
52
|
+
# @return [Hash] State update with :memories and :memory_context
|
|
53
|
+
def recall_memories(state, _config = nil)
|
|
54
|
+
user_id = state[:user_id]
|
|
55
|
+
return { memories: [], memory_context: "" } unless user_id && !user_id.empty?
|
|
56
|
+
|
|
57
|
+
query = extract_last_user_message(state[:messages])
|
|
58
|
+
return { memories: [], memory_context: "" } unless query
|
|
59
|
+
|
|
60
|
+
result = @client.profile(container_tag: user_id, q: query)
|
|
61
|
+
|
|
62
|
+
static_facts = result.dig("profile", "static") || []
|
|
63
|
+
dynamic_context = result.dig("profile", "dynamic") || []
|
|
64
|
+
search_results = result.dig("searchResults", "results") || []
|
|
65
|
+
|
|
66
|
+
memories = search_results.map { |r| r["memory"] || r["chunk"] }.compact
|
|
67
|
+
|
|
68
|
+
context = build_context(static_facts, dynamic_context, memories)
|
|
69
|
+
|
|
70
|
+
{ memories: search_results, memory_context: context }
|
|
71
|
+
rescue => e
|
|
72
|
+
warn "[Supermemory::GraphAgent] Failed to recall memories: #{e.message}"
|
|
73
|
+
{ memories: [], memory_context: "" }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Node: Store the latest conversation exchange as a memory.
|
|
77
|
+
#
|
|
78
|
+
# @param state [Hash] Current graph state
|
|
79
|
+
# @param _config [Hash] Graph config
|
|
80
|
+
# @return [Hash] Empty state update
|
|
81
|
+
def store_memory(state, _config = nil)
|
|
82
|
+
user_id = state[:user_id]
|
|
83
|
+
return {} unless user_id && !user_id.empty?
|
|
84
|
+
|
|
85
|
+
messages = state[:messages] || []
|
|
86
|
+
user_msg = extract_last_user_message(messages)
|
|
87
|
+
assistant_msg = extract_last_assistant_message(messages)
|
|
88
|
+
|
|
89
|
+
content = if user_msg && assistant_msg
|
|
90
|
+
"User: #{user_msg}\nAssistant: #{assistant_msg}"
|
|
91
|
+
elsif user_msg
|
|
92
|
+
"User: #{user_msg}"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
if content
|
|
96
|
+
@client.add(content: content, container_tag: user_id)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
{}
|
|
100
|
+
rescue => e
|
|
101
|
+
warn "[Supermemory::GraphAgent] Failed to store memory: #{e.message}"
|
|
102
|
+
{}
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Node: Search memories with a specific query.
|
|
106
|
+
#
|
|
107
|
+
# @param state [Hash] Current graph state (expects :query or last user message)
|
|
108
|
+
# @param _config [Hash] Graph config
|
|
109
|
+
# @return [Hash] State update with :memories
|
|
110
|
+
def search_memories(state, _config = nil)
|
|
111
|
+
user_id = state[:user_id]
|
|
112
|
+
query = state[:query] || extract_last_user_message(state[:messages])
|
|
113
|
+
return { memories: [] } unless user_id && query
|
|
114
|
+
|
|
115
|
+
result = @client.search.memories(
|
|
116
|
+
q: query,
|
|
117
|
+
container_tag: user_id,
|
|
118
|
+
search_mode: "hybrid",
|
|
119
|
+
limit: 5
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
{ memories: result["results"] || [] }
|
|
123
|
+
rescue => e
|
|
124
|
+
warn "[Supermemory::GraphAgent] Failed to search memories: #{e.message}"
|
|
125
|
+
{ memories: [] }
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Node: Add a specific piece of content to memory.
|
|
129
|
+
#
|
|
130
|
+
# @param state [Hash] Current graph state (expects :memory_content)
|
|
131
|
+
# @param _config [Hash] Graph config
|
|
132
|
+
# @return [Hash] Empty state update
|
|
133
|
+
def add_memory(state, _config = nil)
|
|
134
|
+
user_id = state[:user_id]
|
|
135
|
+
content = state[:memory_content]
|
|
136
|
+
return {} unless user_id && content
|
|
137
|
+
|
|
138
|
+
metadata = state[:memory_metadata] || {}
|
|
139
|
+
@client.add(content: content, container_tag: user_id, metadata: metadata)
|
|
140
|
+
|
|
141
|
+
{}
|
|
142
|
+
rescue => e
|
|
143
|
+
warn "[Supermemory::GraphAgent] Failed to add memory: #{e.message}"
|
|
144
|
+
{}
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
private
|
|
148
|
+
|
|
149
|
+
def extract_last_user_message(messages)
|
|
150
|
+
msg = Array(messages).reverse.find { |m| m[:role] == "user" || m["role"] == "user" }
|
|
151
|
+
msg[:content] || msg["content"] if msg
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def extract_last_assistant_message(messages)
|
|
155
|
+
msg = Array(messages).reverse.find { |m| m[:role] == "assistant" || m["role"] == "assistant" }
|
|
156
|
+
msg[:content] || msg["content"] if msg
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def build_context(static_facts, dynamic_context, memories)
|
|
160
|
+
parts = []
|
|
161
|
+
parts << "User Background:\n#{static_facts.map { |f| "- #{f}" }.join("\n")}" if static_facts.any?
|
|
162
|
+
parts << "Recent Context:\n#{dynamic_context.map { |c| "- #{c}" }.join("\n")}" if dynamic_context.any?
|
|
163
|
+
parts << "Relevant Memories:\n#{memories.map { |m| "- #{m}" }.join("\n")}" if memories.any?
|
|
164
|
+
parts.join("\n\n")
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Build a complete memory-augmented graph
|
|
169
|
+
#
|
|
170
|
+
# @param api_key [String] Supermemory API key
|
|
171
|
+
# @param llm_node [Proc] A callable that takes (state, config) and returns
|
|
172
|
+
# a state update with at least a new message in :messages
|
|
173
|
+
# @param should_store [Proc, nil] Optional callable (state) -> Boolean
|
|
174
|
+
# to conditionally skip memory storage
|
|
175
|
+
# @param extra_schema [Hash] Additional schema fields
|
|
176
|
+
# @param base_url [String, nil] Custom API endpoint
|
|
177
|
+
# @return [Object] Compiled graph (call .invoke or .stream)
|
|
178
|
+
#
|
|
179
|
+
# @example
|
|
180
|
+
# llm_node = ->(state, _config) {
|
|
181
|
+
# context = state[:memory_context]
|
|
182
|
+
# # ... call your LLM with context ...
|
|
183
|
+
# { messages: [{ role: "assistant", content: response }] }
|
|
184
|
+
# }
|
|
185
|
+
#
|
|
186
|
+
# app = Supermemory::Integrations::GraphAgent.build_memory_graph(
|
|
187
|
+
# api_key: ENV["SUPERMEMORY_API_KEY"],
|
|
188
|
+
# llm_node: llm_node
|
|
189
|
+
# )
|
|
190
|
+
#
|
|
191
|
+
# result = app.invoke(
|
|
192
|
+
# { messages: [{ role: "user", content: "Hi!" }], user_id: "user-123" }
|
|
193
|
+
# )
|
|
194
|
+
def self.build_memory_graph(api_key:, llm_node:, should_store: nil, extra_schema: {}, base_url: nil)
|
|
195
|
+
require "graph_agent"
|
|
196
|
+
|
|
197
|
+
nodes = Nodes.new(api_key: api_key, base_url: base_url)
|
|
198
|
+
schema = memory_schema(extra_fields: extra_schema)
|
|
199
|
+
graph = ::GraphAgent::Graph::StateGraph.new(schema)
|
|
200
|
+
|
|
201
|
+
graph.add_node("recall", nodes.method(:recall_memories))
|
|
202
|
+
graph.add_node("generate", llm_node)
|
|
203
|
+
graph.add_node("store", nodes.method(:store_memory))
|
|
204
|
+
|
|
205
|
+
graph.add_edge(::GraphAgent::START, "recall")
|
|
206
|
+
graph.add_edge("recall", "generate")
|
|
207
|
+
|
|
208
|
+
if should_store
|
|
209
|
+
graph.add_conditional_edges("generate", lambda { |state|
|
|
210
|
+
should_store.call(state) ? "store" : ::GraphAgent::END_NODE.to_s
|
|
211
|
+
})
|
|
212
|
+
else
|
|
213
|
+
graph.add_edge("generate", "store")
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
graph.add_edge("store", ::GraphAgent::END_NODE)
|
|
217
|
+
|
|
218
|
+
graph.compile
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|