@zleap-ai/dsh-sag 0.1.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/CHANGELOG.md +10 -0
- package/LICENSE +21 -0
- package/README.md +58 -0
- package/README.zh.md +58 -0
- package/THIRD_PARTY_NOTICES +671 -0
- package/cordis.patch.yml +5 -0
- package/docs/embedded.md +61 -0
- package/lib/brand.d.ts +13 -0
- package/lib/brand.js +15 -0
- package/lib/cli/runtime.d.ts +40 -0
- package/lib/cli/runtime.js +121 -0
- package/lib/cli.d.ts +21 -0
- package/lib/cli.js +265 -0
- package/lib/config.d.ts +60 -0
- package/lib/config.js +120 -0
- package/lib/connection/descriptor.d.ts +4 -0
- package/lib/connection/descriptor.js +66 -0
- package/lib/connection/discovery.d.ts +40 -0
- package/lib/connection/discovery.js +139 -0
- package/lib/connection/guidance.d.ts +7 -0
- package/lib/connection/guidance.js +7 -0
- package/lib/connection/manager.d.ts +67 -0
- package/lib/connection/manager.js +273 -0
- package/lib/connection/store.d.ts +42 -0
- package/lib/connection/store.js +164 -0
- package/lib/connection/types.d.ts +35 -0
- package/lib/connection/types.js +2 -0
- package/lib/dsh-sag-cli.js +32202 -0
- package/lib/index.d.ts +11 -0
- package/lib/index.js +68 -0
- package/lib/local/api-client.d.ts +157 -0
- package/lib/local/api-client.js +338 -0
- package/lib/local/gateway.d.ts +43 -0
- package/lib/local/gateway.js +34 -0
- package/lib/local/mcp-probe.d.ts +27 -0
- package/lib/local/mcp-probe.js +92 -0
- package/lib/presentation.d.ts +8 -0
- package/lib/presentation.js +7 -0
- package/lib/runtime/client.d.ts +22 -0
- package/lib/runtime/client.js +117 -0
- package/lib/runtime/protocol.d.ts +113 -0
- package/lib/runtime/protocol.js +118 -0
- package/lib/runtime/supervisor.d.ts +30 -0
- package/lib/runtime/supervisor.js +107 -0
- package/lib/tools/documents.d.ts +8 -0
- package/lib/tools/documents.js +134 -0
- package/lib/tools/ingest.d.ts +5 -0
- package/lib/tools/ingest.js +35 -0
- package/lib/tools/local.d.ts +40 -0
- package/lib/tools/local.js +111 -0
- package/lib/tools/output.d.ts +96 -0
- package/lib/tools/output.js +67 -0
- package/lib/tools/read.d.ts +6 -0
- package/lib/tools/read.js +84 -0
- package/lib/tools/search.d.ts +6 -0
- package/lib/tools/search.js +107 -0
- package/lib/tools/sources.d.ts +5 -0
- package/lib/tools/sources.js +61 -0
- package/lib/tools/status.d.ts +5 -0
- package/lib/tools/status.js +28 -0
- package/lib/tools/upload.d.ts +6 -0
- package/lib/tools/upload.js +68 -0
- package/package.json +96 -0
- package/runtime/pyproject.toml +29 -0
- package/runtime/src/dsh_sag_runtime/__init__.py +3 -0
- package/runtime/src/dsh_sag_runtime/__main__.py +80 -0
- package/runtime/src/dsh_sag_runtime/engines.py +139 -0
- package/runtime/src/dsh_sag_runtime/errors.py +47 -0
- package/runtime/src/dsh_sag_runtime/evidence.py +53 -0
- package/runtime/src/dsh_sag_runtime/protocol.py +161 -0
- package/runtime/src/dsh_sag_runtime/read.py +61 -0
- package/runtime/src/dsh_sag_runtime/search.py +76 -0
- package/runtime/src/dsh_sag_runtime/server.py +136 -0
- package/runtime/uv.lock +2310 -0
- package/scripts/setup-runtime.mjs +47 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
2
|
+
import { presentSearchCall } from '../presentation.js';
|
|
3
|
+
import { localInputError, localOperation, requireToolCapability } from './local.js';
|
|
4
|
+
import { SEARCH_OUTPUT_SCHEMA, renderSearch } from './output.js';
|
|
5
|
+
const LOCAL_SEARCH_OUTPUT_SCHEMA = {
|
|
6
|
+
type: 'object', additionalProperties: false, properties: {
|
|
7
|
+
query: { type: 'string', required: true },
|
|
8
|
+
evidences: { type: 'array', required: true, items: {
|
|
9
|
+
type: 'object', additionalProperties: false, properties: {
|
|
10
|
+
evidenceRef: { type: 'string', required: true }, sourceId: { type: 'string', required: true },
|
|
11
|
+
sourceName: { oneOf: [{ type: 'string' }, { type: 'null' }], required: true },
|
|
12
|
+
chunkId: { type: 'string', required: true }, title: { type: 'string', required: true },
|
|
13
|
+
excerpt: { type: 'string', required: true }, score: { type: 'number', required: true }, rank: { type: 'integer', required: true },
|
|
14
|
+
},
|
|
15
|
+
} },
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
function renderLocalSearch(value) {
|
|
19
|
+
const result = value;
|
|
20
|
+
if (result.evidences.length === 0)
|
|
21
|
+
return 'No SAG evidence matched the query.';
|
|
22
|
+
return result.evidences.map((evidence, index) => `${index + 1}. [${evidence.sourceName ?? evidence.sourceId}] ${evidence.title} (score ${evidence.score.toFixed(3)})\n ${evidence.excerpt}\n evidence_ref: ${evidence.evidenceRef}`).join('\n\n');
|
|
23
|
+
}
|
|
24
|
+
function searchResult(value) {
|
|
25
|
+
if (value === null || typeof value !== 'object' || !('evidences' in value))
|
|
26
|
+
throw new Error('SAG runtime returned the wrong search result type');
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
function createEmbeddedSearchTool(client, config) {
|
|
30
|
+
const allowed = new Set(config.namespaces.map(namespace => namespace.id));
|
|
31
|
+
const labels = new Map(config.namespaces.map(namespace => [namespace.id, namespace.label]));
|
|
32
|
+
return defineTool({
|
|
33
|
+
name: 'sag_search',
|
|
34
|
+
description: 'Search configured embedded SAG namespaces and return evidence_ref values for follow-up sag_read calls.',
|
|
35
|
+
parameters: {
|
|
36
|
+
query: { type: 'string', required: true, description: 'Natural-language or identifier search query.' },
|
|
37
|
+
namespaces: { type: 'array', items: { type: 'string' }, description: 'Optional configured namespace ids.' },
|
|
38
|
+
mode: { type: 'string', enum: ['fast', 'precise'], description: 'Embedded retrieval expansion mode.' },
|
|
39
|
+
limit: { type: 'integer', description: 'Maximum evidence items requested.' },
|
|
40
|
+
},
|
|
41
|
+
output: { schema: SEARCH_OUTPUT_SCHEMA, render: (_args, value) => [{ type: 'text', text: renderSearch(value, labels) }] },
|
|
42
|
+
timeoutMs: config.requestTimeoutMs,
|
|
43
|
+
isConcurrencySafe: () => true,
|
|
44
|
+
async execute(args, exec) {
|
|
45
|
+
const query = args.query.trim();
|
|
46
|
+
if (!query)
|
|
47
|
+
throw new Error('sag_search query must not be empty');
|
|
48
|
+
const namespaces = args.namespaces ?? config.namespaces.map(namespace => namespace.id);
|
|
49
|
+
if (namespaces.length === 0 || namespaces.some(namespace => !allowed.has(namespace)))
|
|
50
|
+
throw new Error('sag_search namespaces must be configured');
|
|
51
|
+
const requestedLimit = args.limit ?? config.maxResults;
|
|
52
|
+
if (!Number.isInteger(requestedLimit) || requestedLimit < 1)
|
|
53
|
+
throw new Error('sag_search limit must be positive');
|
|
54
|
+
return searchResult(await client.request('search', {
|
|
55
|
+
query, namespaces: [...new Set(namespaces)], mode: args.mode ?? config.defaultMode,
|
|
56
|
+
limit: Math.min(requestedLimit, config.maxResults),
|
|
57
|
+
}, exec.signal));
|
|
58
|
+
},
|
|
59
|
+
presentCall: args => presentSearchCall(args),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function createLocalSearchTool(manager, config) {
|
|
63
|
+
return defineTool({
|
|
64
|
+
name: 'sag_search',
|
|
65
|
+
description: 'Search one or more local SAG knowledge bases and return opaque evidence_ref values for sag_read.',
|
|
66
|
+
parameters: {
|
|
67
|
+
query: { type: 'string', required: true, description: 'Natural-language or identifier search query.' },
|
|
68
|
+
source_ids: { type: 'array', items: { type: 'string' }, description: 'Optional local SAG knowledge-base ids.' },
|
|
69
|
+
strategy: { type: 'string', enum: ['vector', 'multi', 'multi_es_fast'], description: 'SAG search strategy.' },
|
|
70
|
+
limit: { type: 'integer', description: 'Maximum evidence items, from 1 through 50.' },
|
|
71
|
+
},
|
|
72
|
+
output: { schema: LOCAL_SEARCH_OUTPUT_SCHEMA, render: (_args, value) => [{ type: 'text', text: renderLocalSearch(value) }] },
|
|
73
|
+
timeoutMs: config.requestTimeoutMs,
|
|
74
|
+
isConcurrencySafe: () => true,
|
|
75
|
+
execute(args, exec) {
|
|
76
|
+
return localOperation(manager, exec.signal, async (connection) => {
|
|
77
|
+
requireToolCapability(connection, 'sag_search');
|
|
78
|
+
const query = args.query.trim();
|
|
79
|
+
if (!query)
|
|
80
|
+
localInputError('sag_search query must not be empty');
|
|
81
|
+
if (args.limit !== undefined && (!Number.isInteger(args.limit) || args.limit < 1 || args.limit > 50))
|
|
82
|
+
localInputError('sag_search limit must be an integer from 1 to 50');
|
|
83
|
+
const sourceIds = args.source_ids?.map(value => value.trim()).filter(Boolean);
|
|
84
|
+
if (args.source_ids !== undefined && sourceIds?.length === 0)
|
|
85
|
+
localInputError('sag_search source_ids must contain a knowledge-base id');
|
|
86
|
+
const result = await connection.gateway.search({
|
|
87
|
+
query,
|
|
88
|
+
...(sourceIds === undefined ? {} : { sourceIds: [...new Set(sourceIds)] }),
|
|
89
|
+
...(args.limit === undefined ? {} : { topK: args.limit }),
|
|
90
|
+
...(args.strategy === undefined ? {} : { strategy: args.strategy }),
|
|
91
|
+
}, exec.signal);
|
|
92
|
+
return { query: result.query, evidences: result.evidences.map(evidence => ({
|
|
93
|
+
evidenceRef: evidence.evidenceRef, sourceId: evidence.sourceId, sourceName: evidence.sourceName, chunkId: evidence.chunkId,
|
|
94
|
+
title: evidence.heading || evidence.sourceName || evidence.chunkId, excerpt: evidence.content, score: evidence.score, rank: evidence.rank,
|
|
95
|
+
})) };
|
|
96
|
+
});
|
|
97
|
+
},
|
|
98
|
+
presentCall: args => presentSearchCall(args),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/** Build the mode-specific model-facing SAG search tool. */
|
|
102
|
+
export function createSearchTool(client, config) {
|
|
103
|
+
return config.mode === 'embedded'
|
|
104
|
+
? createEmbeddedSearchTool(client, config)
|
|
105
|
+
: createLocalSearchTool(client, config);
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=search.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ResolvedLocalConfig } from '../config.js';
|
|
2
|
+
import { type ConnectionManager } from './local.js';
|
|
3
|
+
/** Build source-list and source-creation tools for local SAG. */
|
|
4
|
+
export declare function createSourceTools(manager: ConnectionManager, config: ResolvedLocalConfig): readonly [import("@deepseek-ai/dsh-tools").ToolDefinition, import("@deepseek-ai/dsh-tools").ToolDefinition];
|
|
5
|
+
//# sourceMappingURL=sources.d.ts.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
2
|
+
import { jsonRender, localInputError, localOperation, requireToolCapability } from './local.js';
|
|
3
|
+
const SOURCE = {
|
|
4
|
+
type: 'object', additionalProperties: false, properties: {
|
|
5
|
+
id: { type: 'string', required: true }, name: { type: 'string', required: true },
|
|
6
|
+
description: { type: 'string' }, status: { type: 'string' },
|
|
7
|
+
documentCount: { type: 'integer' }, chunkCount: { type: 'integer' },
|
|
8
|
+
},
|
|
9
|
+
};
|
|
10
|
+
function sourceValue(source) {
|
|
11
|
+
return {
|
|
12
|
+
id: source.id, name: source.name,
|
|
13
|
+
...(typeof source.description === 'string' ? { description: source.description } : {}),
|
|
14
|
+
...(typeof source.status === 'string' ? { status: source.status } : {}),
|
|
15
|
+
...(typeof source.documentCount === 'number' ? { documentCount: source.documentCount } : {}),
|
|
16
|
+
...(typeof source.chunkCount === 'number' ? { chunkCount: source.chunkCount } : {}),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** Build source-list and source-creation tools for local SAG. */
|
|
20
|
+
export function createSourceTools(manager, config) {
|
|
21
|
+
const list = defineTool({
|
|
22
|
+
name: 'sag_list_sources',
|
|
23
|
+
description: 'List local SAG knowledge bases with stable ids for search, upload, and document operations.',
|
|
24
|
+
parameters: {},
|
|
25
|
+
output: {
|
|
26
|
+
schema: { type: 'object', additionalProperties: false, properties: { sources: { type: 'array', required: true, items: SOURCE } } },
|
|
27
|
+
render: (_args, value) => jsonRender(value),
|
|
28
|
+
},
|
|
29
|
+
timeoutMs: config.requestTimeoutMs,
|
|
30
|
+
isConcurrencySafe: () => true,
|
|
31
|
+
async execute(_args, exec) {
|
|
32
|
+
return localOperation(manager, exec.signal, async (connection) => {
|
|
33
|
+
requireToolCapability(connection, 'sag_list_sources');
|
|
34
|
+
return { sources: (await connection.gateway.listSources(exec.signal)).map(sourceValue) };
|
|
35
|
+
});
|
|
36
|
+
},
|
|
37
|
+
presentCall: () => ({ card: 'generic', title: 'List SAG knowledge bases', kind: 'read' }),
|
|
38
|
+
});
|
|
39
|
+
const create = defineTool({
|
|
40
|
+
name: 'sag_create_source',
|
|
41
|
+
description: 'Create a local SAG knowledge base that can receive uploaded files or directly ingested text.',
|
|
42
|
+
parameters: {
|
|
43
|
+
name: { type: 'string', required: true, description: 'Knowledge-base name.' },
|
|
44
|
+
description: { type: 'string', description: 'Optional description.' },
|
|
45
|
+
},
|
|
46
|
+
output: { schema: SOURCE, render: (_args, value) => jsonRender(value) },
|
|
47
|
+
timeoutMs: config.requestTimeoutMs,
|
|
48
|
+
async execute(args, exec) {
|
|
49
|
+
return localOperation(manager, exec.signal, async (connection) => {
|
|
50
|
+
requireToolCapability(connection, 'sag_create_source');
|
|
51
|
+
const name = args.name.trim();
|
|
52
|
+
if (!name)
|
|
53
|
+
localInputError('sag_create_source name must not be empty');
|
|
54
|
+
return sourceValue(await connection.gateway.createSource({ name, ...(args.description === undefined ? {} : { description: args.description }) }, exec.signal));
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
presentCall: args => ({ card: 'generic', title: 'Create SAG knowledge base', kind: 'edit', rawInput: args.name }),
|
|
58
|
+
});
|
|
59
|
+
return [list, create];
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=sources.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ResolvedLocalConfig } from '../config.js';
|
|
2
|
+
import { type ConnectionManager } from './local.js';
|
|
3
|
+
/** Build a read-only summary of the current local SAG connection. */
|
|
4
|
+
export declare function createStatusTool(manager: ConnectionManager, config: ResolvedLocalConfig): import("@deepseek-ai/dsh-tools").ToolDefinition;
|
|
5
|
+
//# sourceMappingURL=status.d.ts.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
2
|
+
import { jsonRender, localOperation } from './local.js';
|
|
3
|
+
const STATUS_OUTPUT = {
|
|
4
|
+
type: 'object', additionalProperties: false, properties: {
|
|
5
|
+
status: { type: 'string', required: true },
|
|
6
|
+
name: { type: 'string', required: true },
|
|
7
|
+
sourceCount: { type: 'integer', required: true },
|
|
8
|
+
capabilities: { type: 'array', required: true, items: { type: 'string' } },
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
/** Build a read-only summary of the current local SAG connection. */
|
|
12
|
+
export function createStatusTool(manager, config) {
|
|
13
|
+
return defineTool({
|
|
14
|
+
name: 'sag_status',
|
|
15
|
+
description: 'Check the local SAG connection and report its public capabilities without exposing credentials.',
|
|
16
|
+
parameters: {},
|
|
17
|
+
output: { schema: STATUS_OUTPUT, render: (_args, value) => jsonRender(value) },
|
|
18
|
+
timeoutMs: config.requestTimeoutMs,
|
|
19
|
+
isConcurrencySafe: () => true,
|
|
20
|
+
async execute(_args, exec) {
|
|
21
|
+
return localOperation(manager, exec.signal, async (connection) => {
|
|
22
|
+
return { status: 'ready', name: connection.descriptor.name, sourceCount: connection.sourceCount, capabilities: [...connection.capabilities.capabilities] };
|
|
23
|
+
});
|
|
24
|
+
},
|
|
25
|
+
presentCall: () => ({ card: 'generic', title: 'Check SAG status', kind: 'fetch' }),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=status.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { FileSystem } from '@deepseek-ai/dsh-fs';
|
|
2
|
+
import type { ResolvedLocalConfig } from '../config.js';
|
|
3
|
+
import { type ConnectionManager } from './local.js';
|
|
4
|
+
/** Build a bounded file-upload tool that reads exclusively through dsh FS. */
|
|
5
|
+
export declare function createUploadTool(manager: ConnectionManager, config: ResolvedLocalConfig, fs: FileSystem): import("@deepseek-ai/dsh-tools").ToolDefinition;
|
|
6
|
+
//# sourceMappingURL=upload.d.ts.map
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
2
|
+
import { jsonRender, localInputError, localOperation, requireToolCapability, selectSource } from './local.js';
|
|
3
|
+
const ACCEPTED_OUTPUT = {
|
|
4
|
+
type: 'object', additionalProperties: false, properties: {
|
|
5
|
+
accepted: { type: 'boolean', required: true }, documentId: { type: 'string', required: true },
|
|
6
|
+
sourceId: { type: 'string', required: true }, status: { type: 'string', required: true },
|
|
7
|
+
},
|
|
8
|
+
};
|
|
9
|
+
function basename(path) {
|
|
10
|
+
return path.split(/[\\/]/).at(-1) ?? path;
|
|
11
|
+
}
|
|
12
|
+
function extension(filename) {
|
|
13
|
+
const index = filename.lastIndexOf('.');
|
|
14
|
+
return index < 0 ? '' : filename.slice(index + 1).toLowerCase();
|
|
15
|
+
}
|
|
16
|
+
function contentType(ext) {
|
|
17
|
+
if (ext === 'md' || ext === 'markdown')
|
|
18
|
+
return 'text/markdown';
|
|
19
|
+
if (ext === 'txt')
|
|
20
|
+
return 'text/plain';
|
|
21
|
+
if (ext === 'json')
|
|
22
|
+
return 'application/json';
|
|
23
|
+
if (ext === 'pdf')
|
|
24
|
+
return 'application/pdf';
|
|
25
|
+
return 'application/octet-stream';
|
|
26
|
+
}
|
|
27
|
+
/** Build a bounded file-upload tool that reads exclusively through dsh FS. */
|
|
28
|
+
export function createUploadTool(manager, config, fs) {
|
|
29
|
+
return defineTool({
|
|
30
|
+
name: 'sag_upload_file',
|
|
31
|
+
description: 'Upload one admitted local file to a SAG knowledge base and return its asynchronous processing status.',
|
|
32
|
+
parameters: {
|
|
33
|
+
path: { type: 'string', required: true, description: 'File path resolved through the dsh filesystem.' },
|
|
34
|
+
source_id: { type: 'string', description: 'Target knowledge-base id; omitted only when a default or single source exists.' },
|
|
35
|
+
},
|
|
36
|
+
output: { schema: ACCEPTED_OUTPUT, render: (_args, value) => jsonRender(value) },
|
|
37
|
+
timeoutMs: config.requestTimeoutMs,
|
|
38
|
+
async execute(args, exec) {
|
|
39
|
+
return localOperation(manager, exec.signal, async (connection) => {
|
|
40
|
+
requireToolCapability(connection, 'sag_upload_file');
|
|
41
|
+
const upload = connection.capabilities.upload;
|
|
42
|
+
if (upload === undefined)
|
|
43
|
+
localInputError('This SAG version does not provide documents.upload limits. Upgrade SAG and retry.');
|
|
44
|
+
const sourceId = await selectSource(connection, args.source_id, exec.signal);
|
|
45
|
+
const target = await fs.resolve(args.path, { signal: exec.signal });
|
|
46
|
+
const info = await fs.stat(target, exec.signal);
|
|
47
|
+
if (info?.type !== 'file')
|
|
48
|
+
localInputError('sag_upload_file 只能上传普通文件。');
|
|
49
|
+
const filename = basename(target.displayPath);
|
|
50
|
+
const ext = extension(filename);
|
|
51
|
+
const allowed = upload.extensions.map(value => value.toLowerCase());
|
|
52
|
+
if (!allowed.includes(ext))
|
|
53
|
+
localInputError(`SAG 不允许 .${ext || '(无扩展名)'} 文件;可用扩展名:${allowed.join('、')}`);
|
|
54
|
+
const sagLimit = upload.maxMb * 1024 * 1024;
|
|
55
|
+
const limit = Math.min(sagLimit, config.maxUploadBytes);
|
|
56
|
+
if (info.size !== undefined && info.size > limit)
|
|
57
|
+
localInputError(`文件超过上传上限 ${Math.floor(limit / 1024 / 1024)} MiB。`);
|
|
58
|
+
const bytes = await fs.readBytes(target, exec.signal, limit);
|
|
59
|
+
if (bytes.length > limit)
|
|
60
|
+
localInputError(`文件超过上传上限 ${Math.floor(limit / 1024 / 1024)} MiB。`);
|
|
61
|
+
const document = await connection.gateway.uploadFile({ sourceId, filename, contentType: contentType(ext), bytes }, exec.signal);
|
|
62
|
+
return { accepted: true, documentId: document.id, sourceId, status: document.status ?? 'pending' };
|
|
63
|
+
});
|
|
64
|
+
},
|
|
65
|
+
presentCall: args => ({ card: 'generic', title: 'Upload file to SAG', kind: 'edit', rawInput: args.path, locations: [{ path: args.path }] }),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=upload.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zleap-ai/dsh-sag",
|
|
3
|
+
"description": "Local SAG personal knowledge base plugin for DeepSeek Harness",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"deepseek-harness",
|
|
7
|
+
"dsh",
|
|
8
|
+
"dsh-plugin",
|
|
9
|
+
"sag",
|
|
10
|
+
"knowledge-base"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/Zleap-AI/dsh-sag#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/Zleap-AI/dsh-sag/issues"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/Zleap-AI/dsh-sag.git"
|
|
19
|
+
},
|
|
20
|
+
"type": "module",
|
|
21
|
+
"main": "lib/index.js",
|
|
22
|
+
"types": "lib/index.d.ts",
|
|
23
|
+
"bin": {
|
|
24
|
+
"dsh-sag": "./lib/dsh-sag-cli.js"
|
|
25
|
+
},
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./lib/index.d.ts",
|
|
29
|
+
"default": "./lib/index.js"
|
|
30
|
+
},
|
|
31
|
+
"./package.json": "./package.json"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"lib/**/*.js",
|
|
35
|
+
"lib/**/*.d.ts",
|
|
36
|
+
"cordis.patch.yml",
|
|
37
|
+
"runtime",
|
|
38
|
+
"scripts/setup-runtime.mjs",
|
|
39
|
+
"docs/embedded.md",
|
|
40
|
+
"THIRD_PARTY_NOTICES",
|
|
41
|
+
"CHANGELOG.md",
|
|
42
|
+
"README.md",
|
|
43
|
+
"README.zh.md",
|
|
44
|
+
"LICENSE"
|
|
45
|
+
],
|
|
46
|
+
"dsh": {
|
|
47
|
+
"bundle": {
|
|
48
|
+
"patch": "./cordis.patch.yml"
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": "^22.19.0 || >=24.0.0"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"@deepseek-ai/schemastery": "3.18.1",
|
|
56
|
+
"@modelcontextprotocol/sdk": "^1.12.0"
|
|
57
|
+
},
|
|
58
|
+
"peerDependencies": {
|
|
59
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
60
|
+
"@deepseek-ai/dsh-credentials": "0.1.1-rc.2",
|
|
61
|
+
"@deepseek-ai/dsh-credentials-local": "0.1.1-rc.2",
|
|
62
|
+
"@deepseek-ai/dsh-fs": "0.1.1-rc.2",
|
|
63
|
+
"@deepseek-ai/dsh-llm": "0.1.1-rc.2",
|
|
64
|
+
"@deepseek-ai/dsh-session": "0.1.1-rc.2",
|
|
65
|
+
"@deepseek-ai/dsh-settings": "0.1.1-rc.2",
|
|
66
|
+
"@deepseek-ai/dsh-settings-file": "0.1.1-rc.2",
|
|
67
|
+
"@deepseek-ai/dsh-subprocess": "0.1.1-rc.2",
|
|
68
|
+
"@deepseek-ai/dsh-subprocess-local": "0.1.1-rc.2",
|
|
69
|
+
"@deepseek-ai/dsh-system-prompt": "0.1.1-rc.2",
|
|
70
|
+
"@deepseek-ai/dsh-tools": "0.1.1-rc.2"
|
|
71
|
+
},
|
|
72
|
+
"devDependencies": {
|
|
73
|
+
"@deepseek-ai/cordis": "4.0.1",
|
|
74
|
+
"@deepseek-ai/dsh-credentials": "0.1.1-rc.2",
|
|
75
|
+
"@deepseek-ai/dsh-credentials-local": "0.1.1-rc.2",
|
|
76
|
+
"@deepseek-ai/dsh-fs": "0.1.1-rc.2",
|
|
77
|
+
"@deepseek-ai/dsh-llm": "0.1.1-rc.2",
|
|
78
|
+
"@deepseek-ai/dsh-session": "0.1.1-rc.2",
|
|
79
|
+
"@deepseek-ai/dsh-settings": "0.1.1-rc.2",
|
|
80
|
+
"@deepseek-ai/dsh-settings-file": "0.1.1-rc.2",
|
|
81
|
+
"@deepseek-ai/dsh-subprocess": "0.1.1-rc.2",
|
|
82
|
+
"@deepseek-ai/dsh-subprocess-local": "0.1.1-rc.2",
|
|
83
|
+
"@deepseek-ai/dsh-system-prompt": "0.1.1-rc.2",
|
|
84
|
+
"@deepseek-ai/dsh-tools": "0.1.1-rc.2",
|
|
85
|
+
"@types/node": "^24.3.0",
|
|
86
|
+
"typescript": "^5.9.2"
|
|
87
|
+
},
|
|
88
|
+
"license": "MIT",
|
|
89
|
+
"publishConfig": {
|
|
90
|
+
"access": "public"
|
|
91
|
+
},
|
|
92
|
+
"scripts": {
|
|
93
|
+
"build": "tsc -p tsconfig.json && node ../../scripts/build-cli.mjs && node ../../scripts/sync-runtime.mjs",
|
|
94
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27,<2"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "dsh-sag-runtime"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Managed zleap-sag sidecar for the dsh-sag plugin"
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"pydantic>=2.5,<3",
|
|
12
|
+
"zleap-sag==0.10.0",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.scripts]
|
|
16
|
+
dsh-sag-runtime = "dsh_sag_runtime.__main__:main"
|
|
17
|
+
|
|
18
|
+
[dependency-groups]
|
|
19
|
+
dev = [
|
|
20
|
+
"pytest>=8.4,<9",
|
|
21
|
+
"pytest-asyncio>=1.1,<2",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[tool.hatch.build.targets.wheel]
|
|
25
|
+
packages = ["src/dsh_sag_runtime"]
|
|
26
|
+
|
|
27
|
+
[tool.pytest.ini_options]
|
|
28
|
+
asyncio_mode = "auto"
|
|
29
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""stdio entrypoint for the managed dsh-sag runtime."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import asyncio
|
|
7
|
+
import importlib.metadata
|
|
8
|
+
import sys
|
|
9
|
+
from typing import BinaryIO, TextIO
|
|
10
|
+
|
|
11
|
+
from zleap.sag import EngineConfig
|
|
12
|
+
|
|
13
|
+
from .engines import EnginePool
|
|
14
|
+
from .evidence import EvidenceRefCodec
|
|
15
|
+
from .protocol import REQUIRED_ENGINE_VERSION, decode_request, encode_response
|
|
16
|
+
from .read import ReadAdapter
|
|
17
|
+
from .search import SearchAdapter
|
|
18
|
+
from .server import RuntimeServer
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def run_stdio(server: RuntimeServer, reader: BinaryIO, writer: BinaryIO, diagnostics: TextIO) -> None:
|
|
22
|
+
"""Serve requests until stdin closes, keeping stdout protocol-only."""
|
|
23
|
+
write_lock = asyncio.Lock()
|
|
24
|
+
tasks: set[asyncio.Task[None]] = set()
|
|
25
|
+
|
|
26
|
+
async def process(frame: bytes) -> None:
|
|
27
|
+
try:
|
|
28
|
+
request = decode_request(frame.rstrip(b"\r\n"))
|
|
29
|
+
response = await server.accept(request)
|
|
30
|
+
if response is not None:
|
|
31
|
+
encoded = encode_response(response)
|
|
32
|
+
async with write_lock:
|
|
33
|
+
writer.write(encoded)
|
|
34
|
+
writer.flush()
|
|
35
|
+
except Exception as error:
|
|
36
|
+
print(f"dsh-sag-runtime rejected a frame: {type(error).__name__}", file=diagnostics, flush=True)
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
while frame := await asyncio.to_thread(reader.readline):
|
|
40
|
+
task = asyncio.create_task(process(frame))
|
|
41
|
+
tasks.add(task)
|
|
42
|
+
task.add_done_callback(tasks.discard)
|
|
43
|
+
if tasks:
|
|
44
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
45
|
+
finally:
|
|
46
|
+
await server.aclose()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
50
|
+
parser = argparse.ArgumentParser(prog="dsh-sag-runtime")
|
|
51
|
+
parser.add_argument("--env-file", required=True)
|
|
52
|
+
parser.add_argument("--namespace", action="append", required=True)
|
|
53
|
+
parser.add_argument("--max-read-engines", type=int, default=4)
|
|
54
|
+
parser.add_argument("--max-results", type=int, default=20)
|
|
55
|
+
parser.add_argument("--max-excerpt-chars", type=int, default=1200)
|
|
56
|
+
parser.add_argument("--max-read-chars", type=int, default=40_000)
|
|
57
|
+
return parser.parse_args(argv)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
async def async_main(argv: list[str] | None = None) -> None:
|
|
61
|
+
args = parse_args(argv)
|
|
62
|
+
engine_version = importlib.metadata.version("zleap-sag")
|
|
63
|
+
if engine_version != REQUIRED_ENGINE_VERSION:
|
|
64
|
+
raise RuntimeError(f"zleap-sag {REQUIRED_ENGINE_VERSION} is required")
|
|
65
|
+
namespaces = tuple(dict.fromkeys(args.namespace))
|
|
66
|
+
config = EngineConfig.from_env(args.env_file)
|
|
67
|
+
pool = EnginePool(config, set(namespaces), max_read_engines=args.max_read_engines)
|
|
68
|
+
codec = EvidenceRefCodec(set(namespaces))
|
|
69
|
+
search = SearchAdapter(pool, codec, set(namespaces), max_results=args.max_results, max_excerpt_chars=args.max_excerpt_chars)
|
|
70
|
+
read = ReadAdapter(pool, codec, max_read_chars=args.max_read_chars, max_events=20)
|
|
71
|
+
server = RuntimeServer(pool, search, read, namespaces, engine_version=engine_version)
|
|
72
|
+
await run_stdio(server, sys.stdin.buffer, sys.stdout.buffer, sys.stderr)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def main() -> None:
|
|
76
|
+
asyncio.run(async_main())
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
if __name__ == "__main__":
|
|
80
|
+
main()
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Owned zleap-sag engine lifecycle with a bounded read-engine LRU."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from collections import OrderedDict
|
|
7
|
+
from collections.abc import AsyncIterator, Callable
|
|
8
|
+
from contextlib import asynccontextmanager
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Any, Protocol
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Engine(Protocol):
|
|
14
|
+
async def start(self) -> None: ...
|
|
15
|
+
async def aclose(self) -> None: ...
|
|
16
|
+
async def health(self) -> Any: ...
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
EngineFactory = Callable[[object, str | None], Engine]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _default_factory(config: object, namespace: str | None) -> Engine:
|
|
23
|
+
from zleap.sag import DataEngine
|
|
24
|
+
|
|
25
|
+
return DataEngine(config, data_source_id=namespace) # type: ignore[arg-type]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(slots=True)
|
|
29
|
+
class _ReadEntry:
|
|
30
|
+
engine: Engine
|
|
31
|
+
active: int = 0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class EnginePool:
|
|
35
|
+
"""Own one search engine and at most `max_read_engines` namespace engines."""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
config: object,
|
|
40
|
+
namespaces: set[str] | frozenset[str],
|
|
41
|
+
*,
|
|
42
|
+
max_read_engines: int,
|
|
43
|
+
factory: EngineFactory = _default_factory,
|
|
44
|
+
) -> None:
|
|
45
|
+
if max_read_engines < 1:
|
|
46
|
+
raise ValueError("max_read_engines must be positive")
|
|
47
|
+
self._config = config
|
|
48
|
+
self._namespaces = frozenset(namespaces)
|
|
49
|
+
self._max_read_engines = max_read_engines
|
|
50
|
+
self._factory = factory
|
|
51
|
+
self._condition = asyncio.Condition()
|
|
52
|
+
self._search: Engine | None = None
|
|
53
|
+
self._reads: OrderedDict[str, _ReadEntry] = OrderedDict()
|
|
54
|
+
self._closed = False
|
|
55
|
+
|
|
56
|
+
async def start(self) -> None:
|
|
57
|
+
async with self._condition:
|
|
58
|
+
if self._closed:
|
|
59
|
+
raise RuntimeError("engine pool is closed")
|
|
60
|
+
if self._search is not None:
|
|
61
|
+
return
|
|
62
|
+
engine = self._factory(self._config, None)
|
|
63
|
+
try:
|
|
64
|
+
await engine.start()
|
|
65
|
+
except BaseException:
|
|
66
|
+
await engine.aclose()
|
|
67
|
+
raise
|
|
68
|
+
self._search = engine
|
|
69
|
+
|
|
70
|
+
async def search_engine(self) -> Engine:
|
|
71
|
+
async with self._condition:
|
|
72
|
+
if self._closed:
|
|
73
|
+
raise RuntimeError("engine pool is closed")
|
|
74
|
+
if self._search is None:
|
|
75
|
+
raise RuntimeError("engine pool is not started")
|
|
76
|
+
return self._search
|
|
77
|
+
|
|
78
|
+
@asynccontextmanager
|
|
79
|
+
async def read_engine(self, namespace_id: str) -> AsyncIterator[Engine]:
|
|
80
|
+
if namespace_id not in self._namespaces:
|
|
81
|
+
raise ValueError("namespace is not configured")
|
|
82
|
+
entry: _ReadEntry
|
|
83
|
+
async with self._condition:
|
|
84
|
+
while True:
|
|
85
|
+
if self._closed:
|
|
86
|
+
raise RuntimeError("engine pool is closed")
|
|
87
|
+
existing = self._reads.get(namespace_id)
|
|
88
|
+
if existing is not None:
|
|
89
|
+
entry = existing
|
|
90
|
+
entry.active += 1
|
|
91
|
+
self._reads.move_to_end(namespace_id)
|
|
92
|
+
break
|
|
93
|
+
if len(self._reads) >= self._max_read_engines:
|
|
94
|
+
idle_namespace = next((name for name, candidate in self._reads.items() if candidate.active == 0), None)
|
|
95
|
+
if idle_namespace is None:
|
|
96
|
+
await self._condition.wait()
|
|
97
|
+
continue
|
|
98
|
+
idle = self._reads.pop(idle_namespace)
|
|
99
|
+
await idle.engine.aclose()
|
|
100
|
+
engine = self._factory(self._config, namespace_id)
|
|
101
|
+
try:
|
|
102
|
+
await engine.start()
|
|
103
|
+
except BaseException:
|
|
104
|
+
await engine.aclose()
|
|
105
|
+
raise
|
|
106
|
+
entry = _ReadEntry(engine=engine, active=1)
|
|
107
|
+
self._reads[namespace_id] = entry
|
|
108
|
+
break
|
|
109
|
+
try:
|
|
110
|
+
yield entry.engine
|
|
111
|
+
finally:
|
|
112
|
+
async with self._condition:
|
|
113
|
+
entry.active -= 1
|
|
114
|
+
self._condition.notify_all()
|
|
115
|
+
|
|
116
|
+
async def health(self) -> Any:
|
|
117
|
+
return await (await self.search_engine()).health()
|
|
118
|
+
|
|
119
|
+
async def capabilities(self) -> Any:
|
|
120
|
+
engine = await self.search_engine()
|
|
121
|
+
return engine.capabilities() # type: ignore[attr-defined]
|
|
122
|
+
|
|
123
|
+
async def aclose(self) -> None:
|
|
124
|
+
async with self._condition:
|
|
125
|
+
if self._closed:
|
|
126
|
+
return
|
|
127
|
+
self._closed = True
|
|
128
|
+
self._condition.notify_all()
|
|
129
|
+
while any(entry.active for entry in self._reads.values()):
|
|
130
|
+
await self._condition.wait()
|
|
131
|
+
engines = [entry.engine for entry in self._reads.values()]
|
|
132
|
+
if self._search is not None:
|
|
133
|
+
engines.insert(0, self._search)
|
|
134
|
+
self._reads.clear()
|
|
135
|
+
self._search = None
|
|
136
|
+
results = await asyncio.gather(*(engine.aclose() for engine in engines), return_exceptions=True)
|
|
137
|
+
failures = [result for result in results if isinstance(result, BaseException)]
|
|
138
|
+
if failures:
|
|
139
|
+
raise ExceptionGroup("failed to close SAG engines", failures)
|