persnally 2.6.0 → 2.6.1
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/build/src/consolidate.d.ts +1 -1
- package/build/src/consolidate.js +5 -1
- package/build/src/daemon.js +49 -2
- package/build/src/dashboard.html +73 -2
- package/build/src/llm.d.ts +9 -2
- package/build/src/llm.js +55 -10
- package/package.json +1 -1
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* and re-synthesizes the profile. Last-run state lives in config, not
|
|
6
6
|
* the event log — it's operational state, not user data.
|
|
7
7
|
*/
|
|
8
|
-
import type
|
|
8
|
+
import { type ChosenExtractor } from "./llm.js";
|
|
9
9
|
import type { EventStore } from "./store.js";
|
|
10
10
|
export declare const CONSOLIDATION_HOUR = 3;
|
|
11
11
|
export interface ConsolidationResult {
|
package/build/src/consolidate.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { z } from "zod";
|
|
9
9
|
import { loadConfig, saveConfig } from "./config.js";
|
|
10
10
|
import { newEvent, PAYLOAD_SCHEMAS } from "./events.js";
|
|
11
|
+
import { chooseExtractor } from "./llm.js";
|
|
11
12
|
import { synthesizeProfile } from "./profile.js";
|
|
12
13
|
const ASSERTION_MIN_SIGNALS = 5;
|
|
13
14
|
const PROFILE_MIN_SIGNALS = 10;
|
|
@@ -61,9 +62,12 @@ export async function runConsolidation(store, engine, now = new Date()) {
|
|
|
61
62
|
store.rebuild(now.getTime());
|
|
62
63
|
}
|
|
63
64
|
}
|
|
65
|
+
// The profile is the centerpiece artifact — always synthesize it with the
|
|
66
|
+
// profile model, not the cheaper assertions engine passed in as `engine`.
|
|
64
67
|
let profileRefreshed = false;
|
|
65
68
|
if (engine && newSignals.length >= PROFILE_MIN_SIGNALS) {
|
|
66
|
-
await
|
|
69
|
+
const profileEngine = await chooseExtractor("profile");
|
|
70
|
+
await synthesizeProfile(store, profileEngine.extract, profileEngine.model);
|
|
67
71
|
profileRefreshed = true;
|
|
68
72
|
}
|
|
69
73
|
saveConfig({ last_consolidation: now.toISOString() });
|
package/build/src/daemon.js
CHANGED
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import http from "node:http";
|
|
6
6
|
import { readFileSync } from "node:fs";
|
|
7
|
-
import { loadConfig } from "./config.js";
|
|
7
|
+
import { loadConfig, saveConfig } from "./config.js";
|
|
8
8
|
import { runConsolidation, shouldRunNow } from "./consolidate.js";
|
|
9
9
|
import { allowedCategories, loadScopes } from "./permissions.js";
|
|
10
10
|
import { newEvent, validateEvent } from "./events.js";
|
|
11
11
|
import { importNewClaudeCodeSessions } from "./importers/claude-code.js";
|
|
12
|
-
import { chooseExtractor } from "./llm.js";
|
|
12
|
+
import { chooseExtractor, ollamaTags, pullOllamaModel, RECOMMENDED_LOCAL_MODEL } from "./llm.js";
|
|
13
13
|
import { synthesizeProfile } from "./profile.js";
|
|
14
14
|
export const DEFAULT_PORT = 4983;
|
|
15
15
|
const MAX_BODY_BYTES = 25 * 1024 * 1024; // generous for import batches; bounds memory
|
|
@@ -17,6 +17,7 @@ const MAX_QUERY_LIMIT = 10_000; // ceiling for public ?limit= params
|
|
|
17
17
|
// Single source of truth for the user-visible version: package.json.
|
|
18
18
|
const pkg = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf-8"));
|
|
19
19
|
export const VERSION = pkg.version;
|
|
20
|
+
let pull = { state: "idle", model: "", percent: 0, status: "", error: "" };
|
|
20
21
|
export function startDaemon(store, port = DEFAULT_PORT) {
|
|
21
22
|
const localHosts = [`127.0.0.1:${port}`, `localhost:${port}`];
|
|
22
23
|
const server = http.createServer(async (req, res) => {
|
|
@@ -83,6 +84,52 @@ export function startDaemon(store, port = DEFAULT_PORT) {
|
|
|
83
84
|
const engine = await chooseExtractor("extract").catch(() => null);
|
|
84
85
|
return json(res, 200, await runConsolidation(store, engine));
|
|
85
86
|
}
|
|
87
|
+
// Engine onboarding: status + live key-save + one-click local-model pull.
|
|
88
|
+
if (req.method === "GET" && url.pathname === "/engine") {
|
|
89
|
+
const tags = await ollamaTags();
|
|
90
|
+
const cfgKey = loadConfig().anthropic_api_key;
|
|
91
|
+
const key = process.env.ANTHROPIC_API_KEY || (typeof cfgKey === "string" ? cfgKey : "");
|
|
92
|
+
return json(res, 200, {
|
|
93
|
+
hasKey: key.startsWith("sk-ant-"),
|
|
94
|
+
keyMasked: key ? `${key.slice(0, 12)}…${key.slice(-4)}` : "",
|
|
95
|
+
hasProfile: !!store.getProfile(),
|
|
96
|
+
ollama: { reachable: tags !== null, models: tags ?? [], hasModel: (tags?.length ?? 0) > 0 },
|
|
97
|
+
recommended: RECOMMENDED_LOCAL_MODEL,
|
|
98
|
+
pull,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
if (req.method === "POST" && url.pathname === "/engine/key") {
|
|
102
|
+
if (!(req.headers["content-type"] ?? "").includes("application/json")) {
|
|
103
|
+
return json(res, 415, { error: "Content-Type must be application/json" });
|
|
104
|
+
}
|
|
105
|
+
const body = (await readBody(req));
|
|
106
|
+
const key = typeof body.key === "string" ? body.key.trim() : "";
|
|
107
|
+
if (!key.startsWith("sk-ant-"))
|
|
108
|
+
return json(res, 400, { error: "expected an Anthropic key (sk-ant-…)" });
|
|
109
|
+
saveConfig({ anthropic_api_key: key });
|
|
110
|
+
process.env.ANTHROPIC_API_KEY = key; // apply to the running daemon — no restart needed
|
|
111
|
+
return json(res, 200, { ok: true, keyMasked: `${key.slice(0, 12)}…${key.slice(-4)}` });
|
|
112
|
+
}
|
|
113
|
+
if (req.method === "POST" && url.pathname === "/engine/pull") {
|
|
114
|
+
if (!(req.headers["content-type"] ?? "").includes("application/json")) {
|
|
115
|
+
return json(res, 415, { error: "Content-Type must be application/json" });
|
|
116
|
+
}
|
|
117
|
+
if (pull.state === "pulling")
|
|
118
|
+
return json(res, 200, { started: false, ...pull });
|
|
119
|
+
const body = (await readBody(req).catch(() => ({})));
|
|
120
|
+
const model = typeof body.model === "string" && body.model ? body.model : RECOMMENDED_LOCAL_MODEL;
|
|
121
|
+
if ((await ollamaTags()) === null) {
|
|
122
|
+
return json(res, 400, { error: "Ollama isn't running. Install it from ollama.com, then try again." });
|
|
123
|
+
}
|
|
124
|
+
pull = { state: "pulling", model, percent: 0, status: "starting", error: "" };
|
|
125
|
+
pullOllamaModel(model, (p) => { pull.percent = p.percent; pull.status = p.status; })
|
|
126
|
+
.then(() => { pull = { ...pull, state: "done", percent: 100, status: "ready" }; })
|
|
127
|
+
.catch((e) => { pull = { ...pull, state: "error", error: e instanceof Error ? e.message : String(e) }; });
|
|
128
|
+
return json(res, 200, { started: true, model });
|
|
129
|
+
}
|
|
130
|
+
if (req.method === "GET" && url.pathname === "/engine/pull") {
|
|
131
|
+
return json(res, 200, pull);
|
|
132
|
+
}
|
|
86
133
|
if (req.method === "GET" && url.pathname === "/events") {
|
|
87
134
|
const ids = url.searchParams.get("ids");
|
|
88
135
|
if (ids)
|
package/build/src/dashboard.html
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
6
|
<title>Persnally — your context engine</title>
|
|
7
|
+
<link rel="icon" type="image/png" href="data:image/png;base64,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" />
|
|
7
8
|
<style>
|
|
8
9
|
:root {
|
|
9
10
|
--bg: #0B0C0E; /* dark, default */
|
|
@@ -239,6 +240,17 @@
|
|
|
239
240
|
|
|
240
241
|
<div id="delta"></div>
|
|
241
242
|
|
|
243
|
+
<style>
|
|
244
|
+
.onboard{margin:0 0 22px}
|
|
245
|
+
.onboard .ob-card{border:1px solid #2c67ff55;box-shadow:0 0 0 1px #2c67ff14}
|
|
246
|
+
.onboard .ob-or{margin:16px 0 8px;color:#8b90a0;font-size:13px}
|
|
247
|
+
.onboard .ob-row{display:flex;gap:8px;max-width:540px}
|
|
248
|
+
.onboard .ob-bar{height:8px;border-radius:6px;background:#ffffff14;overflow:hidden;margin-top:10px}
|
|
249
|
+
.onboard .ob-fill{height:100%;width:0;background:#2c67ff;transition:width .35s ease}
|
|
250
|
+
.onboard input{flex:1;background:#0e1016;border:1px solid #1b1e26;border-radius:8px;color:#f5f6f8;padding:9px 12px;font:inherit;min-width:0}
|
|
251
|
+
</style>
|
|
252
|
+
<div id="onboard" class="onboard" style="display:none"></div>
|
|
253
|
+
|
|
242
254
|
<div class="hero reveal" style="animation-delay:.05s">
|
|
243
255
|
<div class="scale-beat" id="scaleBeat"></div>
|
|
244
256
|
<div class="archetype" id="archetype">Reading your context…</div>
|
|
@@ -714,6 +726,62 @@ function switchView(v) {
|
|
|
714
726
|
$("vGraph").onclick = () => switchView("map");
|
|
715
727
|
$("vList").onclick = () => switchView("list");
|
|
716
728
|
|
|
729
|
+
/* ── engine onboarding (local-first: Ollama one-click, key optional) ── */
|
|
730
|
+
function renderOnboard(engine, profile) {
|
|
731
|
+
const el = $("onboard");
|
|
732
|
+
// Show only when there's no engine at all and no portrait yet — once either
|
|
733
|
+
// exists the normal dashboard (and the Re-synthesize button) take over.
|
|
734
|
+
const ready = engine && (engine.hasKey || (engine.ollama && engine.ollama.hasModel));
|
|
735
|
+
if (DEMO || !engine || ready || profile) { el.style.display = "none"; el.innerHTML = ""; return; }
|
|
736
|
+
el.style.display = "";
|
|
737
|
+
const oll = engine.ollama || { reachable:false };
|
|
738
|
+
const local = oll.reachable
|
|
739
|
+
? `<button class="btn" id="obPull">Set up local AI — download model (~2GB)</button>
|
|
740
|
+
<div class="sub" style="margin-top:6px">Runs entirely on your machine. Free. Nothing leaves your computer.</div>
|
|
741
|
+
<div id="obProg" style="display:none"><div class="ob-bar"><div class="ob-fill" id="obFill"></div></div><div class="sub" id="obStat" style="margin-top:6px"></div></div>`
|
|
742
|
+
: `<a class="btn" href="https://ollama.com/download" target="_blank" rel="noopener">Install Ollama — local & free <span class="arr">↗</span></a>
|
|
743
|
+
<div class="sub" style="margin-top:6px">One install and your data stays on this machine. <button class="link-btn" id="obRecheck">Recheck</button> once it's running.</div>`;
|
|
744
|
+
el.innerHTML = `<div class="card ob-card">
|
|
745
|
+
<div class="claim-title">Set up your AI engine to see your portrait</div>
|
|
746
|
+
<div class="sub" style="margin:4px 0 14px">Persnally needs a model to read your history and write your self-portrait. Choose local (private, free) — or bring your own key.</div>
|
|
747
|
+
<div style="display:flex;flex-direction:column;gap:2px;max-width:540px">${local}</div>
|
|
748
|
+
<div class="ob-or">— or use your own Anthropic key —</div>
|
|
749
|
+
<div class="ob-row"><input id="obKey" type="password" placeholder="sk-ant-…" autocomplete="off" spellcheck="false"><button class="btn ghost" id="obSaveKey">Save & use</button></div>
|
|
750
|
+
<div class="sub" id="obKeyMsg" style="margin-top:6px"></div>
|
|
751
|
+
<div class="sub" style="margin-top:14px">No model yet? You can still import code offline — <code>persnallyd import git <path></code> (no AI needed).</div>
|
|
752
|
+
</div>`;
|
|
753
|
+
if (oll.reachable) $("obPull").onclick = () => startPull(engine.recommended);
|
|
754
|
+
else $("obRecheck").onclick = () => loadAll();
|
|
755
|
+
$("obSaveKey").onclick = saveKey;
|
|
756
|
+
$("obKey").addEventListener("keydown", (e) => { if (e.key === "Enter") saveKey(); });
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
async function startPull(model) {
|
|
760
|
+
$("obProg").style.display = ""; $("obPull").disabled = true;
|
|
761
|
+
$("obStat").textContent = "Starting…";
|
|
762
|
+
const r = await fetch("/engine/pull", { method:"POST", headers:{ "content-type":"application/json" }, body: JSON.stringify({ model }) });
|
|
763
|
+
if (!r.ok) { const e = await r.json().catch(()=>({})); $("obStat").textContent = e.error || ("Couldn't start (" + r.status + ")"); $("obPull").disabled=false; return; }
|
|
764
|
+
(async function poll() {
|
|
765
|
+
const p = await get("/engine/pull");
|
|
766
|
+
if (!p) return void setTimeout(poll, 1500);
|
|
767
|
+
$("obFill").style.width = (p.percent||0) + "%";
|
|
768
|
+
if (p.state === "pulling") { $("obStat").textContent = (p.status||"downloading") + " · " + (p.percent||0) + "%"; return void setTimeout(poll, 1500); }
|
|
769
|
+
if (p.state === "error") { $("obStat").textContent = "Failed: " + (p.error||"unknown"); $("obPull").disabled=false; return; }
|
|
770
|
+
if (p.state === "done") { $("obStat").textContent = "Model ready — writing your portrait…"; await fetch("/synthesize", { method:"POST" }); return void loadAll(); }
|
|
771
|
+
setTimeout(poll, 1500);
|
|
772
|
+
})();
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
async function saveKey() {
|
|
776
|
+
const key = $("obKey").value.trim();
|
|
777
|
+
if (!key.startsWith("sk-ant-")) { $("obKeyMsg").textContent = "Enter a key starting with sk-ant-"; return; }
|
|
778
|
+
$("obSaveKey").disabled = true; $("obKeyMsg").textContent = "Saving…";
|
|
779
|
+
const r = await fetch("/engine/key", { method:"POST", headers:{ "content-type":"application/json" }, body: JSON.stringify({ key }) });
|
|
780
|
+
if (!r.ok) { const e = await r.json().catch(()=>({})); $("obKeyMsg").textContent = e.error || ("Failed (" + r.status + ")"); $("obSaveKey").disabled=false; return; }
|
|
781
|
+
$("obKeyMsg").textContent = "Saved — writing your portrait…";
|
|
782
|
+
await fetch("/synthesize", { method:"POST" }); loadAll();
|
|
783
|
+
}
|
|
784
|
+
|
|
717
785
|
/* ── load ── */
|
|
718
786
|
async function loadAll() {
|
|
719
787
|
let stats, profile, topics, reads, assertions, voice, activity;
|
|
@@ -728,6 +796,7 @@ async function loadAll() {
|
|
|
728
796
|
({ stats, profile, topics, reads, assertions, voice, activity } = DEMO_DATA());
|
|
729
797
|
}
|
|
730
798
|
topics = topics||[]; reads = reads||[]; assertions = assertions||[];
|
|
799
|
+
const engine = DEMO ? null : await get("/engine");
|
|
731
800
|
const st = $("status");
|
|
732
801
|
st.querySelector(".dot").classList.toggle("off", DEMO);
|
|
733
802
|
st.querySelector(".lbl").textContent = DEMO ? "preview" : "active";
|
|
@@ -736,6 +805,7 @@ async function loadAll() {
|
|
|
736
805
|
renderDelta(topics, reads, assertions);
|
|
737
806
|
renderScaleBeat(stats);
|
|
738
807
|
renderPortrait(profile);
|
|
808
|
+
renderOnboard(engine, profile);
|
|
739
809
|
renderVoice(voice);
|
|
740
810
|
renderTopicList(topics);
|
|
741
811
|
renderMap(topics);
|
|
@@ -749,7 +819,8 @@ async function loadAll() {
|
|
|
749
819
|
$("synthesize").onclick = async () => {
|
|
750
820
|
const b = $("synthesize"); if (DEMO) return alert("Preview mode — start persnallyd to synthesize for real.");
|
|
751
821
|
b.disabled = true; b.textContent = "Synthesizing…";
|
|
752
|
-
const r = await fetch("/synthesize", { method:"POST" });
|
|
822
|
+
const r = await fetch("/synthesize", { method:"POST" });
|
|
823
|
+
if (!r.ok) { const e = await r.json().catch(()=>({})); alert(e.error || "Synthesis failed — check the daemon log."); }
|
|
753
824
|
b.disabled = false; b.textContent = "Re-synthesize"; loadAll();
|
|
754
825
|
};
|
|
755
826
|
$("reflect").onclick = async () => {
|
|
@@ -917,7 +988,7 @@ function DEMO_DATA() {
|
|
|
917
988
|
const now = Date.now(), iso = (d)=>new Date(now-d).toISOString();
|
|
918
989
|
return {
|
|
919
990
|
stats: { total:4127, byType:{ "signal.topic":312, "signal.assertion":14, "context.read":1247 }, bySource:{ "git:persnallyd":58 }, first:iso(86400000*214), last:iso(3600000*2) },
|
|
920
|
-
profile: { headline:"A refactor-first systems thinker who ships hardest under a deadline.", model:"
|
|
991
|
+
profile: { headline:"A refactor-first systems thinker who ships hardest under a deadline.", model:"opus-4-8", generated_at:iso(3600000*5).slice(0,10),
|
|
921
992
|
sections:[
|
|
922
993
|
{ title:"How you work", body:"You build in tight, evidence-driven loops — prototype, measure, delete. You reach for SQLite and local-first architectures before reaching for a cloud, and you distrust abstractions you can't audit. Your commits are small and frequent, and you keep a working build at every step rather than big-bang merges.", evidence_event_ids:["e1","e2","e3","e4"] },
|
|
923
994
|
{ title:"How you decide", body:"You kill ideas fast and out loud. You actively seek the case against your own plans, and you change course when the evidence turns — rare for someone who generates this many ideas. You ask for the falsification first.", evidence_event_ids:["e5","e6","e7"] },
|
package/build/src/llm.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Structured extraction shared by importers and profile synthesis.
|
|
3
|
-
* Uses output_config structured outputs (
|
|
4
|
-
* including Fable 5 where forced tool_choice is not supported).
|
|
3
|
+
* Uses output_config structured outputs (broadly supported; no forced tool_choice).
|
|
5
4
|
*/
|
|
6
5
|
import { z } from "zod";
|
|
7
6
|
export declare const DEFAULT_EXTRACT_MODEL: string;
|
|
@@ -15,6 +14,14 @@ export type LlmExtract = (opts: {
|
|
|
15
14
|
}) => Promise<unknown>;
|
|
16
15
|
/** Default extractor backed by the Anthropic API; injectable for tests. */
|
|
17
16
|
export declare const anthropicExtract: LlmExtract;
|
|
17
|
+
export declare const RECOMMENDED_LOCAL_MODEL = "llama3.2";
|
|
18
|
+
/** Installed Ollama models, or null if Ollama isn't reachable (not installed / not running). */
|
|
19
|
+
export declare function ollamaTags(): Promise<string[] | null>;
|
|
20
|
+
/** Streams an `ollama pull`, reporting download progress (0–100). Throws on failure. */
|
|
21
|
+
export declare function pullOllamaModel(model: string, onProgress: (p: {
|
|
22
|
+
status: string;
|
|
23
|
+
percent: number;
|
|
24
|
+
}) => void): Promise<void>;
|
|
18
25
|
export declare const ollamaExtract: LlmExtract;
|
|
19
26
|
export interface ChosenExtractor {
|
|
20
27
|
extract: LlmExtract;
|
package/build/src/llm.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Structured extraction shared by importers and profile synthesis.
|
|
3
|
-
* Uses output_config structured outputs (
|
|
4
|
-
* including Fable 5 where forced tool_choice is not supported).
|
|
3
|
+
* Uses output_config structured outputs (broadly supported; no forced tool_choice).
|
|
5
4
|
*/
|
|
6
5
|
import Anthropic from "@anthropic-ai/sdk";
|
|
7
6
|
import { zodOutputFormat } from "@anthropic-ai/sdk/helpers/zod";
|
|
8
7
|
import { z } from "zod";
|
|
9
8
|
export const DEFAULT_EXTRACT_MODEL = process.env.PERSNALLY_MODEL ?? "claude-haiku-4-5";
|
|
10
|
-
export const DEFAULT_PROFILE_MODEL = process.env.PERSNALLY_PROFILE_MODEL ?? "claude-
|
|
9
|
+
export const DEFAULT_PROFILE_MODEL = process.env.PERSNALLY_PROFILE_MODEL ?? "claude-opus-4-8";
|
|
11
10
|
/** Default extractor backed by the Anthropic API; injectable for tests. */
|
|
12
11
|
export const anthropicExtract = async ({ model, instruction, schema, content, maxTokens }) => {
|
|
13
12
|
const client = new Anthropic();
|
|
@@ -29,6 +28,56 @@ export const anthropicExtract = async ({ model, instruction, schema, content, ma
|
|
|
29
28
|
// ── Local extraction via Ollama: zero key, zero cloud ───────
|
|
30
29
|
const OLLAMA_URL = process.env.PERSNALLY_OLLAMA_URL ?? "http://127.0.0.1:11434";
|
|
31
30
|
const LOCAL_MODEL_PREFERENCE = ["qwen2.5:14b", "qwen2.5:7b", "llama3.1:8b", "llama3.2", "qwen2.5:1.5b"];
|
|
31
|
+
// Smallest model that still yields a usable profile — the one-click local default.
|
|
32
|
+
// (~2GB; bigger models in LOCAL_MODEL_PREFERENCE win automatically once present.)
|
|
33
|
+
export const RECOMMENDED_LOCAL_MODEL = "llama3.2";
|
|
34
|
+
/** Installed Ollama models, or null if Ollama isn't reachable (not installed / not running). */
|
|
35
|
+
export async function ollamaTags() {
|
|
36
|
+
try {
|
|
37
|
+
const r = await fetch(`${OLLAMA_URL}/api/tags`, { signal: AbortSignal.timeout(1500) });
|
|
38
|
+
if (!r.ok)
|
|
39
|
+
return null;
|
|
40
|
+
return (await r.json()).models?.map((m) => m.name) ?? [];
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** Streams an `ollama pull`, reporting download progress (0–100). Throws on failure. */
|
|
47
|
+
export async function pullOllamaModel(model, onProgress) {
|
|
48
|
+
const r = await fetch(`${OLLAMA_URL}/api/pull`, {
|
|
49
|
+
method: "POST",
|
|
50
|
+
body: JSON.stringify({ name: model, stream: true }),
|
|
51
|
+
});
|
|
52
|
+
if (!r.ok || !r.body)
|
|
53
|
+
throw new Error(`ollama pull: ${r.status} ${(await r.text().catch(() => "")).slice(0, 200)}`);
|
|
54
|
+
const reader = r.body.getReader();
|
|
55
|
+
const decoder = new TextDecoder();
|
|
56
|
+
let buf = "";
|
|
57
|
+
for (;;) {
|
|
58
|
+
const { done, value } = await reader.read();
|
|
59
|
+
if (done)
|
|
60
|
+
break;
|
|
61
|
+
buf += decoder.decode(value, { stream: true });
|
|
62
|
+
const lines = buf.split("\n");
|
|
63
|
+
buf = lines.pop() ?? ""; // keep the partial last line for the next chunk
|
|
64
|
+
for (const line of lines) {
|
|
65
|
+
if (!line.trim())
|
|
66
|
+
continue;
|
|
67
|
+
let obj;
|
|
68
|
+
try {
|
|
69
|
+
obj = JSON.parse(line);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (obj.error)
|
|
75
|
+
throw new Error(`ollama pull: ${obj.error}`);
|
|
76
|
+
const percent = obj.total ? Math.round(((obj.completed ?? 0) / obj.total) * 100) : 0;
|
|
77
|
+
onProgress({ status: obj.status ?? "", percent });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
32
81
|
export const ollamaExtract = async ({ model, instruction, schema, content }) => {
|
|
33
82
|
const r = await fetch(`${OLLAMA_URL}/api/chat`, {
|
|
34
83
|
method: "POST",
|
|
@@ -50,14 +99,10 @@ export const ollamaExtract = async ({ model, instruction, schema, content }) =>
|
|
|
50
99
|
async function localModel() {
|
|
51
100
|
if (process.env.PERSNALLY_LOCAL_MODEL)
|
|
52
101
|
return process.env.PERSNALLY_LOCAL_MODEL;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const tags = (await r.json()).models?.map((m) => m.name) ?? [];
|
|
56
|
-
return LOCAL_MODEL_PREFERENCE.find((p) => tags.some((t) => t === p || t === `${p}:latest`)) ?? tags[0] ?? null;
|
|
57
|
-
}
|
|
58
|
-
catch {
|
|
102
|
+
const tags = await ollamaTags();
|
|
103
|
+
if (!tags)
|
|
59
104
|
return null;
|
|
60
|
-
}
|
|
105
|
+
return LOCAL_MODEL_PREFERENCE.find((p) => tags.some((t) => t === p || t === `${p}:latest`)) ?? tags[0] ?? null;
|
|
61
106
|
}
|
|
62
107
|
/** Anthropic key wins (quality); otherwise local Ollama (privacy, zero setup); otherwise guide the user. */
|
|
63
108
|
export async function chooseExtractor(purpose = "extract") {
|