Skip to content

Commit be4d6b8

Browse files
committed
Add phase 3 local ICP example
1 parent a809c0d commit be4d6b8

15 files changed

Lines changed: 282 additions & 7 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.dfx/
2+
canister_ids.json
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# icp-knowledge-canister
2+
3+
Minimal local `dfx` example for deploying the Knolo ICP canister without any middleware server.
4+
5+
The example:
6+
7+
- builds a real `.knolo` pack from local sample docs
8+
- deploys the Rust canister from `packages/icp-canister`
9+
- uploads the pack with `dfx canister call`
10+
- queries the canister directly
11+
12+
## Prerequisites
13+
14+
From the repo root:
15+
16+
```bash
17+
npm install
18+
```
19+
20+
Local ICP development also needs `dfx` plus the Rust wasm target:
21+
22+
```bash
23+
rustup target add wasm32-unknown-unknown
24+
```
25+
26+
## Run The Example
27+
28+
```bash
29+
cd examples/icp-knowledge-canister
30+
dfx start --background
31+
dfx deploy
32+
node scripts/build-sample-pack.mjs
33+
bash scripts/upload-pack.sh
34+
bash scripts/query.sh "alpha beta"
35+
```
36+
37+
If `dfx` is running under a minimal shell and complains about terminal colors, rerun the `dfx` commands with:
38+
39+
```bash
40+
TERM=xterm-256color dfx start --background
41+
TERM=xterm-256color dfx deploy
42+
```
43+
44+
## What Gets Built
45+
46+
- `dfx deploy` compiles the canister from `../../packages/icp-canister`
47+
- `node scripts/build-sample-pack.mjs` writes `dist/knowledge.knolo`
48+
- `bash scripts/upload-pack.sh` calls `set_pack(bytes, label)`
49+
- `bash scripts/query.sh "alpha beta"` calls `search("alpha beta", 5)`
50+
51+
## Sample Content
52+
53+
The pack is built from the checked-in files under `knowledge/`, so the example stays deterministic and easy to rerun locally.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"version": 1,
3+
"dfx": "0.20.0",
4+
"canisters": {
5+
"knolo_knowledge": {
6+
"type": "custom",
7+
"candid": "../../packages/icp-canister/knolo_icp.did",
8+
"wasm": "../../packages/icp-canister/target/wasm32-unknown-unknown/release/knolo_icp_canister.wasm",
9+
"build": [
10+
"cargo build --target wasm32-unknown-unknown --release --manifest-path ../../packages/icp-canister/Cargo.toml"
11+
],
12+
"metadata": [
13+
{
14+
"name": "candid:service"
15+
}
16+
],
17+
"declarations": {
18+
"bindings": [
19+
"did"
20+
],
21+
"output": "./.dfx/local/declarations/knolo_knowledge"
22+
}
23+
}
24+
}
25+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"version": 1,
3+
"sources": [
4+
{
5+
"name": "docs",
6+
"path": "./knowledge"
7+
}
8+
],
9+
"output": {
10+
"path": "./dist/knowledge.knolo"
11+
},
12+
"query": {
13+
"topK": 5
14+
}
15+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Alpha Guide
2+
3+
Alpha beta deployment notes for Knolo on the Internet Computer.
4+
5+
This sample document explains that a developer can build a local knowledge pack, upload it to the canister with `set_pack`, and run deterministic lexical search without a middleware server.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Beta Notes
2+
3+
Beta guidance for local testing.
4+
5+
Use the sample query `alpha beta` after uploading the pack. The canister should return this note and the alpha guide as direct lexical hits.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#!/usr/bin/env node
2+
3+
import { execFileSync } from 'node:child_process';
4+
import { existsSync } from 'node:fs';
5+
import path from 'node:path';
6+
import { fileURLToPath } from 'node:url';
7+
8+
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
9+
const exampleDir = path.resolve(scriptDir, '..');
10+
const repoRoot = path.resolve(exampleDir, '../..');
11+
const cliPath = path.join(repoRoot, 'packages/cli/bin/knolo.mjs');
12+
const outPath = path.join(exampleDir, 'dist/knowledge.knolo');
13+
14+
if (!existsSync(cliPath)) {
15+
console.error(`knolo CLI not found at ${cliPath}`);
16+
process.exit(1);
17+
}
18+
19+
console.log('[build-sample-pack] Building @knolo/core');
20+
execFileSync('npm', ['run', 'build', '--workspace', '@knolo/core'], {
21+
cwd: repoRoot,
22+
stdio: 'inherit',
23+
});
24+
25+
console.log('[build-sample-pack] Building dist/knowledge.knolo');
26+
execFileSync(process.execPath, [cliPath, 'build'], {
27+
cwd: exampleDir,
28+
stdio: 'inherit',
29+
});
30+
31+
if (!existsSync(outPath)) {
32+
console.error(`Expected pack output at ${outPath}`);
33+
process.exit(1);
34+
}
35+
36+
console.log('[build-sample-pack] OK');
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5+
DFX_BIN="${DFX_BIN:-dfx}"
6+
CANISTER_NAME="${CANISTER_NAME:-knolo_knowledge}"
7+
QUERY_TEXT="${1:-alpha beta}"
8+
TOP_K="${TOP_K:-5}"
9+
ARGS_FILE="$(mktemp "${TMPDIR:-/tmp}/knolo-query-XXXXXX.did")"
10+
11+
if [ "${TERM:-}" = 'dumb' ]; then
12+
export TERM=xterm-256color
13+
fi
14+
15+
cleanup() {
16+
rm -f "$ARGS_FILE"
17+
}
18+
19+
trap cleanup EXIT
20+
21+
node --input-type=module - "$QUERY_TEXT" "$TOP_K" "$ARGS_FILE" <<'EOF'
22+
import { writeFileSync } from 'node:fs';
23+
24+
const [queryText, topK, outPath] = process.argv.slice(2);
25+
const renderedTopK = Number.parseInt(topK, 10);
26+
27+
if (!Number.isInteger(renderedTopK) || renderedTopK <= 0) {
28+
console.error(`TOP_K must be a positive integer. Received: ${topK}`);
29+
process.exit(1);
30+
}
31+
32+
writeFileSync(outPath, `(${JSON.stringify(queryText)}, ${renderedTopK} : nat32)\n`);
33+
EOF
34+
35+
cd "$ROOT"
36+
"$DFX_BIN" canister call "$CANISTER_NAME" search --query --argument-file "$ARGS_FILE" --output json
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5+
DFX_BIN="${DFX_BIN:-dfx}"
6+
CANISTER_NAME="${CANISTER_NAME:-knolo_knowledge}"
7+
PACK_LABEL="${PACK_LABEL:-sample-knowledge-pack}"
8+
PACK_PATH="${PACK_PATH:-$ROOT/dist/knowledge.knolo}"
9+
ARGS_FILE="$(mktemp "${TMPDIR:-/tmp}/knolo-set-pack-XXXXXX.did")"
10+
11+
if [ "${TERM:-}" = 'dumb' ]; then
12+
export TERM=xterm-256color
13+
fi
14+
15+
cleanup() {
16+
rm -f "$ARGS_FILE"
17+
}
18+
19+
trap cleanup EXIT
20+
21+
if [ ! -f "$PACK_PATH" ]; then
22+
echo "Pack file not found at $PACK_PATH. Run node scripts/build-sample-pack.mjs first." >&2
23+
exit 1
24+
fi
25+
26+
node --input-type=module - "$PACK_PATH" "$PACK_LABEL" "$ARGS_FILE" <<'EOF'
27+
import { readFileSync, writeFileSync } from 'node:fs';
28+
29+
const [packPath, label, outPath] = process.argv.slice(2);
30+
const bytes = readFileSync(packPath);
31+
const renderedBytes = bytes.length
32+
? ` ${Array.from(bytes, (value) => `${value}`).join('; ')} `
33+
: '';
34+
35+
writeFileSync(outPath, `(vec {${renderedBytes}}, ${JSON.stringify(label)})\n`);
36+
EOF
37+
38+
cd "$ROOT"
39+
"$DFX_BIN" canister call "$CANISTER_NAME" set_pack --argument-file "$ARGS_FILE" --output json
40+
"$DFX_BIN" canister call "$CANISTER_NAME" pack_info --query --output json

packages/core-rust/src/lib.rs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -441,7 +441,34 @@ fn tokenize(text: &str) -> Vec<String> {
441441
}
442442

443443
fn compact(s: &str) -> String {
444-
s.chars().filter(|c| !c.is_whitespace()).collect()
444+
let mut out = String::with_capacity(s.len());
445+
let mut in_string = false;
446+
let mut escaped = false;
447+
448+
for ch in s.chars() {
449+
if in_string {
450+
out.push(ch);
451+
if escaped {
452+
escaped = false;
453+
} else if ch == '\\' {
454+
escaped = true;
455+
} else if ch == '"' {
456+
in_string = false;
457+
}
458+
continue;
459+
}
460+
461+
if ch.is_whitespace() {
462+
continue;
463+
}
464+
465+
out.push(ch);
466+
if ch == '"' {
467+
in_string = true;
468+
}
469+
}
470+
471+
out
445472
}
446473

447474
fn unescape(s: &str) -> String {

0 commit comments

Comments
 (0)