suemo 0.0.1 → 0.0.2

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.
@@ -1,5 +1,6 @@
1
1
  import type { Surreal } from 'surrealdb'
2
- import type { LLMConfig } from '../config.ts'
2
+ import type { LLMConfig, SuemoConfig } from '../config.ts'
3
+ import { getEmbedding } from '../embedding/index.ts'
3
4
  import { getLogger } from '../logger.ts'
4
5
  import type { ConsolidationRun, MemoryNode } from '../types.ts'
5
6
 
@@ -40,6 +41,7 @@ async function runNREM(
40
41
  db: Surreal,
41
42
  llm: LLMConfig,
42
43
  similarityThreshold: number,
44
+ embeddingConfig: SuemoConfig['embedding'],
43
45
  ): Promise<{ nodesIn: number; nodesOut: number }> {
44
46
  log.info('NREM phase starting')
45
47
 
@@ -75,12 +77,21 @@ async function runNREM(
75
77
  // Find similar unassigned nodes using DB-side cosine
76
78
  const similarResult = await db.query<[{ id: string; score: number }[]]>(
77
79
  `
78
- SELECT id, vector::similarity::cosine(embedding, $emb) AS score
79
- FROM memory
80
- WHERE consolidated = false
81
- AND id != $self
82
- AND (valid_until = NONE OR valid_until > time::now())
83
- ORDER BY embedding <|10, 20|> $emb
80
+ LET $cand = (
81
+ SELECT id, embedding
82
+ FROM memory
83
+ WHERE consolidated = false
84
+ AND id != $self
85
+ AND (valid_until = NONE OR valid_until > time::now())
86
+ AND embedding <|10, 20|> $emb
87
+ );
88
+
89
+ SELECT id, score
90
+ FROM (
91
+ SELECT id, vector::similarity::cosine(embedding, $emb) AS score
92
+ FROM $cand
93
+ )
94
+ ORDER BY score DESC
84
95
  LIMIT 10
85
96
  `,
86
97
  { emb: node.embedding, self: node.id },
@@ -103,7 +114,10 @@ async function runNREM(
103
114
  for (const cluster of clusters) {
104
115
  if (cluster.length === 1) {
105
116
  // Singleton — mark consolidated without compression
106
- await db.query('UPDATE $id SET consolidated = true', { id: cluster[0]!.id })
117
+ await db.query(
118
+ 'UPDATE <record<memory>>$id SET consolidated = true',
119
+ { id: cluster[0]!.id },
120
+ )
107
121
  nodesOut++
108
122
  continue
109
123
  }
@@ -122,13 +136,22 @@ async function runNREM(
122
136
  } catch (e) {
123
137
  log.error('LLM compression failed — marking cluster nodes individually', { error: String(e) })
124
138
  for (const n of cluster) {
125
- await db.query('UPDATE $id SET consolidated = true', { id: n.id })
139
+ await db.query(
140
+ 'UPDATE <record<memory>>$id SET consolidated = true',
141
+ { id: n.id },
142
+ )
126
143
  }
127
144
  nodesOut += cluster.length
128
145
  continue
129
146
  }
130
147
 
131
148
  // Create the compressed summary node
149
+ const { clause: embeddingClause, param: embeddingParam } = await getEmbedding(summary, embeddingConfig)
150
+ log.debug('NREM summary embedding resolved', {
151
+ provider: embeddingConfig.provider,
152
+ hasVectorParam: Boolean(embeddingParam),
153
+ dimension: embeddingParam?.length,
154
+ })
132
155
  const created = await db.query<[MemoryNode[]]>(
133
156
  `
134
157
  CREATE memory CONTENT {
@@ -137,7 +160,7 @@ async function runNREM(
137
160
  summary: $summary,
138
161
  tags: $tags,
139
162
  scope: $scope,
140
- embedding: fn::embed($summary),
163
+ embedding: ${embeddingClause},
141
164
  confidence: 1.0,
142
165
  salience: 0.7,
143
166
  source: 'consolidation:nrem',
@@ -151,7 +174,8 @@ async function runNREM(
151
174
  {
152
175
  summary,
153
176
  tags: [...new Set(cluster.flatMap((n) => n.tags))],
154
- scope: cluster[0]!.scope ?? null,
177
+ ...(cluster[0]!.scope !== null && cluster[0]!.scope !== undefined ? { scope: cluster[0]!.scope } : {}),
178
+ ...(embeddingParam ? { embedding: embeddingParam } : {}),
155
179
  },
156
180
  )
157
181
  const summaryNode = created[0]?.[0]
@@ -161,7 +185,7 @@ async function runNREM(
161
185
  for (const n of cluster) {
162
186
  await db.query(
163
187
  `
164
- UPDATE $id SET
188
+ UPDATE <record<memory>>$id SET
165
189
  consolidated = true,
166
190
  consolidated_into = $into,
167
191
  updated_at = time::now()
@@ -214,10 +238,13 @@ async function runREM(
214
238
  const candidates = await db.query<[MemoryNode[]]>(
215
239
  `
216
240
  SELECT * FROM memory
217
- WHERE id != $self
218
- AND consolidated = true
219
- AND (valid_until = NONE OR valid_until > time::now())
220
- ORDER BY embedding <|10, 40|> $emb
241
+ WHERE id IN (
242
+ SELECT VALUE id FROM memory
243
+ WHERE id != $self
244
+ AND consolidated = true
245
+ AND (valid_until = NONE OR valid_until > time::now())
246
+ AND embedding <|10, 40|> $emb
247
+ )
221
248
  LIMIT 10
222
249
  `,
223
250
  { self: node.id, emb: node.embedding },
@@ -274,7 +301,7 @@ async function runREM(
274
301
 
275
302
  // Bump salience on newly connected node
276
303
  await db.query(
277
- 'UPDATE $id SET salience = math::min(salience + 0.1, 1.0)',
304
+ 'UPDATE <record<memory>>$id SET salience = math::min(salience + 0.1, 1.0)',
278
305
  { id: node.id },
279
306
  )
280
307
  }
@@ -288,6 +315,7 @@ export async function consolidate(
288
315
  nremSimilarityThreshold?: number
289
316
  remRelationThreshold?: number
290
317
  llm: LLMConfig
318
+ embedding: SuemoConfig['embedding']
291
319
  },
292
320
  ): Promise<ConsolidationRun> {
293
321
  const phase = opts.nremOnly ? 'nrem' : 'full'
@@ -313,6 +341,7 @@ export async function consolidate(
313
341
  db,
314
342
  opts.llm,
315
343
  opts.nremSimilarityThreshold ?? 0.85,
344
+ opts.embedding,
316
345
  )
317
346
 
318
347
  if (!opts.nremOnly) {
@@ -321,7 +350,7 @@ export async function consolidate(
321
350
 
322
351
  await db.query(
323
352
  `
324
- UPDATE $id SET
353
+ UPDATE <record<consolidation_run>>$id SET
325
354
  completed_at = time::now(),
326
355
  status = 'done',
327
356
  nodes_in = $nodesIn,
@@ -337,7 +366,7 @@ export async function consolidate(
337
366
  log.error('consolidate() failed', { error: errMsg })
338
367
  await db.query(
339
368
  `
340
- UPDATE $id SET
369
+ UPDATE <record<consolidation_run>>$id SET
341
370
  completed_at = time::now(),
342
371
  status = 'failed',
343
372
  error = $err
@@ -23,12 +23,21 @@ export async function detectContradiction(
23
23
 
24
24
  const candidates = await db.query<[{ id: string; score: number }[]]>(
25
25
  `
26
- SELECT id, vector::similarity::cosine(embedding, $emb) AS score
27
- FROM memory
28
- WHERE kind = 'belief'
29
- AND id != $self
30
- AND (valid_until = NONE OR valid_until > time::now())
31
- ORDER BY embedding <|3, 20|> $emb
26
+ LET $cand = (
27
+ SELECT id, embedding
28
+ FROM memory
29
+ WHERE kind = 'belief'
30
+ AND id != $self
31
+ AND (valid_until = NONE OR valid_until > time::now())
32
+ AND embedding <|3, 20|> $emb
33
+ );
34
+
35
+ SELECT id, score
36
+ FROM (
37
+ SELECT id, vector::similarity::cosine(embedding, $emb) AS score
38
+ FROM $cand
39
+ )
40
+ ORDER BY score DESC
32
41
  LIMIT 3
33
42
  `,
34
43
  { emb: newNode.embedding, self: newNode.id },
@@ -41,7 +50,10 @@ export async function detectContradiction(
41
50
  if (best.score < similarityThreshold) return null
42
51
 
43
52
  // Fetch full node
44
- const result = await db.query<[MemoryNode[]]>('SELECT * FROM $id', { id: best.id })
53
+ const result = await db.query<[MemoryNode[]]>(
54
+ 'SELECT * FROM <record<memory>>$id',
55
+ { id: best.id },
56
+ )
45
57
  const existing = result[0]?.[0]
46
58
  if (!existing) return null
47
59
 
@@ -0,0 +1,36 @@
1
+ import { defineConfig } from 'suemo'
2
+
3
+ export default defineConfig({
4
+ surreal: {
5
+ url: process.env.SUEMO_URL ?? 'ws://localhost:8000',
6
+ namespace: process.env.SUEMO_NS ?? 'suemo',
7
+ database: process.env.SUEMO_DB ?? 'suemo',
8
+ auth: {
9
+ user: process.env.SUEMO_USER ?? 'root',
10
+ pass: process.env.SUEMO_PASS ?? 'pass',
11
+ },
12
+ },
13
+ embedding: {
14
+ provider: 'stub',
15
+ dimension: 384,
16
+ },
17
+ consolidation: {
18
+ trigger: 'timer',
19
+ intervalMinutes: 30,
20
+ reactiveThreshold: 50,
21
+ nremSimilarityThreshold: 0.85,
22
+ remRelationThreshold: 0.4,
23
+ llm: {
24
+ url: process.env.SUEMO_LLM_URL ?? 'https://api.z.ai/api/coding/paas/v4',
25
+ model: process.env.SUEMO_LLM_MODEL ?? 'glm-4.7-flash',
26
+ apiKey: process.env.SUEMO_LLM_API_KEY!,
27
+ },
28
+ },
29
+ retrieval: {
30
+ weights: { vector: 0.5, bm25: 0.25, graph: 0.15, temporal: 0.1 },
31
+ },
32
+ mcp: {
33
+ port: Number(process.env.SUEMO_PORT) || 4242,
34
+ host: '127.0.0.1',
35
+ },
36
+ })
package/src/config.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { existsSync } from 'node:fs'
2
2
  import { join, resolve } from 'node:path'
3
3
  import { pathToFileURL } from 'node:url'
4
+ import { getLogger } from './logger.ts'
4
5
 
5
6
  // ── Config shape ──────────────────────────────────────────────────────────────
6
7
 
@@ -17,8 +18,9 @@ export interface SurrealTarget {
17
18
  }
18
19
 
19
20
  export type EmbeddingProvider =
21
+ | { provider: 'openai-compatible'; url: string; model: string; dimension: number; apiKey?: string }
20
22
  | { provider: 'surreal'; dimension: number }
21
- | { provider: 'stub'; dimension: number } // test-only
23
+ | { provider: 'stub'; dimension: number }
22
24
 
23
25
  export interface LLMConfig {
24
26
  url: string // OpenAI-compatible endpoint
@@ -76,11 +78,21 @@ const CONFIG_CANDIDATES = [
76
78
  'suemo.config.js',
77
79
  ]
78
80
 
79
- const HOME_CONFIG = join(
80
- process.env.HOME ?? process.env.USERPROFILE ?? '~',
81
- '.suemo',
82
- 'suemo.ts',
83
- )
81
+ const log = getLogger(['suemo', 'config'])
82
+
83
+ function getHomeConfigPath(): string | null {
84
+ const home = process.env.HOME ?? process.env.USERPROFILE
85
+ if (!home) return null
86
+ return join(home, '.suemo', 'suemo.ts')
87
+ }
88
+
89
+ function resolveConfigPath(p: string): string {
90
+ if (p.startsWith('~/')) {
91
+ const home = process.env.HOME ?? process.env.USERPROFILE
92
+ if (home) return resolve(home, p.slice(2))
93
+ }
94
+ return resolve(p)
95
+ }
84
96
 
85
97
  export async function loadConfig(
86
98
  cwd = process.cwd(),
@@ -88,23 +100,40 @@ export async function loadConfig(
88
100
  ): Promise<SuemoConfig> {
89
101
  // 1. Explicit --config flag
90
102
  if (overridePath) {
91
- return importConfig(resolve(overridePath))
103
+ const resolved = resolveConfigPath(overridePath)
104
+ log.debug('Loading config from --config', { path: resolved })
105
+ return importConfig(resolved)
106
+ }
107
+ // 2. Environment override
108
+ const envConfigPath = process.env.SUEMO_CONFIG_PATH?.trim()
109
+ if (envConfigPath) {
110
+ const resolved = resolveConfigPath(envConfigPath)
111
+ log.debug('Loading config from SUEMO_CONFIG_PATH', { path: resolved })
112
+ return importConfig(resolved)
92
113
  }
93
- // 2. Project-local
114
+ // 3. Project-local
94
115
  for (const name of CONFIG_CANDIDATES) {
95
116
  const p = resolve(cwd, name)
96
- if (existsSync(p)) return importConfig(p)
117
+ if (existsSync(p)) {
118
+ log.debug('Loading project-local config', { path: p })
119
+ return importConfig(p)
120
+ }
121
+ }
122
+ // 4. User-level
123
+ const homeConfig = getHomeConfigPath()
124
+ if (homeConfig && existsSync(homeConfig)) {
125
+ log.debug('Loading user-level config', { path: homeConfig })
126
+ return importConfig(homeConfig)
97
127
  }
98
- // 3. User-level
99
- if (existsSync(HOME_CONFIG)) return importConfig(HOME_CONFIG)
100
128
 
101
129
  throw new Error(
102
130
  'No suemo config found.\n'
103
- + 'Run `suemo init` to create ~/.suemo/suemo.ts, or create suemo.config.ts in the project root.',
131
+ + 'Run `suemo init config` to create ~/.suemo/suemo.ts, set SUEMO_CONFIG_PATH, or create suemo.config.ts in the project root.',
104
132
  )
105
133
  }
106
134
 
107
135
  async function importConfig(path: string): Promise<SuemoConfig> {
136
+ log.debug('Importing config module', { path })
108
137
  const mod = await import(pathToFileURL(path).href)
109
138
  const cfg: unknown = mod.default ?? mod
110
139
  if (!cfg || typeof cfg !== 'object') {
@@ -12,18 +12,33 @@ export interface CompatibilityResult {
12
12
  errors: string[]
13
13
  }
14
14
 
15
+ export interface CompatibilityOptions {
16
+ /**
17
+ * Whether missing fn::embed() should fail compatibility.
18
+ * Keep true for runtime paths that execute vector/embed queries.
19
+ */
20
+ requireEmbedding?: boolean
21
+ /**
22
+ * Human-readable label for logging this preflight execution.
23
+ */
24
+ context?: string
25
+ }
26
+
15
27
  const MIN_MAJOR = 3
16
28
 
17
29
  export async function checkCompatibility(
18
30
  db: Surreal,
31
+ options: CompatibilityOptions = {},
19
32
  ): Promise<CompatibilityResult> {
20
33
  const errors: string[] = []
21
34
  let surrealVersion = 'unknown'
22
35
  let surrealkv = false
23
36
  let retention_ok = false
24
37
  let embedding = false
38
+ const requireEmbedding = options.requireEmbedding ?? true
39
+ const context = options.context ?? 'default'
25
40
 
26
- log.info('Running preflight compatibility checks')
41
+ log.info('Running preflight compatibility checks', { requireEmbedding, context })
27
42
 
28
43
  // ── Check 1: version string ───────────────────────────────────────────────
29
44
  try {
@@ -41,6 +56,7 @@ export async function checkCompatibility(
41
56
  )
42
57
  }
43
58
  }
59
+ log.debug('SurrealDB version check complete', { surrealVersion })
44
60
  } catch (e) {
45
61
  errors.push(`Cannot retrieve SurrealDB version: ${String(e)}`)
46
62
  }
@@ -54,6 +70,7 @@ export async function checkCompatibility(
54
70
  // if it was around then), not throw. An error means VERSION is unsupported.
55
71
  await db.query("SELECT * FROM suemo_preflight:probe VERSION d'2020-01-01T00:00:00Z'")
56
72
  surrealkv = true
73
+ log.debug('SurrealKV VERSION probe passed')
57
74
  } catch (e: unknown) {
58
75
  const msg = String(e).toLowerCase()
59
76
  if (msg.includes('version') || msg.includes('surrealkv') || msg.includes('not supported')) {
@@ -83,6 +100,7 @@ export async function checkCompatibility(
83
100
  await db.query('CREATE suemo_retention_probe:v SET t = time::now()')
84
101
  await db.query(`SELECT * FROM suemo_retention_probe:v VERSION d'${ninetyDaysAgo}'`)
85
102
  retention_ok = true
103
+ log.debug('Retention probe passed', { ninetyDaysAgo })
86
104
  } catch (e: unknown) {
87
105
  const msg = String(e).toLowerCase()
88
106
  if (msg.includes('retention') || msg.includes('version')) {
@@ -104,20 +122,28 @@ export async function checkCompatibility(
104
122
 
105
123
  // ── Check 4: fn::embed() resolves ────────────────────────────────────────
106
124
  // We don't actually embed anything — we just check that the function exists.
107
- // An "Unknown function" error means the embedding plugin is not configured.
125
+ // An "Unknown function" error means embedding runtime is unavailable.
108
126
  try {
109
127
  // fn::embed requires the ML module and a configured model.
110
128
  // If it throws "No embedding model configured" that's acceptable — the
111
129
  // function exists. If it throws "Unknown function 'fn::embed'" → not available.
112
130
  await db.query(`RETURN fn::embed("suemo preflight test")`)
113
131
  embedding = true
132
+ log.debug('Embedding function probe passed')
114
133
  } catch (e: unknown) {
115
134
  const msg = String(e).toLowerCase()
116
135
  if (msg.includes('unknown function') || msg.includes('fn::embed')) {
117
- errors.push(
118
- 'fn::embed() is not available. Configure an embedding model in SurrealDB, or '
119
- + "use embedding.provider = 'stub' in suemo config (test-only).",
120
- )
136
+ embedding = false
137
+ if (requireEmbedding) {
138
+ errors.push(
139
+ 'fn::embed() is not available in this SurrealDB database. Import/configure a SurrealML embedding model for this namespace/database, then retry.',
140
+ )
141
+ } else {
142
+ log.warn('Embedding function unavailable; continuing due to non-strict preflight mode', {
143
+ context,
144
+ error: String(e),
145
+ })
146
+ }
121
147
  } else {
122
148
  // Other error (e.g. no model configured but function exists) — treat as available
123
149
  embedding = true
@@ -30,27 +30,30 @@ DEFINE FIELD OVERWRITE consolidated ON memory TYPE bool DEFAULT false;
30
30
 
31
31
  -- REFERENCES: bidirectional record link (SurrealDB v3.0)
32
32
  DEFINE FIELD OVERWRITE consolidated_into ON memory
33
- TYPE option<record<memory>> REFERENCES;
33
+ TYPE option<record<memory>> REFERENCE;
34
34
 
35
35
  -- FSRS spaced-repetition fields (all optional; populated on first recall())
36
36
  DEFINE FIELD OVERWRITE fsrs_stability ON memory TYPE option<float>;
37
37
  DEFINE FIELD OVERWRITE fsrs_difficulty ON memory TYPE option<float>;
38
38
  DEFINE FIELD OVERWRITE fsrs_next_review ON memory TYPE option<datetime>;
39
39
 
40
- -- Vector index: HNSW, DEFER so init does not block on large existing datasets
40
+ -- Vector index: HNSW (keep syntax compatible with current SurrealDB target)
41
41
  DEFINE INDEX OVERWRITE idx_memory_embedding
42
42
  ON memory FIELDS embedding
43
- HNSW DIMENSION 1536 DIST COSINE
44
- DEFER;
43
+ HNSW DIMENSION 384 DIST COSINE;
45
44
 
46
45
  -- Full-text index: BM25 over content + summary
47
46
  DEFINE ANALYZER OVERWRITE suemo_analyzer
48
47
  TOKENIZERS blank,class
49
48
  FILTERS lowercase,snowball(english);
50
49
 
51
- DEFINE INDEX OVERWRITE idx_memory_fts
52
- ON memory FIELDS content, summary
53
- SEARCH ANALYZER suemo_analyzer BM25;
50
+ DEFINE INDEX OVERWRITE idx_memory_content_fts
51
+ ON memory FIELDS content
52
+ FULLTEXT ANALYZER suemo_analyzer BM25;
53
+
54
+ DEFINE INDEX OVERWRITE idx_memory_summary_fts
55
+ ON memory FIELDS summary
56
+ FULLTEXT ANALYZER suemo_analyzer BM25;
54
57
 
55
58
  -- Compound indexes for timeline and filter queries
56
59
  DEFINE INDEX OVERWRITE idx_memory_scope_time
@@ -84,7 +87,7 @@ DEFINE FIELD OVERWRITE summary ON episode TYPE option<string>;
84
87
 
85
88
  -- REFERENCES: bidirectional array of memory links
86
89
  DEFINE FIELD OVERWRITE memory_ids ON episode
87
- TYPE array<record<memory>> REFERENCES
90
+ TYPE array<record<memory>> REFERENCE
88
91
  DEFAULT [];
89
92
 
90
93
  -- ── consolidation_run ────────────────────────────────────────────────────────
package/src/db/schema.ts CHANGED
@@ -11,12 +11,15 @@ const log = getLogger(['suemo', 'db', 'schema'])
11
11
  export async function runSchema(db: Surreal): Promise<void> {
12
12
  log.info('Running schema migrations')
13
13
  const statements = SCHEMA.split(/;\s*\n/).map((s) => s.trim()).filter(Boolean)
14
- for (const stmt of statements) {
14
+ log.debug('Prepared schema statements', { count: statements.length, schemaBytes: SCHEMA.length })
15
+ for (const [index, stmt] of statements.entries()) {
15
16
  try {
17
+ const snippet = stmt.length > 160 ? `${stmt.slice(0, 160)}…` : stmt
18
+ log.debug('Executing schema statement', { index, snippet })
16
19
  await db.query(stmt)
17
- log.debug('Schema statement OK', { stmt: stmt.slice(0, 60) })
20
+ log.debug('Schema statement OK', { index, stmt: stmt.slice(0, 60) })
18
21
  } catch (e) {
19
- log.error('Schema statement failed', { stmt, error: String(e) })
22
+ log.error('Schema statement failed', { index, stmt, error: String(e) })
20
23
  throw e
21
24
  }
22
25
  }
@@ -0,0 +1,52 @@
1
+ import type { EmbeddingProvider } from '../config.ts'
2
+ import { getLogger } from '../logger.ts'
3
+ import { embedText } from './openai-compatible.ts'
4
+
5
+ const log = getLogger(['suemo', 'embedding'])
6
+
7
+ export interface EmbeddingResult {
8
+ clause: string
9
+ param?: number[]
10
+ }
11
+
12
+ export async function getEmbedding(
13
+ text: string,
14
+ config: EmbeddingProvider,
15
+ ): Promise<EmbeddingResult> {
16
+ log.debug('getEmbedding()', { provider: config.provider, dimension: config.dimension })
17
+
18
+ switch (config.provider) {
19
+ case 'surreal':
20
+ return { clause: 'fn::embed($content)' }
21
+
22
+ case 'openai-compatible': {
23
+ const vec = await embedText(text, config)
24
+ if (vec.length !== config.dimension) {
25
+ throw new Error(
26
+ `Embedding dimension mismatch: expected ${config.dimension}, got ${vec.length}`,
27
+ )
28
+ }
29
+ return { clause: '$embedding', param: vec }
30
+ }
31
+
32
+ case 'stub': {
33
+ const vec = new Array(config.dimension).fill(0)
34
+ log.debug('stub embedding: returning zero vector', { dimension: config.dimension })
35
+ return { clause: '$embedding', param: vec }
36
+ }
37
+ }
38
+ }
39
+
40
+ export function buildEmbeddingClause(config: EmbeddingProvider): string {
41
+ switch (config.provider) {
42
+ case 'surreal':
43
+ return 'fn::embed($content)'
44
+ case 'openai-compatible':
45
+ case 'stub':
46
+ return '$embedding'
47
+ }
48
+ }
49
+
50
+ export function getZeroVector(dimension: number): number[] {
51
+ return new Array(dimension).fill(0)
52
+ }
@@ -0,0 +1,43 @@
1
+ // src/embedding/openai_compatible.ts
2
+ import { getLogger } from "../logger.ts";
3
+
4
+ const log = getLogger(["suemo", "embedding", "openai-compatible"]);
5
+
6
+ export interface OpenAICompatibleEmbeddingConfig {
7
+ url: string; // e.g. http://127.0.0.1:8080/v1/embeddings
8
+ model: string;
9
+ dimension: number;
10
+ apiKey?: string;
11
+ }
12
+
13
+ export async function embedText(
14
+ text: string,
15
+ config: OpenAICompatibleEmbeddingConfig,
16
+ ): Promise<number[]> {
17
+ log.debug("embedText()", { textPreview: text.slice(0, 60) });
18
+
19
+ const res = await fetch(config.url, {
20
+ method: "POST",
21
+ headers: {
22
+ "Content-Type": "application/json",
23
+ "Authorization": `Bearer ${config.apiKey ?? "local"}`,
24
+ },
25
+ body: JSON.stringify({ input: text, model: config.model }),
26
+ });
27
+
28
+ if (!res.ok) {
29
+ throw new Error(`Embedding server error ${res.status}: ${await res.text()}`);
30
+ }
31
+
32
+ const json = await res.json() as {
33
+ data: { embedding: number[]; index: number }[];
34
+ };
35
+
36
+ const vec = json.data[0]?.embedding;
37
+ if (!vec) throw new Error("Embedding server returned empty data");
38
+ if (vec.length !== config.dimension) {
39
+ throw new Error(`Dimension mismatch: expected ${config.dimension}, got ${vec.length}`);
40
+ }
41
+
42
+ return vec;
43
+ }
package/src/goal.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { Surreal } from 'surrealdb'
2
+ import type { SuemoConfig } from './config.ts'
2
3
  import { getLogger } from './logger.ts'
3
4
  import { invalidate, observe } from './memory/write.ts'
4
5
  import type { MemoryNode } from './types.ts'
@@ -8,10 +9,11 @@ const log = getLogger(['suemo', 'goal'])
8
9
  export async function goalSet(
9
10
  db: Surreal,
10
11
  content: string,
12
+ config: SuemoConfig,
11
13
  opts: { scope?: string; tags?: string[] } = {},
12
14
  ): Promise<MemoryNode> {
13
15
  log.info('goalSet()', { content: content.slice(0, 60) })
14
- return observe(db, { content, kind: 'goal', ...opts })
16
+ return observe(db, { content, kind: 'goal', ...opts }, config)
15
17
  }
16
18
 
17
19
  export async function goalResolve(db: Surreal, goalId: string): Promise<void> {