@kaleidorg/mind 0.1.0 → 0.3.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/dist/capabilities.d.ts +38 -0
- package/dist/capabilities.d.ts.map +1 -0
- package/dist/capabilities.js +41 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/context/budget.d.ts +29 -0
- package/dist/context/budget.d.ts.map +1 -0
- package/dist/context/budget.js +36 -0
- package/dist/context/budget.js.map +1 -0
- package/dist/context/builder.d.ts +39 -0
- package/dist/context/builder.d.ts.map +1 -0
- package/dist/context/builder.js +77 -0
- package/dist/context/builder.js.map +1 -0
- package/dist/engine.d.ts +9 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +1 -0
- package/dist/engine.js.map +1 -1
- package/dist/fastpath/fastpath.d.ts +38 -0
- package/dist/fastpath/fastpath.d.ts.map +1 -0
- package/dist/fastpath/fastpath.js +52 -0
- package/dist/fastpath/fastpath.js.map +1 -0
- package/dist/funnel.d.ts +117 -0
- package/dist/funnel.d.ts.map +1 -0
- package/dist/funnel.js +195 -0
- package/dist/funnel.js.map +1 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -1
- package/dist/kaleidoswap/contract.d.ts +72 -0
- package/dist/kaleidoswap/contract.d.ts.map +1 -0
- package/dist/kaleidoswap/contract.js +125 -0
- package/dist/kaleidoswap/contract.js.map +1 -0
- package/dist/knowledge/bitcoin-copilot.d.ts +11 -0
- package/dist/knowledge/bitcoin-copilot.d.ts.map +1 -0
- package/dist/knowledge/bitcoin-copilot.js +155 -0
- package/dist/knowledge/bitcoin-copilot.js.map +1 -0
- package/dist/knowledge/btc-map.d.ts +87 -0
- package/dist/knowledge/btc-map.d.ts.map +1 -0
- package/dist/knowledge/btc-map.js +365 -0
- package/dist/knowledge/btc-map.js.map +1 -0
- package/dist/knowledge/merchants.d.ts +24 -0
- package/dist/knowledge/merchants.d.ts.map +1 -0
- package/dist/knowledge/merchants.js +34 -0
- package/dist/knowledge/merchants.js.map +1 -0
- package/dist/knowledge/wallet.d.ts +34 -0
- package/dist/knowledge/wallet.d.ts.map +1 -0
- package/dist/knowledge/wallet.js +63 -0
- package/dist/knowledge/wallet.js.map +1 -0
- package/dist/lsps1/contract.d.ts +55 -0
- package/dist/lsps1/contract.d.ts.map +1 -0
- package/dist/lsps1/contract.js +91 -0
- package/dist/lsps1/contract.js.map +1 -0
- package/dist/memory/store.d.ts +40 -0
- package/dist/memory/store.d.ts.map +1 -0
- package/dist/memory/store.js +143 -0
- package/dist/memory/store.js.map +1 -0
- package/dist/memory/tool.d.ts +9 -0
- package/dist/memory/tool.d.ts.map +1 -0
- package/dist/memory/tool.js +70 -0
- package/dist/memory/tool.js.map +1 -0
- package/dist/memory/types.d.ts +68 -0
- package/dist/memory/types.d.ts.map +1 -0
- package/dist/memory/types.js +14 -0
- package/dist/memory/types.js.map +1 -0
- package/dist/rag/retriever.d.ts +30 -0
- package/dist/rag/retriever.d.ts.map +1 -0
- package/dist/rag/retriever.js +72 -0
- package/dist/rag/retriever.js.map +1 -0
- package/dist/rag/tool.d.ts +15 -0
- package/dist/rag/tool.d.ts.map +1 -0
- package/dist/rag/tool.js +42 -0
- package/dist/rag/tool.js.map +1 -0
- package/dist/rag/types.d.ts +44 -0
- package/dist/rag/types.d.ts.map +1 -0
- package/dist/rag/types.js +11 -0
- package/dist/rag/types.js.map +1 -0
- package/dist/rag/vector-store.d.ts +23 -0
- package/dist/rag/vector-store.d.ts.map +1 -0
- package/dist/rag/vector-store.js +72 -0
- package/dist/rag/vector-store.js.map +1 -0
- package/dist/recipe/asset-send.d.ts +15 -0
- package/dist/recipe/asset-send.d.ts.map +1 -0
- package/dist/recipe/asset-send.js +83 -0
- package/dist/recipe/asset-send.js.map +1 -0
- package/dist/recipe/kaleidoswap-atomic.d.ts +27 -0
- package/dist/recipe/kaleidoswap-atomic.d.ts.map +1 -0
- package/dist/recipe/kaleidoswap-atomic.js +111 -0
- package/dist/recipe/kaleidoswap-atomic.js.map +1 -0
- package/dist/recipe/payments.d.ts +15 -0
- package/dist/recipe/payments.d.ts.map +1 -0
- package/dist/recipe/payments.js +119 -0
- package/dist/recipe/payments.js.map +1 -0
- package/dist/recipe/receive.d.ts +14 -0
- package/dist/recipe/receive.d.ts.map +1 -0
- package/dist/recipe/receive.js +109 -0
- package/dist/recipe/receive.js.map +1 -0
- package/dist/recipe/runner.d.ts +42 -0
- package/dist/recipe/runner.d.ts.map +1 -0
- package/dist/recipe/runner.js +106 -0
- package/dist/recipe/runner.js.map +1 -0
- package/dist/recipe/swap.d.ts +16 -0
- package/dist/recipe/swap.d.ts.map +1 -0
- package/dist/recipe/swap.js +73 -0
- package/dist/recipe/swap.js.map +1 -0
- package/dist/recipe/types.d.ts +71 -0
- package/dist/recipe/types.d.ts.map +1 -0
- package/dist/recipe/types.js +13 -0
- package/dist/recipe/types.js.map +1 -0
- package/dist/skills/registry.d.ts.map +1 -1
- package/dist/skills/registry.js +20 -2
- package/dist/skills/registry.js.map +1 -1
- package/dist/tools/cli.d.ts +43 -0
- package/dist/tools/cli.d.ts.map +1 -0
- package/dist/tools/cli.js +61 -0
- package/dist/tools/cli.js.map +1 -0
- package/dist/tools/mcp.d.ts +3 -2
- package/dist/tools/mcp.d.ts.map +1 -1
- package/dist/tools/mcp.js +3 -2
- package/dist/tools/mcp.js.map +1 -1
- package/dist/wallet/confirm.d.ts +12 -0
- package/dist/wallet/confirm.d.ts.map +1 -0
- package/dist/wallet/confirm.js +67 -0
- package/dist/wallet/confirm.js.map +1 -0
- package/dist/wallet/contract.d.ts +57 -0
- package/dist/wallet/contract.d.ts.map +1 -0
- package/dist/wallet/contract.js +113 -0
- package/dist/wallet/contract.js.map +1 -0
- package/package.json +10 -5
- package/skills/README.md +6 -1
- package/skills/kaleido-lsps/SKILL.md +56 -0
- package/skills/kaleido-trading/SKILL.md +85 -18
- package/skills/merchant-finder/SKILL.md +87 -0
- package/skills/paid-data/SKILL.md +12 -0
- package/skills/wallet-assistant/SKILL.md +38 -0
- package/src/capabilities.ts +79 -0
- package/src/context/budget.ts +46 -0
- package/src/context/builder.ts +100 -0
- package/src/context/context.test.ts +87 -0
- package/src/engine.ts +6 -0
- package/src/fastpath/fastpath.test.ts +34 -0
- package/src/fastpath/fastpath.ts +70 -0
- package/src/funnel.test.ts +207 -0
- package/src/funnel.ts +285 -0
- package/src/index.ts +128 -0
- package/src/kaleidoswap/contract.test.ts +147 -0
- package/src/kaleidoswap/contract.ts +212 -0
- package/src/knowledge/bitcoin-copilot.ts +177 -0
- package/src/knowledge/btc-map.test.ts +188 -0
- package/src/knowledge/btc-map.ts +446 -0
- package/src/knowledge/knowledge.test.ts +63 -0
- package/src/knowledge/merchants.ts +49 -0
- package/src/knowledge/wallet.ts +84 -0
- package/src/lsps1/contract.test.ts +81 -0
- package/src/lsps1/contract.ts +132 -0
- package/src/memory/memory.test.ts +140 -0
- package/src/memory/store.ts +174 -0
- package/src/memory/tool.ts +76 -0
- package/src/memory/types.ts +76 -0
- package/src/rag/rag.test.ts +85 -0
- package/src/rag/retriever.ts +94 -0
- package/src/rag/tool.ts +55 -0
- package/src/rag/types.ts +49 -0
- package/src/rag/vector-store.ts +78 -0
- package/src/recipe/asset-send.ts +79 -0
- package/src/recipe/kaleidoswap-atomic.test.ts +138 -0
- package/src/recipe/kaleidoswap-atomic.ts +117 -0
- package/src/recipe/payments.ts +116 -0
- package/src/recipe/receive.ts +98 -0
- package/src/recipe/recipe.test.ts +193 -0
- package/src/recipe/runner.ts +134 -0
- package/src/recipe/swap.ts +74 -0
- package/src/recipe/types.ts +76 -0
- package/src/skills/registry.ts +21 -2
- package/src/skills/skills.test.ts +42 -0
- package/src/tools/cli.test.ts +53 -0
- package/src/tools/cli.ts +98 -0
- package/src/tools/mcp.ts +3 -2
- package/src/wallet/confirm.test.ts +57 -0
- package/src/wallet/confirm.ts +74 -0
- package/src/wallet/contract.test.ts +89 -0
- package/src/wallet/contract.ts +157 -0
- package/skills/kaleido-wallet/SKILL.md +0 -28
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/** RAG tests — vector store, chunking, retriever + tool with a fake embedder. */
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect } from 'vitest';
|
|
4
|
+
import { cosineSimilarity, InMemoryVectorStore } from './vector-store.js';
|
|
5
|
+
import { Retriever, chunkText } from './retriever.js';
|
|
6
|
+
import { createRagToolSource } from './tool.js';
|
|
7
|
+
import type { EmbeddingProvider } from './types.js';
|
|
8
|
+
|
|
9
|
+
// Deterministic bag-of-words embedder over a tiny vocab.
|
|
10
|
+
const VOCAB = ['btc', 'lightning', 'channel', 'swap', 'balance', 'rgb', 'price', 'esim'];
|
|
11
|
+
const fakeEmbeddings: EmbeddingProvider = {
|
|
12
|
+
dimension: VOCAB.length,
|
|
13
|
+
async embed(texts) {
|
|
14
|
+
return texts.map((t) => {
|
|
15
|
+
const lower = t.toLowerCase();
|
|
16
|
+
return VOCAB.map((w) => (lower.match(new RegExp(w, 'g'))?.length ?? 0));
|
|
17
|
+
});
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
describe('cosineSimilarity', () => {
|
|
22
|
+
it('is 1 for identical, 0 for orthogonal / degenerate', () => {
|
|
23
|
+
expect(cosineSimilarity([1, 2, 3], [1, 2, 3])).toBeCloseTo(1);
|
|
24
|
+
expect(cosineSimilarity([1, 0], [0, 1])).toBe(0);
|
|
25
|
+
expect(cosineSimilarity([0, 0], [1, 1])).toBe(0);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('chunkText', () => {
|
|
30
|
+
it('returns one chunk when short', () => {
|
|
31
|
+
expect(chunkText('hello world', 800)).toEqual(['hello world']);
|
|
32
|
+
});
|
|
33
|
+
it('splits long text with overlap on boundaries', () => {
|
|
34
|
+
const text = Array.from({ length: 50 }, (_, i) => `Sentence number ${i}.`).join(' ');
|
|
35
|
+
const chunks = chunkText(text, 120, 20);
|
|
36
|
+
expect(chunks.length).toBeGreaterThan(1);
|
|
37
|
+
expect(chunks.every((c) => c.length <= 140)).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('InMemoryVectorStore', () => {
|
|
42
|
+
it('upserts (dedup by id) and queries by similarity', async () => {
|
|
43
|
+
const store = new InMemoryVectorStore();
|
|
44
|
+
await store.upsert([
|
|
45
|
+
{ id: 'a', text: 'about balance', embedding: [0, 0, 0, 0, 1, 0, 0, 0] },
|
|
46
|
+
{ id: 'b', text: 'about price', embedding: [0, 0, 0, 0, 0, 0, 1, 0] },
|
|
47
|
+
]);
|
|
48
|
+
await store.upsert([{ id: 'a', text: 'about balance v2', embedding: [0, 0, 0, 0, 1, 0, 0, 0] }]);
|
|
49
|
+
expect(await store.size()).toBe(2); // 'a' replaced, not duplicated
|
|
50
|
+
|
|
51
|
+
const hits = await store.query([0, 0, 0, 0, 1, 0, 0, 0], 1);
|
|
52
|
+
expect(hits[0].text).toBe('about balance v2');
|
|
53
|
+
expect(hits[0].score).toBeCloseTo(1);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('Retriever + RAG tool', () => {
|
|
58
|
+
it('ingests, then retrieves the most relevant chunk', async () => {
|
|
59
|
+
const retriever = new Retriever({ embeddings: fakeEmbeddings });
|
|
60
|
+
const n = await retriever.ingest([
|
|
61
|
+
{ id: 'd1', text: 'How to open a lightning channel and manage balance.' },
|
|
62
|
+
{ id: 'd2', text: 'eSIM data plans and price comparison.' },
|
|
63
|
+
]);
|
|
64
|
+
expect(n).toBeGreaterThanOrEqual(2);
|
|
65
|
+
|
|
66
|
+
const hits = await retriever.search('what is my balance on lightning', 1);
|
|
67
|
+
expect(hits[0].text).toMatch(/lightning channel/i);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('search_knowledge tool returns formatted snippets', async () => {
|
|
71
|
+
const retriever = new Retriever({ embeddings: fakeEmbeddings });
|
|
72
|
+
await retriever.ingest([{ id: 'd', text: 'BTC swap and channel fees explained.' }]);
|
|
73
|
+
const src = createRagToolSource(retriever, { k: 2 });
|
|
74
|
+
expect(src.has('search_knowledge')).toBe(true);
|
|
75
|
+
const out = await src.execute('search_knowledge', { query: 'swap channel' });
|
|
76
|
+
expect(String(out)).toMatch(/swap and channel/i);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('returns a friendly message when nothing matches', async () => {
|
|
80
|
+
const retriever = new Retriever({ embeddings: fakeEmbeddings });
|
|
81
|
+
const src = createRagToolSource(retriever);
|
|
82
|
+
const out = await src.execute('search_knowledge', { query: 'anything' });
|
|
83
|
+
expect(String(out)).toMatch(/No relevant passages/);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retriever — ties an injected EmbeddingProvider to a VectorStore: ingest
|
|
3
|
+
* documents (chunk → embed → upsert) and search (embed query → top-k). Pure
|
|
4
|
+
* TS; the embedding model is the host's (QVAC `embed()` on device).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { InMemoryVectorStore } from './vector-store.js';
|
|
8
|
+
import type {
|
|
9
|
+
EmbeddingProvider,
|
|
10
|
+
RagDocument,
|
|
11
|
+
RetrievedChunk,
|
|
12
|
+
VectorStore,
|
|
13
|
+
} from './types.js';
|
|
14
|
+
|
|
15
|
+
export interface RetrieverOptions {
|
|
16
|
+
embeddings: EmbeddingProvider;
|
|
17
|
+
/** Vector index (defaults to a fresh in-memory cosine store). */
|
|
18
|
+
store?: VectorStore;
|
|
19
|
+
/** Approx chars per chunk (default 800 ≈ 200 tokens). */
|
|
20
|
+
chunkSize?: number;
|
|
21
|
+
/** Chars of overlap between chunks (default 100). */
|
|
22
|
+
chunkOverlap?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Split text into overlapping chunks, preferring paragraph/sentence breaks. */
|
|
26
|
+
export function chunkText(text: string, size = 800, overlap = 100): string[] {
|
|
27
|
+
const clean = text.replace(/\r\n/g, '\n').trim();
|
|
28
|
+
if (clean.length <= size) return clean ? [clean] : [];
|
|
29
|
+
const chunks: string[] = [];
|
|
30
|
+
let start = 0;
|
|
31
|
+
while (start < clean.length) {
|
|
32
|
+
let end = Math.min(start + size, clean.length);
|
|
33
|
+
if (end < clean.length) {
|
|
34
|
+
// Back up to the nearest paragraph/sentence/space boundary.
|
|
35
|
+
const slice = clean.slice(start, end);
|
|
36
|
+
const brk = Math.max(
|
|
37
|
+
slice.lastIndexOf('\n\n'),
|
|
38
|
+
slice.lastIndexOf('\n'),
|
|
39
|
+
slice.lastIndexOf('. '),
|
|
40
|
+
slice.lastIndexOf(' '),
|
|
41
|
+
);
|
|
42
|
+
if (brk > size * 0.5) end = start + brk + 1;
|
|
43
|
+
}
|
|
44
|
+
const piece = clean.slice(start, end).trim();
|
|
45
|
+
if (piece) chunks.push(piece);
|
|
46
|
+
if (end >= clean.length) break;
|
|
47
|
+
start = Math.max(end - overlap, start + 1);
|
|
48
|
+
}
|
|
49
|
+
return chunks;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class Retriever {
|
|
53
|
+
private readonly embeddings: EmbeddingProvider;
|
|
54
|
+
private readonly store: VectorStore;
|
|
55
|
+
private readonly chunkSize: number;
|
|
56
|
+
private readonly chunkOverlap: number;
|
|
57
|
+
|
|
58
|
+
constructor(opts: RetrieverOptions) {
|
|
59
|
+
this.embeddings = opts.embeddings;
|
|
60
|
+
this.store = opts.store ?? new InMemoryVectorStore();
|
|
61
|
+
this.chunkSize = opts.chunkSize ?? 800;
|
|
62
|
+
this.chunkOverlap = opts.chunkOverlap ?? 100;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Chunk, embed, and index documents. Returns the number of chunks stored. */
|
|
66
|
+
async ingest(docs: RagDocument[]): Promise<number> {
|
|
67
|
+
const pending: { id: string; text: string; metadata?: Record<string, unknown> }[] = [];
|
|
68
|
+
for (const doc of docs) {
|
|
69
|
+
const pieces = chunkText(doc.text, this.chunkSize, this.chunkOverlap);
|
|
70
|
+
pieces.forEach((text, i) => {
|
|
71
|
+
const baseId = doc.id ?? `doc_${pending.length}`;
|
|
72
|
+
pending.push({ id: `${baseId}#${i}`, text, metadata: doc.metadata });
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
if (pending.length === 0) return 0;
|
|
76
|
+
const vectors = await this.embeddings.embed(pending.map((p) => p.text));
|
|
77
|
+
await this.store.upsert(
|
|
78
|
+
pending.map((p, i) => ({ ...p, embedding: vectors[i] })),
|
|
79
|
+
);
|
|
80
|
+
return pending.length;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Embed the query and return the top-k most similar chunks. */
|
|
84
|
+
async search(query: string, k = 4): Promise<RetrievedChunk[]> {
|
|
85
|
+
if (!query.trim()) return [];
|
|
86
|
+
const [qv] = await this.embeddings.embed([query]);
|
|
87
|
+
if (!qv) return [];
|
|
88
|
+
return this.store.query(qv, k);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
vectorStore(): VectorStore {
|
|
92
|
+
return this.store;
|
|
93
|
+
}
|
|
94
|
+
}
|
package/src/rag/tool.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RAG tool source — exposes `search_knowledge` so the model can pull in
|
|
3
|
+
* relevant context on demand (agentic RAG). Preferred over always-injecting
|
|
4
|
+
* retrieved text, which burns the small-model context window.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ToolDef } from '../types.js';
|
|
8
|
+
import type { ToolSource } from '../tools/source.js';
|
|
9
|
+
import type { Retriever } from './retriever.js';
|
|
10
|
+
|
|
11
|
+
const SEARCH = 'search_knowledge';
|
|
12
|
+
|
|
13
|
+
export interface RagToolOptions {
|
|
14
|
+
/** Chunks to return (default 4). */
|
|
15
|
+
k?: number;
|
|
16
|
+
/** Override the tool description for your corpus, e.g. "Search the docs." */
|
|
17
|
+
description?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function createRagToolSource(retriever: Retriever, opts: RagToolOptions = {}): ToolSource {
|
|
21
|
+
const tool: ToolDef = {
|
|
22
|
+
name: SEARCH,
|
|
23
|
+
description:
|
|
24
|
+
opts.description ??
|
|
25
|
+
'Search the knowledge base for passages relevant to a question and return ' +
|
|
26
|
+
'the best matches. Use this before answering when the answer might be in ' +
|
|
27
|
+
'ingested documents.',
|
|
28
|
+
parameters: {
|
|
29
|
+
type: 'object',
|
|
30
|
+
properties: {
|
|
31
|
+
query: { type: 'string', description: 'What to look up' },
|
|
32
|
+
k: { type: 'number', description: 'How many passages (default 4)' },
|
|
33
|
+
},
|
|
34
|
+
required: ['query'],
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
async function execute(_name: string, args: Record<string, unknown>): Promise<unknown> {
|
|
39
|
+
const query = String(args.query ?? '').trim();
|
|
40
|
+
if (!query) throw new Error('search_knowledge: query is required');
|
|
41
|
+
const k = Number(args.k) > 0 ? Number(args.k) : (opts.k ?? 4);
|
|
42
|
+
const hits = await retriever.search(query, k);
|
|
43
|
+
if (hits.length === 0) return 'No relevant passages found.';
|
|
44
|
+
return hits
|
|
45
|
+
.map((h, i) => `[${i + 1}] (score ${h.score.toFixed(2)}) ${h.text}`)
|
|
46
|
+
.join('\n\n');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
id: 'rag',
|
|
51
|
+
listTools: () => [tool],
|
|
52
|
+
has: (name) => name === SEARCH,
|
|
53
|
+
execute,
|
|
54
|
+
};
|
|
55
|
+
}
|
package/src/rag/types.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RAG types — retrieval-augmented generation primitives.
|
|
3
|
+
*
|
|
4
|
+
* The heavy parts (the embedding model, the vector index) are interfaces the
|
|
5
|
+
* host injects. On QVAC the EmbeddingProvider wraps the SDK `embed()` API
|
|
6
|
+
* (on-device); a server host could inject a remote embedder. The default
|
|
7
|
+
* VectorStore is a pure-JS in-memory cosine index (good for thousands of
|
|
8
|
+
* chunks); a host can swap in SQLite/native for more.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Turns text into vectors. Injected — QVAC `embed()` on device, etc. */
|
|
12
|
+
export interface EmbeddingProvider {
|
|
13
|
+
embed(texts: string[]): Promise<number[][]>;
|
|
14
|
+
/** Vector dimension, when known (informational). */
|
|
15
|
+
dimension?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface Chunk {
|
|
19
|
+
id: string;
|
|
20
|
+
text: string;
|
|
21
|
+
metadata?: Record<string, unknown>;
|
|
22
|
+
embedding?: number[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface RetrievedChunk extends Chunk {
|
|
26
|
+
/** Similarity score in [-1, 1] (cosine). */
|
|
27
|
+
score: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A document to ingest; chunked + embedded by the Retriever. */
|
|
31
|
+
export interface RagDocument {
|
|
32
|
+
id?: string;
|
|
33
|
+
text: string;
|
|
34
|
+
metadata?: Record<string, unknown>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface VectorStore {
|
|
38
|
+
upsert(chunks: Chunk[]): Promise<void>;
|
|
39
|
+
/** Top-k by cosine similarity to `embedding`. */
|
|
40
|
+
query(embedding: number[], k: number): Promise<RetrievedChunk[]>;
|
|
41
|
+
size(): Promise<number>;
|
|
42
|
+
clear(): Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Injected persistence for the vector store (optional). */
|
|
46
|
+
export interface VectorStoreIO {
|
|
47
|
+
load(): Promise<Chunk[]>;
|
|
48
|
+
save(chunks: Chunk[]): Promise<void>;
|
|
49
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory vector store — pure-JS cosine similarity index. Zero deps, so it
|
|
3
|
+
* bundles in Bare/RN. Good for thousands of chunks; swap in a native/SQLite
|
|
4
|
+
* store via the VectorStore interface for larger corpora.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Chunk, RetrievedChunk, VectorStore, VectorStoreIO } from './types.js';
|
|
8
|
+
|
|
9
|
+
/** Cosine similarity of two equal-length vectors. Returns 0 on degenerate input. */
|
|
10
|
+
export function cosineSimilarity(a: number[], b: number[]): number {
|
|
11
|
+
const n = Math.min(a.length, b.length);
|
|
12
|
+
let dot = 0;
|
|
13
|
+
let na = 0;
|
|
14
|
+
let nb = 0;
|
|
15
|
+
for (let i = 0; i < n; i++) {
|
|
16
|
+
dot += a[i]! * b[i]!;
|
|
17
|
+
na += a[i]! * a[i]!;
|
|
18
|
+
nb += b[i]! * b[i]!;
|
|
19
|
+
}
|
|
20
|
+
if (na === 0 || nb === 0) return 0;
|
|
21
|
+
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface InMemoryVectorStoreOptions {
|
|
25
|
+
io?: VectorStoreIO;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class InMemoryVectorStore implements VectorStore {
|
|
29
|
+
private chunks: Chunk[] = [];
|
|
30
|
+
private hydrated = false;
|
|
31
|
+
private readonly io?: VectorStoreIO;
|
|
32
|
+
|
|
33
|
+
constructor(opts: InMemoryVectorStoreOptions = {}) {
|
|
34
|
+
this.io = opts.io;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private async hydrate(): Promise<void> {
|
|
38
|
+
if (this.hydrated) return;
|
|
39
|
+
this.hydrated = true;
|
|
40
|
+
if (this.io) {
|
|
41
|
+
try {
|
|
42
|
+
this.chunks = await this.io.load();
|
|
43
|
+
} catch {
|
|
44
|
+
this.chunks = [];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async upsert(chunks: Chunk[]): Promise<void> {
|
|
50
|
+
await this.hydrate();
|
|
51
|
+
for (const c of chunks) {
|
|
52
|
+
const i = this.chunks.findIndex((x) => x.id === c.id);
|
|
53
|
+
if (i >= 0) this.chunks[i] = c;
|
|
54
|
+
else this.chunks.push(c);
|
|
55
|
+
}
|
|
56
|
+
if (this.io) await this.io.save(this.chunks);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async query(embedding: number[], k: number): Promise<RetrievedChunk[]> {
|
|
60
|
+
await this.hydrate();
|
|
61
|
+
return this.chunks
|
|
62
|
+
.filter((c) => c.embedding && c.embedding.length > 0)
|
|
63
|
+
.map((c) => ({ ...c, score: cosineSimilarity(embedding, c.embedding!) }))
|
|
64
|
+
.sort((a, b) => b.score - a.score)
|
|
65
|
+
.slice(0, k);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async size(): Promise<number> {
|
|
69
|
+
await this.hydrate();
|
|
70
|
+
return this.chunks.length;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async clear(): Promise<void> {
|
|
74
|
+
await this.hydrate();
|
|
75
|
+
this.chunks = [];
|
|
76
|
+
if (this.io) await this.io.save(this.chunks);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in "send an RGB asset" recipe — distinct from the BTC payments recipe
|
|
3
|
+
* because an asset amount must NOT be treated as fiat.
|
|
4
|
+
*
|
|
5
|
+
* "send 10 USDT to bob" → resolve_contact → rln_send_asset 🔒
|
|
6
|
+
* "pay alice 5 XAUT" → resolve_contact → rln_send_asset 🔒
|
|
7
|
+
*
|
|
8
|
+
* Fixes the bug where the payments recipe parsed "USDT" as a fiat currency and
|
|
9
|
+
* ran fiat_to_sats. Payments now excludes RGB assets; this recipe owns them.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { Recipe, RecipeContext } from './types.js';
|
|
13
|
+
|
|
14
|
+
const RGB_ASSET = /\b(usdt|tether|xaut|gold)\b/i;
|
|
15
|
+
const isOnchainOrInvoice = (s: string) => /^(ln(bc|tb|bcrt)|bc1|tb1|lnurl|rgb:|[a-z0-9._-]+@)/i.test(s.trim());
|
|
16
|
+
|
|
17
|
+
function normAsset(a?: string): string | undefined {
|
|
18
|
+
if (!a) return undefined;
|
|
19
|
+
const x = a.toLowerCase();
|
|
20
|
+
if (/usdt|tether/.test(x)) return 'USDT';
|
|
21
|
+
if (/xaut|gold/.test(x)) return 'XAUT';
|
|
22
|
+
return a.toUpperCase();
|
|
23
|
+
}
|
|
24
|
+
function parseAmount(t: string): number | undefined {
|
|
25
|
+
const m = t.match(/(\d[\d.,]*)\s*([km])?\b/i);
|
|
26
|
+
if (!m) return undefined;
|
|
27
|
+
let n = Number(m[1]!.replace(/,/g, ''));
|
|
28
|
+
if (m[2]) n *= m[2].toLowerCase() === 'k' ? 1_000 : 1_000_000;
|
|
29
|
+
return Number.isNaN(n) ? undefined : n;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** "send 10 USDT to bob" / "pay alice 5 xaut". */
|
|
33
|
+
export function extractAssetSend(text: string): Record<string, unknown> | null {
|
|
34
|
+
const t = text.trim();
|
|
35
|
+
if (!/\b(send|pay|transfer)\b/i.test(t)) return null;
|
|
36
|
+
const asset = normAsset(t.match(RGB_ASSET)?.[1]);
|
|
37
|
+
if (!asset) return null; // not an asset send → let the payments recipe handle it
|
|
38
|
+
const amount = parseAmount(t);
|
|
39
|
+
let recipient = t.match(/\bto\s+([^\s,]+)/i)?.[1];
|
|
40
|
+
if (!recipient) {
|
|
41
|
+
const after = t.match(/\b(?:pay|send|transfer)\s+([^\s,]+)/i)?.[1];
|
|
42
|
+
if (after && !/^\d/.test(after) && !RGB_ASSET.test(after)) recipient = after;
|
|
43
|
+
}
|
|
44
|
+
if (!recipient && amount == null) return null;
|
|
45
|
+
return { recipient, asset, amount };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const assetSendRecipe: Recipe = {
|
|
49
|
+
name: 'pay-asset',
|
|
50
|
+
description: 'Send an RGB asset (USDT, XAUT) to a contact or address — resolves the contact, then sends (with confirmation).',
|
|
51
|
+
match: (t) => /\b(send|pay|transfer)\b/i.test(t) && RGB_ASSET.test(t) && !/\b(invoice|receive|swap|buy|sell|for)\b/i.test(t),
|
|
52
|
+
triggers: ['send', 'pay', 'transfer'],
|
|
53
|
+
slots: [
|
|
54
|
+
{ name: 'recipient', type: 'string', description: 'Contact name, address, or RGB invoice', required: true },
|
|
55
|
+
{ name: 'asset', type: 'string', description: 'RGB asset: USDT or XAUT', required: true },
|
|
56
|
+
{ name: 'amount', type: 'number', description: 'Asset amount' },
|
|
57
|
+
],
|
|
58
|
+
extract: extractAssetSend,
|
|
59
|
+
confident: (s) => !!s.recipient && !!s.asset,
|
|
60
|
+
steps: [
|
|
61
|
+
{
|
|
62
|
+
tool: 'resolve_contact',
|
|
63
|
+
as: 'contact',
|
|
64
|
+
args: (ctx) => ({ name: ctx.slots.recipient }),
|
|
65
|
+
skipIf: (ctx) => !ctx.slots.recipient || isOnchainOrInvoice(String(ctx.slots.recipient)),
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
final: {
|
|
69
|
+
tool: 'rln_send_asset',
|
|
70
|
+
args: (ctx: RecipeContext) => {
|
|
71
|
+
const contact = ctx.results.contact as { ln_address?: string } | undefined;
|
|
72
|
+
return { asset: ctx.slots.asset, amount: ctx.slots.amount, to: contact?.ln_address ?? ctx.slots.recipient };
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
summary: (ctx) => {
|
|
76
|
+
const to = (ctx.results.contact as { name?: string } | undefined)?.name ?? ctx.slots.recipient;
|
|
77
|
+
return `Sent ${ctx.slots.amount} ${ctx.slots.asset} to ${to}.`;
|
|
78
|
+
},
|
|
79
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { ToolRegistry } from '../tools/registry.js';
|
|
3
|
+
import { InProcessToolSource } from '../tools/in-process.js';
|
|
4
|
+
import type { LLMProvider } from '../providers/types.js';
|
|
5
|
+
import { runRecipe } from './runner.js';
|
|
6
|
+
import { kaleidoswapAtomicRecipe } from './kaleidoswap-atomic.js';
|
|
7
|
+
|
|
8
|
+
// LLM provider that should never be called when slots are extracted deterministically.
|
|
9
|
+
const refusingProvider: LLMProvider = {
|
|
10
|
+
name: 'refusing',
|
|
11
|
+
runTurn: async () => {
|
|
12
|
+
throw new Error('provider should NOT be called when extractSwap succeeds');
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Stub tools that record every call so we can assert the chain ran end-to-end.
|
|
17
|
+
function buildStubs(captured: { name: string; args: any }[]) {
|
|
18
|
+
const tool = (name: string, response: any, spend = false) => ({
|
|
19
|
+
name,
|
|
20
|
+
description: '',
|
|
21
|
+
parameters: { type: 'object', properties: {} },
|
|
22
|
+
requiresConfirmation: spend,
|
|
23
|
+
handler: async (a: any) => {
|
|
24
|
+
captured.push({ name, args: a });
|
|
25
|
+
return typeof response === 'function' ? response(a) : response;
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
return new ToolRegistry([
|
|
29
|
+
new InProcessToolSource('kaleidoswap', [
|
|
30
|
+
tool('kaleidoswap_get_quote', { quote_id: 'q-1', receive_amount: 100, fees: 250 }),
|
|
31
|
+
tool('kaleidoswap_atomic_init', { atomic_id: 'a-1', maker_invoice: 'lnbc1maker' }, /* spend */ true),
|
|
32
|
+
tool('kaleidoswap_atomic_execute', { status: 'completed' }, /* spend */ true),
|
|
33
|
+
]),
|
|
34
|
+
new InProcessToolSource('rln', [
|
|
35
|
+
tool('rln_create_rgb_invoice', { invoice: 'rgb:invoice:USDT:100' }),
|
|
36
|
+
tool('rln_create_ln_invoice', { invoice: 'lnbc1user' }),
|
|
37
|
+
tool('rln_pay_invoice', { status: 'SUCCESS', payment_hash: 'h' }, /* spend */ true),
|
|
38
|
+
]),
|
|
39
|
+
]);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe('kaleidoswapAtomicRecipe — selection (match + triggers)', () => {
|
|
43
|
+
it('triggers on explicit atomic-swap phrasings', () => {
|
|
44
|
+
expect(kaleidoswapAtomicRecipe.match!('atomic swap 100000 sats for usdt')).toBe(true);
|
|
45
|
+
expect(kaleidoswapAtomicRecipe.match!('trustless swap btc to usdt')).toBe(true);
|
|
46
|
+
expect(kaleidoswapAtomicRecipe.match!('htlc swap 1000 sats to USDT')).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('does NOT fire on a plain swap (those go to swapRecipe)', () => {
|
|
50
|
+
expect(kaleidoswapAtomicRecipe.match!('swap 10 usdt for btc')).toBe(false);
|
|
51
|
+
expect(kaleidoswapAtomicRecipe.match!('exchange 1000 sats for usdt')).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('kaleidoswapAtomicRecipe — RGB receive leg', () => {
|
|
56
|
+
it('runs quote → rgb_invoice → atomic_init → pay → atomic_execute (one inference)', async () => {
|
|
57
|
+
const captured: { name: string; args: any }[] = [];
|
|
58
|
+
const tools = buildStubs(captured);
|
|
59
|
+
|
|
60
|
+
const res = await runRecipe(kaleidoswapAtomicRecipe, 'atomic swap 100000 sats for usdt', {
|
|
61
|
+
provider: refusingProvider,
|
|
62
|
+
tools,
|
|
63
|
+
onConfirm: async () => ({ approved: true }),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
expect(res.status).toBe('done');
|
|
67
|
+
expect(res.inferences).toBe(0); // extractSwap handled it deterministically
|
|
68
|
+
|
|
69
|
+
// The chain: quote → rgb_invoice → atomic_init → pay → atomic_execute (5 calls).
|
|
70
|
+
expect(captured.map((c) => c.name)).toEqual([
|
|
71
|
+
'kaleidoswap_get_quote',
|
|
72
|
+
'rln_create_rgb_invoice',
|
|
73
|
+
'kaleidoswap_atomic_init',
|
|
74
|
+
'rln_pay_invoice',
|
|
75
|
+
'kaleidoswap_atomic_execute',
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
// RGB invoice fed into atomic_init.
|
|
79
|
+
const init = captured.find((c) => c.name === 'kaleidoswap_atomic_init')!;
|
|
80
|
+
expect(init.args).toEqual({ quote_id: 'q-1', receive_invoice: 'rgb:invoice:USDT:100' });
|
|
81
|
+
|
|
82
|
+
// Maker invoice fed into pay step.
|
|
83
|
+
const pay = captured.find((c) => c.name === 'rln_pay_invoice')!;
|
|
84
|
+
expect(pay.args).toEqual({ invoice: 'lnbc1maker' });
|
|
85
|
+
|
|
86
|
+
// Final execute carried the atomic id.
|
|
87
|
+
const exe = captured.find((c) => c.name === 'kaleidoswap_atomic_execute')!;
|
|
88
|
+
expect(exe.args).toEqual({ atomic_id: 'a-1' });
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('kaleidoswapAtomicRecipe — BTC receive leg', () => {
|
|
93
|
+
it('uses rln_create_ln_invoice (not rgb) when to_asset is BTC', async () => {
|
|
94
|
+
const captured: { name: string; args: any }[] = [];
|
|
95
|
+
const tools = buildStubs(captured);
|
|
96
|
+
|
|
97
|
+
const res = await runRecipe(kaleidoswapAtomicRecipe, 'atomic swap 100 usdt for btc', {
|
|
98
|
+
provider: refusingProvider,
|
|
99
|
+
tools,
|
|
100
|
+
onConfirm: async () => ({ approved: true }),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
expect(res.status).toBe('done');
|
|
104
|
+
expect(captured.map((c) => c.name)).toEqual([
|
|
105
|
+
'kaleidoswap_get_quote',
|
|
106
|
+
'rln_create_ln_invoice',
|
|
107
|
+
'kaleidoswap_atomic_init',
|
|
108
|
+
'rln_pay_invoice',
|
|
109
|
+
'kaleidoswap_atomic_execute',
|
|
110
|
+
]);
|
|
111
|
+
const init = captured.find((c) => c.name === 'kaleidoswap_atomic_init')!;
|
|
112
|
+
expect(init.args.receive_invoice).toBe('lnbc1user');
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('kaleidoswapAtomicRecipe — confirmation gate', () => {
|
|
117
|
+
it('cancels the chain when the user declines a spend gate', async () => {
|
|
118
|
+
const captured: { name: string; args: any }[] = [];
|
|
119
|
+
const tools = buildStubs(captured);
|
|
120
|
+
let firstSpendSeen = false;
|
|
121
|
+
|
|
122
|
+
const res = await runRecipe(kaleidoswapAtomicRecipe, 'atomic swap 100000 sats for usdt', {
|
|
123
|
+
provider: refusingProvider,
|
|
124
|
+
tools,
|
|
125
|
+
onConfirm: async () => {
|
|
126
|
+
if (firstSpendSeen) return { approved: true };
|
|
127
|
+
firstSpendSeen = true;
|
|
128
|
+
return { approved: false, reason: 'user said no' };
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
expect(res.status).not.toBe('done');
|
|
133
|
+
// The first spend tool (atomic_init) should NOT have completed successfully —
|
|
134
|
+
// the chain stops before pay/execute.
|
|
135
|
+
expect(captured.some((c) => c.name === 'rln_pay_invoice')).toBe(false);
|
|
136
|
+
expect(captured.some((c) => c.name === 'kaleidoswap_atomic_execute')).toBe(false);
|
|
137
|
+
});
|
|
138
|
+
});
|