@kweaver-ai/kweaver-sdk 0.4.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/bin/kweaver.js +9 -0
- package/dist/api/agent-chat.d.ts +69 -0
- package/dist/api/agent-chat.js +379 -0
- package/dist/api/agent-list.d.ts +12 -0
- package/dist/api/agent-list.js +33 -0
- package/dist/api/context-loader.d.ts +115 -0
- package/dist/api/context-loader.js +259 -0
- package/dist/api/conversations.d.ts +24 -0
- package/dist/api/conversations.js +64 -0
- package/dist/api/knowledge-networks.d.ts +57 -0
- package/dist/api/knowledge-networks.js +158 -0
- package/dist/api/ontology-query.d.ts +75 -0
- package/dist/api/ontology-query.js +238 -0
- package/dist/api/semantic-search.d.ts +12 -0
- package/dist/api/semantic-search.js +34 -0
- package/dist/auth/oauth.d.ts +75 -0
- package/dist/auth/oauth.js +417 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +79 -0
- package/dist/client.d.ts +95 -0
- package/dist/client.js +104 -0
- package/dist/commands/agent-chat.d.ts +12 -0
- package/dist/commands/agent-chat.js +193 -0
- package/dist/commands/agent.d.ts +28 -0
- package/dist/commands/agent.js +431 -0
- package/dist/commands/auth.d.ts +9 -0
- package/dist/commands/auth.js +201 -0
- package/dist/commands/bkn.d.ts +70 -0
- package/dist/commands/bkn.js +1371 -0
- package/dist/commands/call.d.ts +14 -0
- package/dist/commands/call.js +151 -0
- package/dist/commands/context-loader.d.ts +1 -0
- package/dist/commands/context-loader.js +383 -0
- package/dist/commands/token.d.ts +2 -0
- package/dist/commands/token.js +24 -0
- package/dist/config/store.d.ts +77 -0
- package/dist/config/store.js +380 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.js +44 -0
- package/dist/kweaver.d.ts +146 -0
- package/dist/kweaver.js +184 -0
- package/dist/resources/agents.d.ts +37 -0
- package/dist/resources/agents.js +60 -0
- package/dist/resources/bkn.d.ts +45 -0
- package/dist/resources/bkn.js +86 -0
- package/dist/resources/context-loader.d.ts +15 -0
- package/dist/resources/context-loader.js +32 -0
- package/dist/resources/conversations.d.ts +11 -0
- package/dist/resources/conversations.js +17 -0
- package/dist/resources/knowledge-networks.d.ts +65 -0
- package/dist/resources/knowledge-networks.js +167 -0
- package/dist/ui/ChatApp.d.ts +16 -0
- package/dist/ui/ChatApp.js +248 -0
- package/dist/ui/MarkdownBlock.d.ts +5 -0
- package/dist/ui/MarkdownBlock.js +137 -0
- package/dist/ui/display-text.d.ts +1 -0
- package/dist/ui/display-text.js +1 -0
- package/dist/utils/browser.d.ts +1 -0
- package/dist/utils/browser.js +20 -0
- package/dist/utils/display-text.d.ts +3 -0
- package/dist/utils/display-text.js +46 -0
- package/dist/utils/http.d.ts +17 -0
- package/dist/utils/http.js +72 -0
- package/package.json +62 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { listKnowledgeNetworks, getKnowledgeNetwork, createKnowledgeNetwork, updateKnowledgeNetwork, deleteKnowledgeNetwork, listObjectTypes, listRelationTypes, listActionTypes, } from "../api/knowledge-networks.js";
|
|
2
|
+
import { fetchTextOrThrow } from "../utils/http.js";
|
|
3
|
+
function is404(err) {
|
|
4
|
+
return !!(err && typeof err === "object" && "status" in err && err.status === 404);
|
|
5
|
+
}
|
|
6
|
+
export class KnowledgeNetworksResource {
|
|
7
|
+
ctx;
|
|
8
|
+
constructor(ctx) {
|
|
9
|
+
this.ctx = ctx;
|
|
10
|
+
}
|
|
11
|
+
async list(opts = {}) {
|
|
12
|
+
const raw = await listKnowledgeNetworks({ ...this.ctx.base(), ...opts });
|
|
13
|
+
const parsed = JSON.parse(raw);
|
|
14
|
+
const data = parsed && typeof parsed === "object" && "data" in parsed
|
|
15
|
+
? parsed.data
|
|
16
|
+
: parsed;
|
|
17
|
+
return Array.isArray(data) ? data : [];
|
|
18
|
+
}
|
|
19
|
+
async get(knId, opts = {}) {
|
|
20
|
+
const raw = await getKnowledgeNetwork({ ...this.ctx.base(), knId, ...opts });
|
|
21
|
+
return JSON.parse(raw);
|
|
22
|
+
}
|
|
23
|
+
async create(opts) {
|
|
24
|
+
const raw = await createKnowledgeNetwork({ ...this.ctx.base(), body: JSON.stringify(opts) });
|
|
25
|
+
return JSON.parse(raw);
|
|
26
|
+
}
|
|
27
|
+
async update(knId, opts) {
|
|
28
|
+
const raw = await updateKnowledgeNetwork({ ...this.ctx.base(), knId, body: JSON.stringify(opts) });
|
|
29
|
+
return JSON.parse(raw);
|
|
30
|
+
}
|
|
31
|
+
async delete(knId) {
|
|
32
|
+
await deleteKnowledgeNetwork({ ...this.ctx.base(), knId });
|
|
33
|
+
}
|
|
34
|
+
async listObjectTypes(knId, opts = {}) {
|
|
35
|
+
const raw = await listObjectTypes({ ...this.ctx.base(), knId, ...opts });
|
|
36
|
+
const parsed = JSON.parse(raw);
|
|
37
|
+
const items = Array.isArray(parsed)
|
|
38
|
+
? parsed
|
|
39
|
+
: parsed && typeof parsed === "object" && "entries" in parsed
|
|
40
|
+
? parsed.entries
|
|
41
|
+
: [];
|
|
42
|
+
return items;
|
|
43
|
+
}
|
|
44
|
+
async listRelationTypes(knId, opts = {}) {
|
|
45
|
+
const raw = await listRelationTypes({ ...this.ctx.base(), knId, ...opts });
|
|
46
|
+
const parsed = JSON.parse(raw);
|
|
47
|
+
const items = Array.isArray(parsed)
|
|
48
|
+
? parsed
|
|
49
|
+
: parsed && typeof parsed === "object" && "entries" in parsed
|
|
50
|
+
? parsed.entries
|
|
51
|
+
: [];
|
|
52
|
+
return items;
|
|
53
|
+
}
|
|
54
|
+
async listActionTypes(knId, opts = {}) {
|
|
55
|
+
const raw = await listActionTypes({ ...this.ctx.base(), knId, ...opts });
|
|
56
|
+
const parsed = JSON.parse(raw);
|
|
57
|
+
const items = Array.isArray(parsed)
|
|
58
|
+
? parsed
|
|
59
|
+
: parsed && typeof parsed === "object" && "entries" in parsed
|
|
60
|
+
? parsed.entries
|
|
61
|
+
: [];
|
|
62
|
+
return items;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Trigger a full build (index rebuild) of a BKN.
|
|
66
|
+
* Call this after adding datasources or modifying object/relation types.
|
|
67
|
+
*
|
|
68
|
+
* @param bknId BKN ID to build.
|
|
69
|
+
* @returns A promise that resolves immediately after the build is triggered.
|
|
70
|
+
* Use `buildAndWait` to block until completion.
|
|
71
|
+
*/
|
|
72
|
+
async build(bknId) {
|
|
73
|
+
const { baseUrl, accessToken, businessDomain } = this.ctx.base();
|
|
74
|
+
const headers = {
|
|
75
|
+
"content-type": "application/json",
|
|
76
|
+
authorization: `Bearer ${accessToken}`,
|
|
77
|
+
token: accessToken,
|
|
78
|
+
"x-business-domain": businessDomain,
|
|
79
|
+
};
|
|
80
|
+
try {
|
|
81
|
+
await fetchTextOrThrow(`${baseUrl}/api/agent-retrieval/in/v1/kn/full_build_ontology`, { method: "POST", headers, body: JSON.stringify({ kn_id: bknId }) });
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
if (!is404(err))
|
|
85
|
+
throw err;
|
|
86
|
+
// Fallback: call ontology-manager jobs endpoint directly
|
|
87
|
+
try {
|
|
88
|
+
await fetchTextOrThrow(`${baseUrl}/api/ontology-manager/in/v1/knowledge-networks/${encodeURIComponent(bknId)}/jobs`, {
|
|
89
|
+
method: "POST",
|
|
90
|
+
headers,
|
|
91
|
+
body: JSON.stringify({ name: `sdk_build_${bknId.slice(0, 8)}`, job_type: "full" }),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
catch (err2) {
|
|
95
|
+
if (is404(err2)) {
|
|
96
|
+
throw new Error(`No build endpoint available for BKN ${bknId}. ` +
|
|
97
|
+
`Both agent-retrieval and ontology-manager returned 404. ` +
|
|
98
|
+
`This deployment may not support index rebuilds.`);
|
|
99
|
+
}
|
|
100
|
+
throw err2;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/** Poll build status for a BKN. */
|
|
105
|
+
async buildStatus(bknId) {
|
|
106
|
+
const { baseUrl, accessToken, businessDomain } = this.ctx.base();
|
|
107
|
+
const headers = {
|
|
108
|
+
authorization: `Bearer ${accessToken}`,
|
|
109
|
+
token: accessToken,
|
|
110
|
+
"x-business-domain": businessDomain,
|
|
111
|
+
};
|
|
112
|
+
try {
|
|
113
|
+
const { body } = await fetchTextOrThrow(`${baseUrl}/api/agent-retrieval/in/v1/kn/full_ontology_building_status?kn_id=${encodeURIComponent(bknId)}`, { headers });
|
|
114
|
+
const data = JSON.parse(body);
|
|
115
|
+
return { state: data.state ?? "running", state_detail: data.state_detail };
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
if (!is404(err))
|
|
119
|
+
throw err;
|
|
120
|
+
// Fallback: check ontology-manager jobs for latest status
|
|
121
|
+
try {
|
|
122
|
+
const { body } = await fetchTextOrThrow(`${baseUrl}/api/ontology-manager/in/v1/knowledge-networks/${encodeURIComponent(bknId)}/jobs?limit=1&direction=desc`, { headers });
|
|
123
|
+
const data = JSON.parse(body);
|
|
124
|
+
const jobs = Array.isArray(data)
|
|
125
|
+
? data
|
|
126
|
+
: data && typeof data === "object" && "entries" in data
|
|
127
|
+
? (data.entries ?? [])
|
|
128
|
+
: data && typeof data === "object" && "data" in data
|
|
129
|
+
? (data.data ?? [])
|
|
130
|
+
: [];
|
|
131
|
+
if (jobs.length > 0) {
|
|
132
|
+
return { state: jobs[0].state ?? "running" };
|
|
133
|
+
}
|
|
134
|
+
return { state: "completed" };
|
|
135
|
+
}
|
|
136
|
+
catch (err2) {
|
|
137
|
+
if (is404(err2))
|
|
138
|
+
return { state: "completed" };
|
|
139
|
+
throw err2;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Trigger a full BKN build and wait for it to complete.
|
|
145
|
+
*
|
|
146
|
+
* @param bknId BKN ID to build.
|
|
147
|
+
* @param timeout Max wait time in milliseconds (default 300_000 = 5 min).
|
|
148
|
+
* @param interval Poll interval in milliseconds (default 2_000).
|
|
149
|
+
* @throws Error if the build fails or times out.
|
|
150
|
+
*/
|
|
151
|
+
async buildAndWait(bknId, { timeout = 300_000, interval = 2_000 } = {}) {
|
|
152
|
+
await this.build(bknId);
|
|
153
|
+
const deadline = Date.now() + timeout;
|
|
154
|
+
while (true) {
|
|
155
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
156
|
+
const status = await this.buildStatus(bknId);
|
|
157
|
+
if (status.state === "completed")
|
|
158
|
+
return status;
|
|
159
|
+
if (status.state === "failed") {
|
|
160
|
+
throw new Error(`BKN build failed for ${bknId}: ${status.state_detail ?? "no detail"}`);
|
|
161
|
+
}
|
|
162
|
+
if (Date.now() > deadline) {
|
|
163
|
+
throw new Error(`BKN build timed out after ${timeout}ms for ${bknId}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export interface TokenPayload {
|
|
3
|
+
baseUrl: string;
|
|
4
|
+
accessToken: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ChatAppProps {
|
|
7
|
+
getToken: () => Promise<TokenPayload>;
|
|
8
|
+
agentId: string;
|
|
9
|
+
agentKey: string;
|
|
10
|
+
agentVersion: string;
|
|
11
|
+
businessDomain: string;
|
|
12
|
+
verbose: boolean;
|
|
13
|
+
initialConversationId?: string;
|
|
14
|
+
stream?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare function ChatApp(props: ChatAppProps): React.JSX.Element;
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useRef, useState } from "react";
|
|
3
|
+
import { Box, Static, Text, useApp, useInput } from "ink";
|
|
4
|
+
import TextInput from "ink-text-input";
|
|
5
|
+
import Spinner from "ink-spinner";
|
|
6
|
+
import { sendChatRequest, sendChatRequestStream } from "../api/agent-chat.js";
|
|
7
|
+
import { formatHttpError } from "../auth/oauth.js";
|
|
8
|
+
import { normalizeDisplayText } from "./display-text.js";
|
|
9
|
+
import { MarkdownBlock } from "./MarkdownBlock.js";
|
|
10
|
+
const MIDDLE_ANSWER_MAX_LINES = 5;
|
|
11
|
+
function formatProgressValue(value) {
|
|
12
|
+
if (typeof value === "string")
|
|
13
|
+
return normalizeDisplayText(value);
|
|
14
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
15
|
+
return String(value);
|
|
16
|
+
if (value === null || value === undefined)
|
|
17
|
+
return "";
|
|
18
|
+
try {
|
|
19
|
+
return normalizeDisplayText(JSON.stringify(value));
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return String(value);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function getLinesForItem(item, idx) {
|
|
26
|
+
const name = item.agent_name ?? item.skill_info?.name ?? `Step ${idx + 1}`;
|
|
27
|
+
const status = item.status ?? "";
|
|
28
|
+
const lines = [`${name}${status ? ` — ${status}` : ""}`];
|
|
29
|
+
const isTool = item.skill_info?.type === "TOOL";
|
|
30
|
+
if (isTool) {
|
|
31
|
+
const toolName = item.skill_info?.name ?? item.agent_name;
|
|
32
|
+
if (toolName) {
|
|
33
|
+
lines.push(`tool: ${normalizeDisplayText(toolName)}`);
|
|
34
|
+
}
|
|
35
|
+
for (const arg of item.skill_info?.args ?? []) {
|
|
36
|
+
const argName = arg.name ?? "arg";
|
|
37
|
+
const argValue = formatProgressValue(arg.value);
|
|
38
|
+
lines.push(`${argName}: ${argValue}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
let answerStr;
|
|
42
|
+
if (typeof item.answer === "string") {
|
|
43
|
+
answerStr = item.answer;
|
|
44
|
+
}
|
|
45
|
+
else if (item.answer && typeof item.answer === "object") {
|
|
46
|
+
const o = item.answer;
|
|
47
|
+
const d = typeof o.description === "string" ? o.description : "";
|
|
48
|
+
const c = typeof o.code === "string" ? o.code : "";
|
|
49
|
+
const s = typeof o.solution === "string" ? o.solution : "";
|
|
50
|
+
const link = typeof o.link === "string" ? o.link : "";
|
|
51
|
+
answerStr = [d, c ? `code: ${c}` : "", s ? `solution: ${s}` : "", link ? `link: ${link}` : ""]
|
|
52
|
+
.filter(Boolean)
|
|
53
|
+
.join("\n");
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
answerStr = item.result ?? "";
|
|
57
|
+
}
|
|
58
|
+
if (answerStr)
|
|
59
|
+
lines.push(...normalizeDisplayText(answerStr).split("\n"));
|
|
60
|
+
return lines;
|
|
61
|
+
}
|
|
62
|
+
function getVisibleProgress(items) {
|
|
63
|
+
const itemLines = items.map((item, idx) => ({ item, lines: getLinesForItem(item, idx) }));
|
|
64
|
+
let remaining = MIDDLE_ANSWER_MAX_LINES;
|
|
65
|
+
const visible = [];
|
|
66
|
+
for (const { item, lines } of itemLines) {
|
|
67
|
+
if (remaining <= 0)
|
|
68
|
+
break;
|
|
69
|
+
const take = Math.min(remaining, lines.length);
|
|
70
|
+
visible.push({ item, lines: lines.slice(0, take) });
|
|
71
|
+
remaining -= take;
|
|
72
|
+
}
|
|
73
|
+
const totalLines = itemLines.reduce((s, { lines }) => s + lines.length, 0);
|
|
74
|
+
return { visible, hiddenLineCount: Math.max(0, totalLines - MIDDLE_ANSWER_MAX_LINES) };
|
|
75
|
+
}
|
|
76
|
+
function renderStaticItem(item, index) {
|
|
77
|
+
if (item.kind === "header") {
|
|
78
|
+
return (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: "Chat \u2014 type exit, quit or q to leave" }) }, index));
|
|
79
|
+
}
|
|
80
|
+
if (item.kind === "msg") {
|
|
81
|
+
const { msg } = item;
|
|
82
|
+
if (msg.role === "user") {
|
|
83
|
+
return (_jsx(Box, { flexDirection: "column", marginY: 1, width: "100%", paddingRight: 1, children: _jsx(Box, { width: "100%", paddingX: 1, paddingY: 1, backgroundColor: "#2C2C2C", children: _jsx(Text, { color: "#9B9B9B", children: normalizeDisplayText(msg.content) }) }) }, index));
|
|
84
|
+
}
|
|
85
|
+
return (_jsx(Box, { flexDirection: "column", marginY: 1, children: _jsxs(_Fragment, { children: [msg.hadProgress ? (_jsx(Text, { dimColor: true, children: "/plan \u751F\u6210\u4FEE\u6539\u8BA1\u5212" })) : null, _jsx(MarkdownBlock, { content: normalizeDisplayText(msg.content) })] }) }, index));
|
|
86
|
+
}
|
|
87
|
+
return _jsx(Box, {}, index);
|
|
88
|
+
}
|
|
89
|
+
const DynamicContent = React.memo(function DynamicContent(props) {
|
|
90
|
+
const { toolProgress, streamingContent, status, errorMessage } = props;
|
|
91
|
+
return (_jsxs(Box, { flexDirection: "column", children: [toolProgress.length > 0 ? (_jsxs(Box, { marginY: 1, borderStyle: "single", borderColor: "gray", paddingX: 1, paddingY: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, dimColor: true, children: "Tool output" }), (() => {
|
|
92
|
+
const { visible, hiddenLineCount } = getVisibleProgress(toolProgress);
|
|
93
|
+
return (_jsxs(_Fragment, { children: [visible.map(({ item, lines }, i) => {
|
|
94
|
+
const isTool = item.skill_info?.type === "TOOL";
|
|
95
|
+
const content = (_jsx(_Fragment, { children: lines.map((line, j) => (_jsx(Text, { dimColor: true, children: line }, j))) }));
|
|
96
|
+
if (isTool) {
|
|
97
|
+
return (_jsx(Box, { marginY: 0, borderStyle: "single", borderColor: "green", paddingX: 1, paddingY: 0, flexDirection: "column", children: content }, i));
|
|
98
|
+
}
|
|
99
|
+
return (_jsx(Box, { flexDirection: "column", children: content }, i));
|
|
100
|
+
}), hiddenLineCount > 0 ? (_jsxs(Text, { dimColor: true, italic: true, children: ["(\u5DF2\u9690\u85CF ", hiddenLineCount, " \u884C)"] })) : null] }));
|
|
101
|
+
})()] })) : null, toolProgress.length > 0 && (streamingContent || status === "loading") ? (_jsx(Text, { dimColor: true, children: "/plan \u751F\u6210\u4FEE\u6539\u8BA1\u5212" })) : null, streamingContent ? (_jsx(Box, { flexDirection: "column", marginY: 1, children: _jsx(MarkdownBlock, { content: normalizeDisplayText(streamingContent) }) })) : null, errorMessage ? (_jsx(Box, { marginY: 1, children: _jsx(Text, { color: "red", children: errorMessage }) })) : null] }));
|
|
102
|
+
});
|
|
103
|
+
export function ChatApp(props) {
|
|
104
|
+
const { getToken, agentId, agentKey, agentVersion, businessDomain, verbose, initialConversationId, stream: useStream = true, } = props;
|
|
105
|
+
const [messages, setMessages] = useState([]);
|
|
106
|
+
const [streamingContent, setStreamingContent] = useState("");
|
|
107
|
+
const [toolProgress, setToolProgress] = useState([]);
|
|
108
|
+
const [conversationId, setConversationId] = useState(initialConversationId);
|
|
109
|
+
const [inputValue, setInputValue] = useState("");
|
|
110
|
+
const [status, setStatus] = useState("idle");
|
|
111
|
+
const [errorMessage, setErrorMessage] = useState("");
|
|
112
|
+
const pendingQueriesRef = useRef([]);
|
|
113
|
+
const streamingBufferRef = useRef("");
|
|
114
|
+
const toolProgressBufferRef = useRef([]);
|
|
115
|
+
const streamFlushIntervalRef = useRef(null);
|
|
116
|
+
const STREAM_FLUSH_MS = 550;
|
|
117
|
+
const { exit } = useApp();
|
|
118
|
+
const EXIT_WORDS = ["exit", "quit", "q"];
|
|
119
|
+
const doSendQuery = (query, convId) => {
|
|
120
|
+
setMessages((prev) => [...prev, { role: "user", content: query }]);
|
|
121
|
+
setInputValue("");
|
|
122
|
+
setStreamingContent("");
|
|
123
|
+
setToolProgress([]);
|
|
124
|
+
setErrorMessage("");
|
|
125
|
+
setStatus("loading");
|
|
126
|
+
streamingBufferRef.current = "";
|
|
127
|
+
toolProgressBufferRef.current = [];
|
|
128
|
+
const onDone = (result) => {
|
|
129
|
+
const hadProgress = (result.progress?.length ?? 0) > 0 || toolProgressBufferRef.current.length > 0;
|
|
130
|
+
setStreamingContent("");
|
|
131
|
+
setToolProgress(result.progress !== undefined ? result.progress : toolProgressBufferRef.current);
|
|
132
|
+
setMessages((prev) => [
|
|
133
|
+
...prev,
|
|
134
|
+
{ role: "agent", content: normalizeDisplayText(result.text), hadProgress },
|
|
135
|
+
]);
|
|
136
|
+
if (result.conversationId) {
|
|
137
|
+
setConversationId(result.conversationId);
|
|
138
|
+
}
|
|
139
|
+
setStatus("idle");
|
|
140
|
+
const next = pendingQueriesRef.current.shift();
|
|
141
|
+
if (next !== undefined) {
|
|
142
|
+
doSendQuery(next, result.conversationId);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
const onFail = (error) => {
|
|
146
|
+
setErrorMessage(formatHttpError(error));
|
|
147
|
+
setStatus("error");
|
|
148
|
+
setStreamingContent("");
|
|
149
|
+
setToolProgress([]);
|
|
150
|
+
const next = pendingQueriesRef.current.shift();
|
|
151
|
+
if (next !== undefined) {
|
|
152
|
+
pendingQueriesRef.current.unshift(next);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
if (!useStream) {
|
|
156
|
+
void getToken()
|
|
157
|
+
.then((token) => sendChatRequest({
|
|
158
|
+
baseUrl: token.baseUrl,
|
|
159
|
+
accessToken: token.accessToken,
|
|
160
|
+
agentId,
|
|
161
|
+
agentKey,
|
|
162
|
+
agentVersion,
|
|
163
|
+
query,
|
|
164
|
+
conversationId: convId,
|
|
165
|
+
stream: false,
|
|
166
|
+
verbose,
|
|
167
|
+
businessDomain,
|
|
168
|
+
}))
|
|
169
|
+
.then(onDone)
|
|
170
|
+
.catch(onFail);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (streamFlushIntervalRef.current) {
|
|
174
|
+
clearInterval(streamFlushIntervalRef.current);
|
|
175
|
+
streamFlushIntervalRef.current = null;
|
|
176
|
+
}
|
|
177
|
+
streamFlushIntervalRef.current = setInterval(() => {
|
|
178
|
+
const nextText = streamingBufferRef.current;
|
|
179
|
+
const nextProgress = toolProgressBufferRef.current;
|
|
180
|
+
const normalizedNextText = normalizeDisplayText(nextText);
|
|
181
|
+
setStreamingContent((prev) => (normalizedNextText === prev ? prev : normalizedNextText));
|
|
182
|
+
setToolProgress((prev) => prev.length !== nextProgress.length || prev.some((p, i) => p !== nextProgress[i])
|
|
183
|
+
? nextProgress
|
|
184
|
+
: prev);
|
|
185
|
+
}, STREAM_FLUSH_MS);
|
|
186
|
+
void getToken()
|
|
187
|
+
.then((token) => sendChatRequestStream({
|
|
188
|
+
baseUrl: token.baseUrl,
|
|
189
|
+
accessToken: token.accessToken,
|
|
190
|
+
agentId,
|
|
191
|
+
agentKey,
|
|
192
|
+
agentVersion,
|
|
193
|
+
query,
|
|
194
|
+
conversationId: convId,
|
|
195
|
+
stream: true,
|
|
196
|
+
verbose,
|
|
197
|
+
businessDomain,
|
|
198
|
+
}, {
|
|
199
|
+
onTextDelta: (fullText) => {
|
|
200
|
+
streamingBufferRef.current = normalizeDisplayText(fullText);
|
|
201
|
+
},
|
|
202
|
+
onProgress: (progress) => {
|
|
203
|
+
toolProgressBufferRef.current = progress;
|
|
204
|
+
},
|
|
205
|
+
}))
|
|
206
|
+
.then((result) => {
|
|
207
|
+
if (streamFlushIntervalRef.current) {
|
|
208
|
+
clearInterval(streamFlushIntervalRef.current);
|
|
209
|
+
streamFlushIntervalRef.current = null;
|
|
210
|
+
}
|
|
211
|
+
streamingBufferRef.current = "";
|
|
212
|
+
onDone(result);
|
|
213
|
+
})
|
|
214
|
+
.catch((error) => {
|
|
215
|
+
if (streamFlushIntervalRef.current) {
|
|
216
|
+
clearInterval(streamFlushIntervalRef.current);
|
|
217
|
+
streamFlushIntervalRef.current = null;
|
|
218
|
+
}
|
|
219
|
+
streamingBufferRef.current = "";
|
|
220
|
+
onFail(error);
|
|
221
|
+
});
|
|
222
|
+
};
|
|
223
|
+
useInput((input, key) => {
|
|
224
|
+
if (key.ctrl && input === "\x03") {
|
|
225
|
+
exit();
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
const handleSubmit = (value) => {
|
|
229
|
+
const trimmed = value.trim();
|
|
230
|
+
if (!trimmed)
|
|
231
|
+
return;
|
|
232
|
+
if (EXIT_WORDS.includes(trimmed.toLowerCase())) {
|
|
233
|
+
exit();
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (status === "loading") {
|
|
237
|
+
pendingQueriesRef.current.push(trimmed);
|
|
238
|
+
setInputValue("");
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
doSendQuery(trimmed, conversationId);
|
|
242
|
+
};
|
|
243
|
+
const staticItems = [
|
|
244
|
+
{ kind: "header" },
|
|
245
|
+
...messages.map((msg) => ({ kind: "msg", msg })),
|
|
246
|
+
];
|
|
247
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Static, { items: staticItems, style: { width: "100%" }, children: (item, index) => renderStaticItem(item, index) }), _jsx(DynamicContent, { toolProgress: toolProgress, streamingContent: streamingContent, status: status, errorMessage: errorMessage }), _jsxs(Box, { marginTop: 1, borderStyle: "single", borderColor: "gray", backgroundColor: "black", paddingX: 1, paddingY: 0, children: [_jsx(Text, { color: "gray", children: "> " }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleSubmit, placeholder: "Type a message..." })] }), status === "loading" ? (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "green", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Thinking..." })] })) : null] }));
|
|
248
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment, useMemo } from "react";
|
|
3
|
+
import { Box, Text } from "ink";
|
|
4
|
+
import { marked } from "marked";
|
|
5
|
+
import { normalizeDisplayText } from "./display-text.js";
|
|
6
|
+
function hasTokens(token) {
|
|
7
|
+
return "tokens" in token && Array.isArray(token.tokens);
|
|
8
|
+
}
|
|
9
|
+
function isListToken(token) {
|
|
10
|
+
return token.type === "list" && "items" in token;
|
|
11
|
+
}
|
|
12
|
+
function isTableToken(token) {
|
|
13
|
+
return token.type === "table" && "header" in token && "rows" in token;
|
|
14
|
+
}
|
|
15
|
+
function tokensToPlainText(tokens) {
|
|
16
|
+
if (!tokens || tokens.length === 0)
|
|
17
|
+
return "";
|
|
18
|
+
return tokens
|
|
19
|
+
.map((token) => {
|
|
20
|
+
switch (token.type) {
|
|
21
|
+
case "text":
|
|
22
|
+
return hasTokens(token) ? tokensToPlainText(token.tokens) : token.text;
|
|
23
|
+
case "strong":
|
|
24
|
+
case "em":
|
|
25
|
+
case "del":
|
|
26
|
+
return tokensToPlainText(token.tokens ?? []);
|
|
27
|
+
case "codespan":
|
|
28
|
+
return `\`${token.text}\``;
|
|
29
|
+
case "link":
|
|
30
|
+
return `${tokensToPlainText(token.tokens ?? [])} (${token.href})`;
|
|
31
|
+
case "image":
|
|
32
|
+
return `[image: ${token.text || token.href}]`;
|
|
33
|
+
case "br":
|
|
34
|
+
return "\n";
|
|
35
|
+
case "escape":
|
|
36
|
+
case "html":
|
|
37
|
+
return token.text;
|
|
38
|
+
default:
|
|
39
|
+
if (hasTokens(token)) {
|
|
40
|
+
return tokensToPlainText(token.tokens);
|
|
41
|
+
}
|
|
42
|
+
if ("text" in token && typeof token.text === "string") {
|
|
43
|
+
return token.text;
|
|
44
|
+
}
|
|
45
|
+
return token.raw;
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
.join("");
|
|
49
|
+
}
|
|
50
|
+
function renderInlineToken(token, key) {
|
|
51
|
+
switch (token.type) {
|
|
52
|
+
case "text":
|
|
53
|
+
if (hasTokens(token)) {
|
|
54
|
+
return _jsx(Fragment, { children: renderInlineTokens(token.tokens, `${key}-text`) }, key);
|
|
55
|
+
}
|
|
56
|
+
return _jsx(Fragment, { children: token.text }, key);
|
|
57
|
+
case "strong":
|
|
58
|
+
return (_jsx(Text, { bold: true, children: renderInlineTokens(token.tokens ?? [], `${key}-strong`) }, key));
|
|
59
|
+
case "em":
|
|
60
|
+
return (_jsx(Text, { italic: true, children: renderInlineTokens(token.tokens ?? [], `${key}-em`) }, key));
|
|
61
|
+
case "del":
|
|
62
|
+
return (_jsx(Text, { dimColor: true, children: renderInlineTokens(token.tokens ?? [], `${key}-del`) }, key));
|
|
63
|
+
case "codespan":
|
|
64
|
+
return (_jsx(Text, { color: "cyan", children: ` \`${token.text}\` ` }, key));
|
|
65
|
+
case "link":
|
|
66
|
+
return (_jsxs(Text, { color: "blue", underline: true, children: [renderInlineTokens(token.tokens ?? [], `${key}-link`), ` (${token.href})`] }, key));
|
|
67
|
+
case "image":
|
|
68
|
+
return (_jsxs(Text, { color: "yellow", children: ["[", token.text || "image", "] ", token.href] }, key));
|
|
69
|
+
case "br":
|
|
70
|
+
return _jsx(Fragment, { children: "\n" }, key);
|
|
71
|
+
case "escape":
|
|
72
|
+
case "html":
|
|
73
|
+
return _jsx(Fragment, { children: token.text }, key);
|
|
74
|
+
default:
|
|
75
|
+
if (hasTokens(token)) {
|
|
76
|
+
return _jsx(Fragment, { children: renderInlineTokens(token.tokens, `${key}-generic`) }, key);
|
|
77
|
+
}
|
|
78
|
+
if ("text" in token && typeof token.text === "string") {
|
|
79
|
+
return _jsx(Fragment, { children: token.text }, key);
|
|
80
|
+
}
|
|
81
|
+
return _jsx(Fragment, { children: token.raw }, key);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function renderInlineTokens(tokens, keyPrefix) {
|
|
85
|
+
return tokens.map((token, index) => renderInlineToken(token, `${keyPrefix}-${index}`));
|
|
86
|
+
}
|
|
87
|
+
function renderTable(token, key) {
|
|
88
|
+
const rows = [
|
|
89
|
+
token.header.map((cell) => tokensToPlainText(cell.tokens)),
|
|
90
|
+
...token.rows.map((row) => row.map((cell) => tokensToPlainText(cell.tokens))),
|
|
91
|
+
];
|
|
92
|
+
return (_jsx(Box, { flexDirection: "column", marginBottom: 1, borderStyle: "single", borderColor: "gray", paddingX: 1, children: rows.map((row, rowIndex) => (_jsx(Text, { children: row.join(" | ") }, `${key}-row-${rowIndex}`))) }, key));
|
|
93
|
+
}
|
|
94
|
+
function renderList(token, key) {
|
|
95
|
+
const start = typeof token.start === "number" ? token.start : 1;
|
|
96
|
+
return (_jsx(Box, { flexDirection: "column", marginBottom: 1, children: token.items.map((item, index) => {
|
|
97
|
+
const marker = token.ordered ? `${start + index}.` : "•";
|
|
98
|
+
const text = tokensToPlainText(item.tokens).trim();
|
|
99
|
+
return (_jsx(Box, { children: _jsx(Text, { children: `${marker} ${text}` }) }, `${key}-item-${index}`));
|
|
100
|
+
}) }, key));
|
|
101
|
+
}
|
|
102
|
+
function renderBlock(token, key) {
|
|
103
|
+
switch (token.type) {
|
|
104
|
+
case "space":
|
|
105
|
+
return null;
|
|
106
|
+
case "heading":
|
|
107
|
+
return (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: token.depth <= 2 ? "cyan" : "green", children: renderInlineTokens(token.tokens ?? [], `${key}-heading`) }) }, key));
|
|
108
|
+
case "paragraph":
|
|
109
|
+
return (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { children: renderInlineTokens(token.tokens ?? [], `${key}-paragraph`) }) }, key));
|
|
110
|
+
case "text":
|
|
111
|
+
return (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { children: hasTokens(token) ? renderInlineTokens(token.tokens, `${key}-text`) : token.text }) }, key));
|
|
112
|
+
case "code":
|
|
113
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, borderStyle: "single", borderColor: "gray", paddingX: 1, children: [token.lang ? _jsx(Text, { dimColor: true, children: token.lang }) : null, token.text.split("\n").map((line, index) => (_jsx(Text, { color: "cyan", children: line }, `${key}-line-${index}`)))] }, key));
|
|
114
|
+
case "blockquote": {
|
|
115
|
+
const lines = tokensToPlainText(token.tokens).split("\n");
|
|
116
|
+
return (_jsx(Box, { flexDirection: "column", marginBottom: 1, paddingLeft: 1, children: lines.map((line, index) => (_jsx(Text, { dimColor: true, children: `> ${line}` }, `${key}-quote-${index}`))) }, key));
|
|
117
|
+
}
|
|
118
|
+
case "list":
|
|
119
|
+
return isListToken(token) ? renderList(token, key) : null;
|
|
120
|
+
case "table":
|
|
121
|
+
return isTableToken(token) ? renderTable(token, key) : null;
|
|
122
|
+
case "hr":
|
|
123
|
+
return (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: "----------------------------------------" }) }, key));
|
|
124
|
+
case "html":
|
|
125
|
+
return token.block ? (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { children: token.text }) }, key)) : null;
|
|
126
|
+
default:
|
|
127
|
+
return (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { children: tokensToPlainText(hasTokens(token) ? token.tokens : undefined) || token.raw }) }, key));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
export function MarkdownBlock({ content }) {
|
|
131
|
+
const trimmed = normalizeDisplayText(content).trim();
|
|
132
|
+
const tokens = useMemo(() => (trimmed ? marked.lexer(trimmed) : []), [trimmed]);
|
|
133
|
+
if (!trimmed) {
|
|
134
|
+
return _jsx(Text, {});
|
|
135
|
+
}
|
|
136
|
+
return _jsx(Box, { flexDirection: "column", children: tokens.map((token, index) => renderBlock(token, `md-${index}`)) });
|
|
137
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { decodeHtmlEntities, normalizeDisplayText, stripHtmlComments, } from "../utils/display-text.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { decodeHtmlEntities, normalizeDisplayText, stripHtmlComments, } from "../utils/display-text.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function openBrowser(url: string): Promise<boolean>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
export function openBrowser(url) {
|
|
3
|
+
const command = process.platform === "darwin"
|
|
4
|
+
? "open"
|
|
5
|
+
: process.platform === "win32"
|
|
6
|
+
? "cmd"
|
|
7
|
+
: "xdg-open";
|
|
8
|
+
const args = process.platform === "win32"
|
|
9
|
+
? ["/c", "start", "", url]
|
|
10
|
+
: [url];
|
|
11
|
+
return new Promise((resolve) => {
|
|
12
|
+
const child = spawn(command, args, {
|
|
13
|
+
detached: true,
|
|
14
|
+
stdio: "ignore",
|
|
15
|
+
});
|
|
16
|
+
child.on("error", () => resolve(false));
|
|
17
|
+
child.unref();
|
|
18
|
+
resolve(true);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
function decodeHtmlEntitiesPass(s) {
|
|
2
|
+
return s
|
|
3
|
+
.replace(/&/g, "&")
|
|
4
|
+
.replace(/&#(\d+);/g, (_, code) => String.fromCodePoint(Number(code)))
|
|
5
|
+
.replace(/&#x([0-9a-fA-F]+);/g, (_, code) => String.fromCodePoint(parseInt(code, 16)))
|
|
6
|
+
.replace(/"/g, "\"")
|
|
7
|
+
.replace(/'|'/g, "'")
|
|
8
|
+
.replace(/</g, "<")
|
|
9
|
+
.replace(/>/g, ">")
|
|
10
|
+
.replace(/ /g, " ");
|
|
11
|
+
}
|
|
12
|
+
export function decodeHtmlEntities(s) {
|
|
13
|
+
let current = s;
|
|
14
|
+
for (let i = 0; i < 5; i += 1) {
|
|
15
|
+
const decoded = decodeHtmlEntitiesPass(current);
|
|
16
|
+
if (decoded === current) {
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
current = decoded;
|
|
20
|
+
}
|
|
21
|
+
return current;
|
|
22
|
+
}
|
|
23
|
+
export function stripHtmlComments(s) {
|
|
24
|
+
let current = s.trimStart();
|
|
25
|
+
// Suppress leading comment-only frames during streaming, but keep text that follows
|
|
26
|
+
// once the leading comment block is complete.
|
|
27
|
+
while (current.startsWith("<!--")) {
|
|
28
|
+
const endIdx = current.indexOf("-->");
|
|
29
|
+
if (endIdx === -1) {
|
|
30
|
+
return "";
|
|
31
|
+
}
|
|
32
|
+
current = current.slice(endIdx + 3).trimStart();
|
|
33
|
+
}
|
|
34
|
+
return current.replace(/<!--[\s\S]*?-->/g, "").trim();
|
|
35
|
+
}
|
|
36
|
+
export function normalizeDisplayText(s) {
|
|
37
|
+
let current = s;
|
|
38
|
+
for (let i = 0; i < 5; i += 1) {
|
|
39
|
+
const normalized = stripHtmlComments(decodeHtmlEntities(current));
|
|
40
|
+
if (normalized === current) {
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
current = normalized;
|
|
44
|
+
}
|
|
45
|
+
return current;
|
|
46
|
+
}
|