dsh-deeptutor 0.1.6 → 0.1.8
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/README.md +181 -177
- package/README.zh.md +133 -131
- package/cordis.patch.yml +17 -17
- package/lib/config.js +1 -0
- package/lib/http-api.js +18 -1
- package/lib/index.js +4 -0
- package/lib/turn.d.ts +10 -2
- package/lib/turn.js +27 -7
- package/package.json +74 -62
- package/scripts/install-profile.mjs +233 -233
- package/scripts/md-to-html.js +381 -353
- package/scripts/sync-skills.mjs +46 -0
- package/skills/deeptutor/README.md +129 -106
- package/skills/deeptutor/SKILL.md +183 -182
- package/skills/html-doc/SKILL.md +91 -91
- package/skills/html-doc/assets/template.html +420 -420
- package/skills/html-doc/scripts/md-to-html.js +381 -353
- package/src/cli-exec.ts +251 -251
- package/src/config.ts +71 -70
- package/src/html-render.ts +53 -53
- package/src/http-api.ts +98 -83
- package/src/index.ts +468 -464
- package/src/turn.ts +197 -174
package/src/turn.ts
CHANGED
|
@@ -1,174 +1,197 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DeepTutor bridge: one learning "turn" — running a capability over WebSocket (HTTP mode)
|
|
3
|
-
* or the CLI (fallback), and shared JSONL/WS event parsing + result formatting.
|
|
4
|
-
*/
|
|
5
|
-
import { API_BASE, truncate } from "./config.ts";
|
|
6
|
-
import { runCli } from "./cli-exec.ts";
|
|
7
|
-
|
|
8
|
-
export interface TurnResult {
|
|
9
|
-
answer: string;
|
|
10
|
-
errors: string[];
|
|
11
|
-
toolCalls: string[];
|
|
12
|
-
sessionId?: string;
|
|
13
|
-
title?: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/** Fields both wsRunTurn and cliRunTurn read off the deeptutor_run tool params. */
|
|
17
|
-
export interface RunParams {
|
|
18
|
-
prompt: string;
|
|
19
|
-
capability: string;
|
|
20
|
-
kbs?: string[];
|
|
21
|
-
tools?: string[];
|
|
22
|
-
session_id?: string;
|
|
23
|
-
language?: string;
|
|
24
|
-
config?: Record<string, unknown>;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** Fold one event (WS or CLI JSONL) into the running turn state. Exported for unit tests. */
|
|
28
|
-
export function collectEvent(state: TurnResult, e: any) {
|
|
29
|
-
if (e.type === "content" && typeof e.content === "string") state.answer += e.content;
|
|
30
|
-
else if (e.type === "result" && typeof e.metadata?.response === "string")
|
|
31
|
-
state.answer = e.metadata.response;
|
|
32
|
-
else if (e.type === "error")
|
|
33
|
-
state.errors.push(typeof e.content === "string" ? e.content : JSON.stringify(e.metadata ?? ""));
|
|
34
|
-
else if (e.type === "tool_call")
|
|
35
|
-
state.toolCalls.push(
|
|
36
|
-
typeof e.content === "string" ? e.content : String(e.metadata?.name ?? ""),
|
|
37
|
-
);
|
|
38
|
-
else if (e.type === "session_meta" && e.metadata?.title) state.title = e.metadata.title;
|
|
39
|
-
if (!state.sessionId && typeof e.session_id === "string") state.sessionId = e.session_id;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** Execute one turn over WebSocket (streaming). */
|
|
43
|
-
export function wsRunTurn(
|
|
44
|
-
params: RunParams,
|
|
45
|
-
timeoutMs: number,
|
|
46
|
-
signal?: AbortSignal,
|
|
47
|
-
): Promise<TurnResult> {
|
|
48
|
-
return new Promise((resolve, reject) => {
|
|
49
|
-
if (signal?.aborted) return reject(new Error("Aborted"));
|
|
50
|
-
const WSImpl: any = (globalThis as any).WebSocket;
|
|
51
|
-
if (!WSImpl) return reject(new Error("WebSocket is not available in this environment"));
|
|
52
|
-
const ws = new WSImpl(`${API_BASE.replace(/^http/, "ws")}/api/v1/ws`);
|
|
53
|
-
let settled = false;
|
|
54
|
-
const state: TurnResult = { answer: "", errors: [], toolCalls: [] };
|
|
55
|
-
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
56
|
-
const finish = (fn: () => void) => {
|
|
57
|
-
if (settled) return;
|
|
58
|
-
settled = true;
|
|
59
|
-
if (timer) clearTimeout(timer);
|
|
60
|
-
signal?.removeEventListener("abort", onAbort);
|
|
61
|
-
fn();
|
|
62
|
-
};
|
|
63
|
-
timer = setTimeout(() => {
|
|
64
|
-
finish(() => {
|
|
65
|
-
try {
|
|
66
|
-
ws.close();
|
|
67
|
-
} catch {}
|
|
68
|
-
reject(new Error(`WebSocket timeout (${Math.round(timeoutMs / 1000)}s)`));
|
|
69
|
-
});
|
|
70
|
-
}, timeoutMs);
|
|
71
|
-
const onAbort = () => {
|
|
72
|
-
finish(() => {
|
|
73
|
-
try {
|
|
74
|
-
ws.close();
|
|
75
|
-
} catch {}
|
|
76
|
-
reject(new Error("Aborted"));
|
|
77
|
-
});
|
|
78
|
-
};
|
|
79
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
80
|
-
ws.onopen = () => {
|
|
81
|
-
ws.send(
|
|
82
|
-
JSON.stringify({
|
|
83
|
-
type: "start_turn",
|
|
84
|
-
content: params.prompt,
|
|
85
|
-
capability: params.capability,
|
|
86
|
-
session_id: params.session_id ?? null,
|
|
87
|
-
tools: params.tools ?? [],
|
|
88
|
-
knowledge_bases: params.kbs ?? [],
|
|
89
|
-
language: params.language ?? "en",
|
|
90
|
-
config: params.config ?? {},
|
|
91
|
-
notebook_references: [],
|
|
92
|
-
history_references: [],
|
|
93
|
-
attachments: [],
|
|
94
|
-
skills: [],
|
|
95
|
-
}),
|
|
96
|
-
);
|
|
97
|
-
};
|
|
98
|
-
ws.onmessage = (ev: any) => {
|
|
99
|
-
let e: any;
|
|
100
|
-
try {
|
|
101
|
-
e = JSON.parse(ev.data);
|
|
102
|
-
} catch {
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
collectEvent(state, e);
|
|
106
|
-
if (e.type === "done") {
|
|
107
|
-
finish(() => {
|
|
108
|
-
try {
|
|
109
|
-
ws.close();
|
|
110
|
-
} catch {}
|
|
111
|
-
resolve(state);
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
};
|
|
115
|
-
ws.onerror = () => {
|
|
116
|
-
finish(() => {
|
|
117
|
-
try {
|
|
118
|
-
ws.close();
|
|
119
|
-
} catch {}
|
|
120
|
-
reject(new Error("WebSocket connection error (server not running or tunnel down)"));
|
|
121
|
-
});
|
|
122
|
-
};
|
|
123
|
-
ws.onclose = () => {
|
|
124
|
-
finish(() => {
|
|
125
|
-
reject(new Error("WebSocket closed unexpectedly"));
|
|
126
|
-
});
|
|
127
|
-
};
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
for (const
|
|
140
|
-
|
|
141
|
-
if (params.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
);
|
|
168
|
-
|
|
169
|
-
if (
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
1
|
+
/**
|
|
2
|
+
* DeepTutor bridge: one learning "turn" — running a capability over WebSocket (HTTP mode)
|
|
3
|
+
* or the CLI (fallback), and shared JSONL/WS event parsing + result formatting.
|
|
4
|
+
*/
|
|
5
|
+
import { API_BASE, truncate } from "./config.ts";
|
|
6
|
+
import { runCli } from "./cli-exec.ts";
|
|
7
|
+
|
|
8
|
+
export interface TurnResult {
|
|
9
|
+
answer: string;
|
|
10
|
+
errors: string[];
|
|
11
|
+
toolCalls: string[];
|
|
12
|
+
sessionId?: string;
|
|
13
|
+
title?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Fields both wsRunTurn and cliRunTurn read off the deeptutor_run tool params. */
|
|
17
|
+
export interface RunParams {
|
|
18
|
+
prompt: string;
|
|
19
|
+
capability: string;
|
|
20
|
+
kbs?: string[];
|
|
21
|
+
tools?: string[];
|
|
22
|
+
session_id?: string;
|
|
23
|
+
language?: string;
|
|
24
|
+
config?: Record<string, unknown>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Fold one event (WS or CLI JSONL) into the running turn state. Exported for unit tests. */
|
|
28
|
+
export function collectEvent(state: TurnResult, e: any) {
|
|
29
|
+
if (e.type === "content" && typeof e.content === "string") state.answer += e.content;
|
|
30
|
+
else if (e.type === "result" && typeof e.metadata?.response === "string")
|
|
31
|
+
state.answer = e.metadata.response;
|
|
32
|
+
else if (e.type === "error")
|
|
33
|
+
state.errors.push(typeof e.content === "string" ? e.content : JSON.stringify(e.metadata ?? ""));
|
|
34
|
+
else if (e.type === "tool_call")
|
|
35
|
+
state.toolCalls.push(
|
|
36
|
+
typeof e.content === "string" ? e.content : String(e.metadata?.name ?? ""),
|
|
37
|
+
);
|
|
38
|
+
else if (e.type === "session_meta" && e.metadata?.title) state.title = e.metadata.title;
|
|
39
|
+
if (!state.sessionId && typeof e.session_id === "string") state.sessionId = e.session_id;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Execute one turn over WebSocket (streaming). */
|
|
43
|
+
export function wsRunTurn(
|
|
44
|
+
params: RunParams,
|
|
45
|
+
timeoutMs: number,
|
|
46
|
+
signal?: AbortSignal,
|
|
47
|
+
): Promise<TurnResult> {
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
if (signal?.aborted) return reject(new Error("Aborted"));
|
|
50
|
+
const WSImpl: any = (globalThis as any).WebSocket;
|
|
51
|
+
if (!WSImpl) return reject(new Error("WebSocket is not available in this environment"));
|
|
52
|
+
const ws = new WSImpl(`${API_BASE.replace(/^http/, "ws")}/api/v1/ws`);
|
|
53
|
+
let settled = false;
|
|
54
|
+
const state: TurnResult = { answer: "", errors: [], toolCalls: [] };
|
|
55
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
56
|
+
const finish = (fn: () => void) => {
|
|
57
|
+
if (settled) return;
|
|
58
|
+
settled = true;
|
|
59
|
+
if (timer) clearTimeout(timer);
|
|
60
|
+
signal?.removeEventListener("abort", onAbort);
|
|
61
|
+
fn();
|
|
62
|
+
};
|
|
63
|
+
timer = setTimeout(() => {
|
|
64
|
+
finish(() => {
|
|
65
|
+
try {
|
|
66
|
+
ws.close();
|
|
67
|
+
} catch {}
|
|
68
|
+
reject(new Error(`WebSocket timeout (${Math.round(timeoutMs / 1000)}s)`));
|
|
69
|
+
});
|
|
70
|
+
}, timeoutMs);
|
|
71
|
+
const onAbort = () => {
|
|
72
|
+
finish(() => {
|
|
73
|
+
try {
|
|
74
|
+
ws.close();
|
|
75
|
+
} catch {}
|
|
76
|
+
reject(new Error("Aborted"));
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
80
|
+
ws.onopen = () => {
|
|
81
|
+
ws.send(
|
|
82
|
+
JSON.stringify({
|
|
83
|
+
type: "start_turn",
|
|
84
|
+
content: params.prompt,
|
|
85
|
+
capability: params.capability,
|
|
86
|
+
session_id: params.session_id ?? null,
|
|
87
|
+
tools: params.tools ?? [],
|
|
88
|
+
knowledge_bases: params.kbs ?? [],
|
|
89
|
+
language: params.language ?? "en",
|
|
90
|
+
config: params.config ?? {},
|
|
91
|
+
notebook_references: [],
|
|
92
|
+
history_references: [],
|
|
93
|
+
attachments: [],
|
|
94
|
+
skills: [],
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
97
|
+
};
|
|
98
|
+
ws.onmessage = (ev: any) => {
|
|
99
|
+
let e: any;
|
|
100
|
+
try {
|
|
101
|
+
e = JSON.parse(ev.data);
|
|
102
|
+
} catch {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
collectEvent(state, e);
|
|
106
|
+
if (e.type === "done") {
|
|
107
|
+
finish(() => {
|
|
108
|
+
try {
|
|
109
|
+
ws.close();
|
|
110
|
+
} catch {}
|
|
111
|
+
resolve(state);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
ws.onerror = () => {
|
|
116
|
+
finish(() => {
|
|
117
|
+
try {
|
|
118
|
+
ws.close();
|
|
119
|
+
} catch {}
|
|
120
|
+
reject(new Error("WebSocket connection error (server not running or tunnel down)"));
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
ws.onclose = () => {
|
|
124
|
+
finish(() => {
|
|
125
|
+
reject(new Error("WebSocket closed unexpectedly"));
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Build the `deeptutor run` argv for a capability turn (pure, unit-tested).
|
|
133
|
+
* Scalar config values go as repeatable --config k=v; object/array values are
|
|
134
|
+
* collected into --config-json (the CLI's documented form for complex config)
|
|
135
|
+
* instead of being stringified into a k=<json> value that no parser expects.
|
|
136
|
+
*/
|
|
137
|
+
export function buildRunArgs(params: RunParams): string[] {
|
|
138
|
+
const argv = ["run", params.capability, params.prompt, "--format", "json"];
|
|
139
|
+
for (const kb of params.kbs ?? []) argv.push("--kb", kb);
|
|
140
|
+
for (const t of params.tools ?? []) argv.push("--tool", t);
|
|
141
|
+
if (params.session_id) argv.push("--session", params.session_id);
|
|
142
|
+
if (params.language) argv.push("--language", params.language);
|
|
143
|
+
const complexConfig: Record<string, unknown> = {};
|
|
144
|
+
for (const [k, v] of Object.entries(params.config ?? {})) {
|
|
145
|
+
if (typeof v === "object" && v !== null) complexConfig[k] = v;
|
|
146
|
+
else argv.push("--config", `${k}=${v}`);
|
|
147
|
+
}
|
|
148
|
+
if (Object.keys(complexConfig).length > 0) {
|
|
149
|
+
argv.push("--config-json", JSON.stringify(complexConfig));
|
|
150
|
+
}
|
|
151
|
+
return argv;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Execute one turn via the CLI (local or SSH). `run` is injectable for tests. */
|
|
155
|
+
export async function cliRunTurn(
|
|
156
|
+
params: RunParams,
|
|
157
|
+
timeoutMs: number,
|
|
158
|
+
signal?: AbortSignal,
|
|
159
|
+
run: typeof runCli = runCli,
|
|
160
|
+
): Promise<TurnResult> {
|
|
161
|
+
const { stdout, stderr, code, killed } = await run(buildRunArgs(params), { signal, timeoutMs });
|
|
162
|
+
if (killed) {
|
|
163
|
+
// A kill can come from the user cancelling (signal) or from the timeout;
|
|
164
|
+
// report the real reason instead of always blaming the timeout.
|
|
165
|
+
throw new Error(
|
|
166
|
+
signal?.aborted ? "Aborted" : `CLI execution timed out (${Math.round(timeoutMs / 1000)}s)`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
if (code !== 0) throw new Error(`CLI exit=${code}: ${stderr || "(no stderr)"}`);
|
|
170
|
+
const state: TurnResult = { answer: "", errors: [], toolCalls: [] };
|
|
171
|
+
for (const line of stdout.split("\n")) {
|
|
172
|
+
if (!line.trim()) continue;
|
|
173
|
+
let e: any;
|
|
174
|
+
try {
|
|
175
|
+
e = JSON.parse(line);
|
|
176
|
+
} catch {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
collectEvent(state, e);
|
|
180
|
+
}
|
|
181
|
+
return state;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export const fmtTurnResult = (capability: string, r: TurnResult, maxChars: number): string => {
|
|
185
|
+
const parts: string[] = [];
|
|
186
|
+
parts.push(`[deeptutor ${capability} done]`);
|
|
187
|
+
if (r.sessionId)
|
|
188
|
+
parts.push(
|
|
189
|
+
`- session_id: ${r.sessionId} (pass session_id in later turns to continue this context)`,
|
|
190
|
+
);
|
|
191
|
+
if (r.title) parts.push(`- session title: ${r.title}`);
|
|
192
|
+
if (r.toolCalls.length) parts.push(`- tools used: ${[...new Set(r.toolCalls)].join(", ")}`);
|
|
193
|
+
if (r.errors.length)
|
|
194
|
+
parts.push(`- note (some steps failed): ${r.errors.slice(0, 3).join(" | ")}`);
|
|
195
|
+
parts.push(`\n===== Answer =====\n${truncate(r.answer || "(no answer)", maxChars)}`);
|
|
196
|
+
return parts.join("\n");
|
|
197
|
+
};
|