dsh-search-enhance 0.1.4 → 0.1.5
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 +11 -9
- package/lib/diagnostics/probes.d.ts +2 -1
- package/lib/diagnostics/probes.js +3 -1
- package/lib/documentation/cache.d.ts +1 -0
- package/lib/documentation/cache.js +4 -0
- package/lib/documentation/context7-cache.d.ts +1 -0
- package/lib/documentation/context7-cache.js +1 -0
- package/lib/documentation/index.d.ts +1 -1
- package/lib/documentation/index.js +1 -1
- package/lib/documentation/service.d.ts +6 -12
- package/lib/documentation/service.js +43 -74
- package/lib/index.js +1 -2
- package/lib/orchestration/orchestrator.d.ts +0 -1
- package/lib/orchestration/orchestrator.js +2 -15
- package/lib/orchestration/types.d.ts +0 -1
- package/lib/presentation/render.d.ts +4 -0
- package/lib/presentation/render.js +8 -3
- package/lib/prompt/tool-discovery.js +2 -0
- package/lib/providers/context7.d.ts +7 -12
- package/lib/providers/context7.js +32 -97
- package/lib/providers/index.d.ts +1 -1
- package/lib/providers/index.js +1 -1
- package/lib/search/parse.d.ts +1 -1
- package/lib/search/parse.js +236 -30
- package/lib/tools/docs-search.js +2 -1
- package/lib/tools/schemas.d.ts +6 -2
- package/lib/tools/schemas.js +6 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,11 +14,11 @@ DSH Agent
|
|
|
14
14
|
│
|
|
15
15
|
└─ 固定模型工具 surface(schema 与顺序不随披露状态变化)
|
|
16
16
|
├─ web_search ──────> Grok 主搜索
|
|
17
|
-
│ ├─ 按需要补充
|
|
17
|
+
│ ├─ 按需要补充 Exa 文档来源
|
|
18
18
|
│ ├─ 按需要补充 Tavily / Firecrawl
|
|
19
19
|
│ └─ 返回回答、来源;有 source_ref 时追加 search_sources manifest
|
|
20
20
|
│
|
|
21
|
-
├─ docs_search ─────> Context7 / Exa
|
|
21
|
+
├─ docs_search ─────> 显式 Context7 库身份 / Exa 文档发现
|
|
22
22
|
│ └─ 返回文档片段、来源;有 source_ref 时追加 search_sources manifest
|
|
23
23
|
│
|
|
24
24
|
├─ web_extract ─────> Tavily → Firecrawl → smart_direct → direct
|
|
@@ -45,7 +45,7 @@ DSH Agent
|
|
|
45
45
|
| 工具 | 调用方式与用途 |
|
|
46
46
|
| --- | --- |
|
|
47
47
|
| `web_search` | 直接调用。使用 Grok 生成通用搜索的主要回答,并按搜索类型补充其他来源 |
|
|
48
|
-
| `docs_search` |
|
|
48
|
+
| `docs_search` | 直接调用。已知精确 `/org/project` 时传 `library_id`;已知包或产品名时传 `library_name`;不知道库身份时用 `provider: auto` 进行 Exa 发现 |
|
|
49
49
|
| `web_extract` | 直接调用。读取指定网页正文,用于核对搜索摘要中的重要内容 |
|
|
50
50
|
| `search_tools` | 直接调用。按需返回延迟能力的 operation manifest,不注册新的模型工具 |
|
|
51
51
|
| `search_call` | 固定网关。通过 `search_call({ operation, arguments })` 调用已经激活的延迟 operation |
|
|
@@ -78,11 +78,13 @@ DSH Agent
|
|
|
78
78
|
### 一次完整搜索如何进行
|
|
79
79
|
|
|
80
80
|
1. 通用问题直接调用 `web_search`,文档问题直接调用 `docs_search`。
|
|
81
|
-
2.
|
|
82
|
-
3.
|
|
83
|
-
4.
|
|
84
|
-
5.
|
|
85
|
-
6.
|
|
81
|
+
2. 调用 `docs_search` 时保持任务问题与库身份分离。例如 `{"query":"JWT authentication middleware API","library_name":"FastAPI"}`。同时提供 `library_id` 和 `library_name` 时,合法 `library_id` 优先并跳过 resolve。
|
|
82
|
+
3. `provider: auto` 没有 `library_id`/`library_name` 时只使用 Exa;`provider: context7` 或 `all` 缺少库身份会在凭据、缓存和网络前失败。普通 `web_search` 的文档增强也只使用 Exa,不猜测 Context7 库。
|
|
83
|
+
4. 当搜索产生来源时,结果会包含可见来源、`source_ref` 和追加的 `search_sources` manifest,同时自动激活 `sources`。
|
|
84
|
+
5. 在下一 step 需要更多来源时,调用 `search_call({ operation: 'search_sources', arguments: { source_ref, offset: 0, limit: 20, format: 'compact' } })` 分页读取,而不是直接调用 `search_sources`。
|
|
85
|
+
6. 对重要结论,选择权威链接并直接调用 `web_extract` 获取网页正文。
|
|
86
|
+
7. 如果任务需要站点内发现、研究计划、精细 Context7 查询或连接检查,先调用例如 `search_tools({ capabilities: ['site_map'] })` 取得 manifest;`progressive` 模式从下一 step、`all` 模式立即通过 `search_call({ operation: 'web_map', arguments: { url: 'https://example.com' } })` 调用相应 operation。
|
|
87
|
+
8. 最终回答综合主搜索、补充来源和已经读取的网页正文,并保留来源链接。
|
|
86
88
|
|
|
87
89
|
`source_ref` 只是完整来源列表的引用,不等同于网页正文;重要事实仍应通过 `web_extract` 读取原页面后再下结论。
|
|
88
90
|
|
|
@@ -135,7 +137,7 @@ dsh web
|
|
|
135
137
|
|
|
136
138
|
| 服务 | 用途 | 默认密钥名称 |
|
|
137
139
|
| --- | --- | --- |
|
|
138
|
-
| Context7 | 查找库和框架文档 | `CONTEXT7_API_KEY` |
|
|
140
|
+
| Context7 | 使用显式 `library_name` 或 `library_id` 查找库和框架文档 | `CONTEXT7_API_KEY` |
|
|
139
141
|
| Exa | 补充文档和网页结果 | `EXA_API_KEY` |
|
|
140
142
|
| Tavily | 补充搜索、读取网页和发现站点页面 | `TAVILY_API_KEY` |
|
|
141
143
|
| Firecrawl | 补充搜索和读取网页 | `FIRECRAWL_API_KEY` |
|
|
@@ -4,7 +4,8 @@ import type { BoundedSourceProvider } from '../providers/types.js';
|
|
|
4
4
|
import type { DiagnosticCapability, DiagnosticProbe, DiagnosticProbeInput, DiagnosticProbeResult, DiagnosticProviderName } from './types.js';
|
|
5
5
|
/** Fixed, non-user-controlled diagnostic targets. */
|
|
6
6
|
export declare const DIAGNOSTIC_SEARCH_QUERY = "search-enhance fixed connectivity diagnostic";
|
|
7
|
-
export declare const
|
|
7
|
+
export declare const DIAGNOSTIC_CONTEXT7_LIBRARY_NAME = "React";
|
|
8
|
+
export declare const DIAGNOSTIC_CONTEXT7_QUERY = "documentation connectivity diagnostic";
|
|
8
9
|
export declare const DIAGNOSTIC_RESULT_LIMIT = 1;
|
|
9
10
|
/** Uses the public bounded model-list GET; it never calls Search API main search. */
|
|
10
11
|
export declare class SearchApiModelListDiagnosticProbe implements DiagnosticProbe {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { throwIfAborted } from '../provider-runtime/index.js';
|
|
2
2
|
/** Fixed, non-user-controlled diagnostic targets. */
|
|
3
3
|
export const DIAGNOSTIC_SEARCH_QUERY = 'search-enhance fixed connectivity diagnostic';
|
|
4
|
-
export const
|
|
4
|
+
export const DIAGNOSTIC_CONTEXT7_LIBRARY_NAME = 'React';
|
|
5
|
+
export const DIAGNOSTIC_CONTEXT7_QUERY = 'documentation connectivity diagnostic';
|
|
5
6
|
export const DIAGNOSTIC_RESULT_LIMIT = 1;
|
|
6
7
|
const COMPLETE = Object.freeze({ state: 'complete' });
|
|
7
8
|
const NOT_CONFIGURED = Object.freeze({ state: 'not_configured' });
|
|
@@ -38,6 +39,7 @@ export class Context7ResolveDiagnosticProbe {
|
|
|
38
39
|
await this.context7.resolve({
|
|
39
40
|
config: input.config,
|
|
40
41
|
limit: DIAGNOSTIC_RESULT_LIMIT,
|
|
42
|
+
libraryName: DIAGNOSTIC_CONTEXT7_LIBRARY_NAME,
|
|
41
43
|
onDispatch: input.onDispatch,
|
|
42
44
|
query: DIAGNOSTIC_CONTEXT7_QUERY,
|
|
43
45
|
signal: input.signal,
|
|
@@ -49,6 +49,7 @@ export interface Context7CacheRepository {
|
|
|
49
49
|
/** Cache identity includes every bounded input that can change a successful resolve value. */
|
|
50
50
|
export declare function context7ResolveCacheKey(input: {
|
|
51
51
|
readonly baseUrl: string;
|
|
52
|
+
readonly libraryName: string;
|
|
52
53
|
readonly query: string;
|
|
53
54
|
readonly maxResults: number;
|
|
54
55
|
readonly maxLibraryTextCharacters: number;
|
|
@@ -40,11 +40,15 @@ export function context7ResolveCacheKey(input) {
|
|
|
40
40
|
positiveSafeInteger(input.maxResults, 'maxResults');
|
|
41
41
|
positiveSafeInteger(input.maxLibraryTextCharacters, 'maxLibraryTextCharacters');
|
|
42
42
|
positiveSafeInteger(input.maxEntryBytes, 'maxEntryBytes');
|
|
43
|
+
const libraryName = input.libraryName.trim();
|
|
43
44
|
const query = input.query.trim();
|
|
45
|
+
if (libraryName.length === 0)
|
|
46
|
+
throw new RangeError('Context7 library name must not be empty');
|
|
44
47
|
if (query.length === 0)
|
|
45
48
|
throw new RangeError('Context7 resolve query must not be empty');
|
|
46
49
|
return `ctx7r_${digestIdentity('resolve', {
|
|
47
50
|
baseUrl: normalizedBaseUrl(input.baseUrl),
|
|
51
|
+
libraryName,
|
|
48
52
|
maxEntryBytes: input.maxEntryBytes,
|
|
49
53
|
maxLibraryTextCharacters: input.maxLibraryTextCharacters,
|
|
50
54
|
maxResults: input.maxResults,
|
|
@@ -174,6 +174,7 @@ export class Context7CachedOperations {
|
|
|
174
174
|
throwIfAborted(input.signal);
|
|
175
175
|
const cacheKey = context7ResolveCacheKey({
|
|
176
176
|
baseUrl: input.config.providers.context7.baseUrl,
|
|
177
|
+
libraryName: input.libraryName,
|
|
177
178
|
maxEntryBytes: input.config.cache.context7EntryMaxBytes,
|
|
178
179
|
maxLibraryTextCharacters: input.config.cache.context7LibraryTextMaxCharacters,
|
|
179
180
|
maxResults: input.maxResults,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { CONTEXT7_CACHE_DOMAIN_NAME, CONTEXT7_CACHE_DOMAIN_SPEC, CONTEXT7_CACHE_FORMAT_VERSION, CONTEXT7_CACHE_KEY_PATTERN, CONTEXT7_CACHE_TABLE_NAME, CONTEXT7_DOC_REF_PATTERN, CachedContext7LibrarySchema, CachedDocumentationSnippetSchema, Context7CacheEntrySchema, Context7CacheKeySchema, Context7DocsCacheEntrySchema, Context7DocRefSchema, Context7ResolveCacheEntrySchema, type CachedContext7Library, type CachedDocumentationSnippet, type Context7CacheDomain, type Context7CacheEntry, type Context7CacheKey, type Context7DocsCacheEntry, type Context7DocRef, type Context7ResolveCacheEntry, } from './cache-domain.js';
|
|
2
2
|
export { CONTEXT7_CACHE_QUERY_MAX_SCAN_RECORDS, CONTEXT7_CACHE_QUERY_MAX_TERMS, CONTEXT7_CACHE_QUERY_MAX_TEXT_CHARACTERS, Context7CacheError, Context7CacheStore, PersistentContext7Cache, context7DocsCacheKey, context7ResolveCacheKey, isContext7DocRef, type Context7CacheErrorCode, type Context7CacheLimits, type Context7CacheRepository, type Context7CacheWriteResult, type Context7CachedDocFound, type Context7CachedDocLookup, type Context7CachedDocMatch, type Context7CachedDocMatchFound, type Context7CachedDocMatchNotFound, type Context7CachedDocNotFound, type Context7CachedDocQuery, } from './cache.js';
|
|
3
3
|
export { CONTEXT7_CACHE_STATES, Context7CachedOperations, Context7OperationFailure, context7CacheEntryIsFresh, isContext7LibraryId, normalizeContext7LibraryId, permitsContext7StaleFallback, type CachedContext7Operation, type Context7CacheClock, type Context7CacheState, type Context7DocsCachedInput, type Context7DocsRemoteResult, type Context7OperationPath, type Context7RemoteDiagnostics, type Context7ResolveCachedInput, type Context7ResolveRemoteResult, } from './context7-cache.js';
|
|
4
|
-
export { DOCUMENTATION_CACHE_PATH_STATES, DOCUMENTATION_CACHE_SKIP_REASONS, DOCUMENTATION_PROVIDER_STATES, DOCUMENTATION_RESULT_PROVIDERS, DOCUMENTATION_SEARCH_PROVIDERS, DOCUMENTATION_SEARCH_SERVICE_KEY, DOCUMENTATION_WARNING_CODES,
|
|
4
|
+
export { DOCUMENTATION_CACHE_PATH_STATES, DOCUMENTATION_CACHE_SKIP_REASONS, DOCUMENTATION_PROVIDER_STATES, DOCUMENTATION_RESULT_PROVIDERS, DOCUMENTATION_SEARCH_PROVIDERS, DOCUMENTATION_SEARCH_SERVICE_KEY, DOCUMENTATION_WARNING_CODES, DocumentationSearchInfrastructureError, DocumentationSearchService, type Context7CachedDocSearchInput, type Context7DocsInput, type Context7DocsResult, type Context7ResolveInput, type Context7ResolveResult, type DocumentationCachePath, type DocumentationCachePathState, type DocumentationCacheReport, type DocumentationCacheSkipReason, type DocumentationProviderState, type DocumentationProviderStatus, type DocumentationResultProvider, type DocumentationSearchDependencies, type DocumentationSearchInput, type DocumentationSearchProvider, type DocumentationSearchResult, type DocumentationWarning, type DocumentationWarningCode, } from './service.js';
|
|
5
5
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { CONTEXT7_CACHE_DOMAIN_NAME, CONTEXT7_CACHE_DOMAIN_SPEC, CONTEXT7_CACHE_FORMAT_VERSION, CONTEXT7_CACHE_KEY_PATTERN, CONTEXT7_CACHE_TABLE_NAME, CONTEXT7_DOC_REF_PATTERN, CachedContext7LibrarySchema, CachedDocumentationSnippetSchema, Context7CacheEntrySchema, Context7CacheKeySchema, Context7DocsCacheEntrySchema, Context7DocRefSchema, Context7ResolveCacheEntrySchema, } from './cache-domain.js';
|
|
2
2
|
export { CONTEXT7_CACHE_QUERY_MAX_SCAN_RECORDS, CONTEXT7_CACHE_QUERY_MAX_TERMS, CONTEXT7_CACHE_QUERY_MAX_TEXT_CHARACTERS, Context7CacheError, Context7CacheStore, PersistentContext7Cache, context7DocsCacheKey, context7ResolveCacheKey, isContext7DocRef, } from './cache.js';
|
|
3
3
|
export { CONTEXT7_CACHE_STATES, Context7CachedOperations, Context7OperationFailure, context7CacheEntryIsFresh, isContext7LibraryId, normalizeContext7LibraryId, permitsContext7StaleFallback, } from './context7-cache.js';
|
|
4
|
-
export { DOCUMENTATION_CACHE_PATH_STATES, DOCUMENTATION_CACHE_SKIP_REASONS, DOCUMENTATION_PROVIDER_STATES, DOCUMENTATION_RESULT_PROVIDERS, DOCUMENTATION_SEARCH_PROVIDERS, DOCUMENTATION_SEARCH_SERVICE_KEY, DOCUMENTATION_WARNING_CODES,
|
|
4
|
+
export { DOCUMENTATION_CACHE_PATH_STATES, DOCUMENTATION_CACHE_SKIP_REASONS, DOCUMENTATION_PROVIDER_STATES, DOCUMENTATION_RESULT_PROVIDERS, DOCUMENTATION_SEARCH_PROVIDERS, DOCUMENTATION_SEARCH_SERVICE_KEY, DOCUMENTATION_WARNING_CODES, DocumentationSearchInfrastructureError, DocumentationSearchService, } from './service.js';
|
|
5
5
|
//# sourceMappingURL=index.js.map
|
|
@@ -3,7 +3,7 @@ import type { Config } from '../config.js';
|
|
|
3
3
|
import type { CanonicalSource, SourceRecordCandidate } from '../contracts/index.js';
|
|
4
4
|
import { type ProviderAttemptRecord, type ProviderErrorKind } from '../provider-runtime/index.js';
|
|
5
5
|
import { type Context7RemoteClient, type Context7Library } from '../providers/context7.js';
|
|
6
|
-
import type { BoundedSourceProvider, DocumentationSnippet
|
|
6
|
+
import type { BoundedSourceProvider, DocumentationSnippet } from '../providers/types.js';
|
|
7
7
|
import type { Context7DocsCacheEntry, Context7DocRef } from './cache-domain.js';
|
|
8
8
|
import { type Context7CachedDocLookup, type Context7CachedDocMatch } from './cache.js';
|
|
9
9
|
import { Context7CachedOperations, type Context7OperationPath } from './context7-cache.js';
|
|
@@ -43,7 +43,10 @@ export interface DocumentationWarning {
|
|
|
43
43
|
export interface DocumentationSearchInput {
|
|
44
44
|
readonly query: string;
|
|
45
45
|
readonly provider?: DocumentationSearchProvider;
|
|
46
|
+
/** Exact Context7 id; when present it takes priority and bypasses resolve. */
|
|
46
47
|
readonly libraryId?: string;
|
|
48
|
+
/** Explicit Context7 package/product identity; never inferred from query. */
|
|
49
|
+
readonly libraryName?: string;
|
|
47
50
|
readonly maxResults: number;
|
|
48
51
|
readonly forceRefresh?: boolean;
|
|
49
52
|
readonly signal: AbortSignal;
|
|
@@ -138,8 +141,8 @@ declare module '@deepseek-ai/cordis' {
|
|
|
138
141
|
}
|
|
139
142
|
}
|
|
140
143
|
/**
|
|
141
|
-
* Lifecycle-bound documentation core shared by the high-level docs Consumer
|
|
142
|
-
*
|
|
144
|
+
* Lifecycle-bound documentation core shared by the high-level docs Consumer
|
|
145
|
+
* and the deferred granular Context7 Consumers.
|
|
143
146
|
*/
|
|
144
147
|
export declare class DocumentationSearchService extends Service {
|
|
145
148
|
private readonly context7;
|
|
@@ -167,13 +170,4 @@ export declare class DocumentationSearchService extends Service {
|
|
|
167
170
|
private runContext7;
|
|
168
171
|
private runExa;
|
|
169
172
|
}
|
|
170
|
-
/** Context7-only adapter used by web_search while sharing the high-level service/cache. */
|
|
171
|
-
export declare class DocumentationContext7Provider implements BoundedSourceProvider {
|
|
172
|
-
private readonly documentation;
|
|
173
|
-
readonly capability: "docs_search";
|
|
174
|
-
readonly provider: "context7";
|
|
175
|
-
constructor(documentation: DocumentationSearchService);
|
|
176
|
-
configured(): Promise<boolean>;
|
|
177
|
-
search(input: SourceProviderSearchInput): Promise<SourceProviderSearchOutcome>;
|
|
178
|
-
}
|
|
179
173
|
//# sourceMappingURL=service.d.ts.map
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Service } from '@deepseek-ai/cordis';
|
|
2
2
|
import { createProviderAttemptRecord, isAbortError, isProviderError, ProviderError, throwIfAborted, } from '../provider-runtime/index.js';
|
|
3
|
-
import { boundSourceProviderResult } from '../providers/bounded-result.js';
|
|
4
3
|
import { context7LibrarySource, selectContext7Library, } from '../providers/context7.js';
|
|
5
4
|
import { applySourceQuality } from '../search/index.js';
|
|
6
5
|
import { CONTEXT7_CACHE_QUERY_MAX_SCAN_RECORDS, } from './cache.js';
|
|
@@ -190,17 +189,34 @@ function validateInput(input, config) {
|
|
|
190
189
|
provider: 'documentation-search',
|
|
191
190
|
});
|
|
192
191
|
}
|
|
193
|
-
|
|
192
|
+
let libraryId;
|
|
193
|
+
if (provider !== 'exa' && input.libraryId !== undefined) {
|
|
194
|
+
if (!isContext7LibraryId(input.libraryId)) {
|
|
195
|
+
throw new ProviderError({
|
|
196
|
+
capability: 'docs_search',
|
|
197
|
+
kind: 'invalid_request',
|
|
198
|
+
provider: 'context7',
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
libraryId = normalizeContext7LibraryId(input.libraryId);
|
|
202
|
+
}
|
|
203
|
+
const libraryName = provider !== 'exa'
|
|
204
|
+
&& libraryId === undefined
|
|
205
|
+
&& input.libraryName !== undefined
|
|
206
|
+
? context7Text(input.libraryName, config)
|
|
207
|
+
: undefined;
|
|
208
|
+
if ((provider === 'context7' || provider === 'all')
|
|
209
|
+
&& libraryId === undefined
|
|
210
|
+
&& libraryName === undefined) {
|
|
194
211
|
throw new ProviderError({
|
|
195
212
|
capability: 'docs_search',
|
|
196
213
|
kind: 'invalid_request',
|
|
197
|
-
provider: 'context7',
|
|
214
|
+
provider: 'context7-library-name-or-id-required',
|
|
198
215
|
});
|
|
199
216
|
}
|
|
200
217
|
return {
|
|
201
|
-
...(
|
|
202
|
-
|
|
203
|
-
: { libraryId: normalizeContext7LibraryId(input.libraryId) }),
|
|
218
|
+
...(libraryId === undefined ? {} : { libraryId }),
|
|
219
|
+
...(libraryName === undefined ? {} : { libraryName }),
|
|
204
220
|
provider,
|
|
205
221
|
query,
|
|
206
222
|
};
|
|
@@ -239,8 +255,8 @@ function frozenPathOutcome(outcome) {
|
|
|
239
255
|
});
|
|
240
256
|
}
|
|
241
257
|
/**
|
|
242
|
-
* Lifecycle-bound documentation core shared by the high-level docs Consumer
|
|
243
|
-
*
|
|
258
|
+
* Lifecycle-bound documentation core shared by the high-level docs Consumer
|
|
259
|
+
* and the deferred granular Context7 Consumers.
|
|
244
260
|
*/
|
|
245
261
|
export class DocumentationSearchService extends Service {
|
|
246
262
|
context7;
|
|
@@ -361,11 +377,13 @@ export class DocumentationSearchService extends Service {
|
|
|
361
377
|
dispatches += 1;
|
|
362
378
|
input.onDispatch?.();
|
|
363
379
|
},
|
|
364
|
-
|
|
380
|
+
libraryName: input.libraryName,
|
|
381
|
+
query: input.query,
|
|
365
382
|
signal: input.signal,
|
|
366
383
|
}),
|
|
367
384
|
maxResults: candidateLimit,
|
|
368
|
-
|
|
385
|
+
libraryName: input.libraryName,
|
|
386
|
+
query: input.query,
|
|
369
387
|
signal: input.signal,
|
|
370
388
|
});
|
|
371
389
|
const candidates = Object.freeze(resolved.entry.libraries.slice(0, input.maxResults));
|
|
@@ -419,8 +437,12 @@ export class DocumentationSearchService extends Service {
|
|
|
419
437
|
const config = input.config ?? this.getConfig();
|
|
420
438
|
const validated = validateInput(input, config);
|
|
421
439
|
const forceRefresh = input.forceRefresh === true;
|
|
422
|
-
const
|
|
423
|
-
const
|
|
440
|
+
const hasContext7Identity = validated.libraryId !== undefined || validated.libraryName !== undefined;
|
|
441
|
+
const runContext7 = validated.provider === 'context7'
|
|
442
|
+
|| validated.provider === 'all'
|
|
443
|
+
|| (validated.provider === 'auto' && hasContext7Identity);
|
|
444
|
+
const autoExa = validated.provider === 'auto'
|
|
445
|
+
&& (!hasContext7Identity || config.fallbackMode === 'auto');
|
|
424
446
|
const considerExa = validated.provider === 'exa' || validated.provider === 'all' || autoExa;
|
|
425
447
|
let exaConfigured = false;
|
|
426
448
|
let exaProbeError;
|
|
@@ -440,6 +462,7 @@ export class DocumentationSearchService extends Service {
|
|
|
440
462
|
config,
|
|
441
463
|
forceRefresh,
|
|
442
464
|
...(validated.libraryId === undefined ? {} : { libraryId: validated.libraryId }),
|
|
465
|
+
...(validated.libraryName === undefined ? {} : { libraryName: validated.libraryName }),
|
|
443
466
|
maxResults: input.maxResults,
|
|
444
467
|
query: validated.query,
|
|
445
468
|
signal,
|
|
@@ -633,12 +656,19 @@ export class DocumentationSearchService extends Service {
|
|
|
633
656
|
attempts.push(skippedAttempt('context7-resolve'));
|
|
634
657
|
}
|
|
635
658
|
else {
|
|
659
|
+
if (input.libraryName === undefined) {
|
|
660
|
+
throw new ProviderError({
|
|
661
|
+
capability: 'docs_search',
|
|
662
|
+
kind: 'invalid_request',
|
|
663
|
+
provider: 'context7-library-name-or-id-required',
|
|
664
|
+
});
|
|
665
|
+
}
|
|
636
666
|
const startedAt = safeClockValue(this.now);
|
|
637
667
|
try {
|
|
638
668
|
const resolved = await this.executeContext7Resolve({
|
|
639
669
|
config: input.config,
|
|
640
670
|
forceRefresh: input.forceRefresh,
|
|
641
|
-
libraryName: input.
|
|
671
|
+
libraryName: input.libraryName,
|
|
642
672
|
maxResults: input.maxResults,
|
|
643
673
|
candidateLimit: Math.min(CONTEXT7_RESOLVE_CANDIDATE_LIMIT, input.config.retention.providerMaxSources),
|
|
644
674
|
onDispatch: () => { resolveDispatches += 1; },
|
|
@@ -838,65 +868,4 @@ export class DocumentationSearchService extends Service {
|
|
|
838
868
|
}
|
|
839
869
|
}
|
|
840
870
|
}
|
|
841
|
-
/** Context7-only adapter used by web_search while sharing the high-level service/cache. */
|
|
842
|
-
export class DocumentationContext7Provider {
|
|
843
|
-
documentation;
|
|
844
|
-
capability = 'docs_search';
|
|
845
|
-
provider = 'context7';
|
|
846
|
-
constructor(documentation) {
|
|
847
|
-
this.documentation = documentation;
|
|
848
|
-
}
|
|
849
|
-
async configured() {
|
|
850
|
-
return true;
|
|
851
|
-
}
|
|
852
|
-
async search(input) {
|
|
853
|
-
let result;
|
|
854
|
-
try {
|
|
855
|
-
result = await this.documentation.search({
|
|
856
|
-
config: input.config,
|
|
857
|
-
forceRefresh: false,
|
|
858
|
-
maxResults: input.limit,
|
|
859
|
-
provider: 'context7',
|
|
860
|
-
query: input.query,
|
|
861
|
-
signal: input.signal,
|
|
862
|
-
});
|
|
863
|
-
}
|
|
864
|
-
catch (error) {
|
|
865
|
-
if (error instanceof DocumentationSearchInfrastructureError && error.cause !== undefined) {
|
|
866
|
-
throw error.cause;
|
|
867
|
-
}
|
|
868
|
-
throw error;
|
|
869
|
-
}
|
|
870
|
-
const warnings = result.warnings.flatMap(warning => {
|
|
871
|
-
if (warning.code !== 'cache_stale'
|
|
872
|
-
&& warning.code !== 'cache_evicted'
|
|
873
|
-
&& warning.code !== 'provider_failed')
|
|
874
|
-
return [];
|
|
875
|
-
return [Object.freeze({
|
|
876
|
-
code: warning.code,
|
|
877
|
-
...(warning.errorKind === undefined ? {} : { errorKind: warning.errorKind }),
|
|
878
|
-
provider: warning.provider ?? 'context7',
|
|
879
|
-
})];
|
|
880
|
-
});
|
|
881
|
-
const attempts = result.attempts
|
|
882
|
-
.filter(attempt => !attempt.provider.startsWith('context7-cache-'))
|
|
883
|
-
.reduce((sum, attempt) => sum + attempt.attempts, 0);
|
|
884
|
-
return Object.freeze({
|
|
885
|
-
attempts: Math.max(1, attempts),
|
|
886
|
-
result: boundSourceProviderResult({
|
|
887
|
-
capability: 'docs_search',
|
|
888
|
-
config: input.config,
|
|
889
|
-
inputTruncated: result.truncated,
|
|
890
|
-
provider: 'context7',
|
|
891
|
-
requestedSources: input.limit,
|
|
892
|
-
responseBytes: result.providerResponseBytes,
|
|
893
|
-
snippets: result.snippets,
|
|
894
|
-
sources: result.sources,
|
|
895
|
-
}),
|
|
896
|
-
state: 'complete',
|
|
897
|
-
totalDelayMs: 0,
|
|
898
|
-
...(warnings.length === 0 ? {} : { warnings: Object.freeze(warnings) }),
|
|
899
|
-
});
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
871
|
//# sourceMappingURL=service.js.map
|
package/lib/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Context7ResolveDiagnosticProbe, SearchApiModelListDiagnosticProbe, SearchDiagnostics, SourceSearchDiagnosticProbe, } from './diagnostics/index.js';
|
|
2
2
|
import { Config as SearchEnhanceConfig, SEARCH_ENHANCE_SETTINGS_NAMESPACE, } from './config.js';
|
|
3
|
-
import { Context7CachedOperations,
|
|
3
|
+
import { Context7CachedOperations, DocumentationSearchService, PersistentContext7Cache, } from './documentation/index.js';
|
|
4
4
|
import { SearchOrchestrator } from './orchestration/index.js';
|
|
5
5
|
import { registerToolDiscoveryGuidance } from './prompt/tool-discovery.js';
|
|
6
6
|
import { Context7RemoteClient } from './providers/context7.js';
|
|
@@ -82,7 +82,6 @@ export async function apply(ctx, config) {
|
|
|
82
82
|
],
|
|
83
83
|
});
|
|
84
84
|
const orchestrator = new SearchOrchestrator({
|
|
85
|
-
context7: new DocumentationContext7Provider(documentation),
|
|
86
85
|
exa,
|
|
87
86
|
firecrawl,
|
|
88
87
|
getConfig,
|
|
@@ -16,7 +16,6 @@ export declare function mergeCanonicalSources(...groups: ReadonlyArray<readonly
|
|
|
16
16
|
export declare class SearchOrchestrator {
|
|
17
17
|
private readonly getConfig;
|
|
18
18
|
private readonly mainSearch;
|
|
19
|
-
private readonly context7;
|
|
20
19
|
private readonly exa;
|
|
21
20
|
private readonly tavily;
|
|
22
21
|
private readonly firecrawl;
|
|
@@ -294,7 +294,6 @@ function sourceAttempt(slot, mainFailed) {
|
|
|
294
294
|
export class SearchOrchestrator {
|
|
295
295
|
getConfig;
|
|
296
296
|
mainSearch;
|
|
297
|
-
context7;
|
|
298
297
|
exa;
|
|
299
298
|
tavily;
|
|
300
299
|
firecrawl;
|
|
@@ -302,7 +301,6 @@ export class SearchOrchestrator {
|
|
|
302
301
|
constructor(dependencies) {
|
|
303
302
|
this.getConfig = dependencies.getConfig;
|
|
304
303
|
this.mainSearch = dependencies.mainSearch;
|
|
305
|
-
this.context7 = dependencies.context7;
|
|
306
304
|
this.exa = dependencies.exa;
|
|
307
305
|
this.tavily = dependencies.tavily;
|
|
308
306
|
this.firecrawl = dependencies.firecrawl;
|
|
@@ -347,17 +345,6 @@ export class SearchOrchestrator {
|
|
|
347
345
|
const relayAbort = () => fanout.abort(signal.reason);
|
|
348
346
|
signal.addEventListener('abort', relayAbort, { once: true });
|
|
349
347
|
const slots = [
|
|
350
|
-
{
|
|
351
|
-
available: false,
|
|
352
|
-
capability: 'docs_search',
|
|
353
|
-
key: 'context7',
|
|
354
|
-
limit: 0,
|
|
355
|
-
planningDurationMs: 0,
|
|
356
|
-
planningError: undefined,
|
|
357
|
-
provider: this.context7,
|
|
358
|
-
skipReason: undefined,
|
|
359
|
-
track: undefined,
|
|
360
|
-
},
|
|
361
348
|
{
|
|
362
349
|
available: false,
|
|
363
350
|
capability: 'docs_search',
|
|
@@ -438,8 +425,8 @@ export class SearchOrchestrator {
|
|
|
438
425
|
}
|
|
439
426
|
throwIfAborted(signal);
|
|
440
427
|
throwIfAborted(fanout.signal);
|
|
441
|
-
const tavilySlot = slots[
|
|
442
|
-
const firecrawlSlot = slots[
|
|
428
|
+
const tavilySlot = slots[1];
|
|
429
|
+
const firecrawlSlot = slots[2];
|
|
443
430
|
if (tavilySlot === undefined || firecrawlSlot === undefined) {
|
|
444
431
|
throw new Error('discovery slots are incomplete');
|
|
445
432
|
}
|
|
@@ -65,7 +65,6 @@ export interface MainSearchProvider {
|
|
|
65
65
|
export interface SearchOrchestratorDependencies {
|
|
66
66
|
readonly getConfig: () => Config;
|
|
67
67
|
readonly mainSearch: MainSearchProvider;
|
|
68
|
-
readonly context7: BoundedSourceProvider;
|
|
69
68
|
readonly exa: BoundedSourceProvider;
|
|
70
69
|
readonly tavily: BoundedSourceProvider;
|
|
71
70
|
readonly firecrawl: BoundedSourceProvider;
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { DocsSearchOutput, WebSearchOutput, SearchDiagnosticsOutput, SearchSourcesOutput, WebExtractOutput, WebMapOutput } from '../tools/schemas.js';
|
|
2
|
+
export declare function sourceDisplayLabel(source: {
|
|
3
|
+
readonly title?: string;
|
|
4
|
+
readonly url: string;
|
|
5
|
+
}): string;
|
|
2
6
|
/**
|
|
3
7
|
* Pure Native projection in the product-defined order. The operation-selected
|
|
4
8
|
* limit is carried in the canonical value, so replay never consults Settings,
|
|
@@ -3,6 +3,11 @@ const DISCOVERY_NOTICE = 'Evidence level: discovery. Snippets are discovery meta
|
|
|
3
3
|
function inline(value) {
|
|
4
4
|
return value.replace(/\s+/gu, ' ').trim();
|
|
5
5
|
}
|
|
6
|
+
export function sourceDisplayLabel(source) {
|
|
7
|
+
if (source.title !== undefined && source.title.trim().length > 0)
|
|
8
|
+
return source.title;
|
|
9
|
+
return new URL(source.url).hostname;
|
|
10
|
+
}
|
|
6
11
|
function renderWithBoundedNotice(text, notice, maximumBytes, fallbackNotice) {
|
|
7
12
|
if (notice === undefined || notice.length === 0) {
|
|
8
13
|
return truncateUtf8(text, maximumBytes).text;
|
|
@@ -77,7 +82,7 @@ function sourceSection(value) {
|
|
|
77
82
|
const source = value.sources[index];
|
|
78
83
|
if (source === undefined)
|
|
79
84
|
continue;
|
|
80
|
-
lines.push(`${index + 1}. ${inline(source
|
|
85
|
+
lines.push(`${index + 1}. ${inline(sourceDisplayLabel(source))}`);
|
|
81
86
|
lines.push(` URL: ${source.url}`);
|
|
82
87
|
if (source.publishedAt !== undefined) {
|
|
83
88
|
lines.push(` Date: ${inline(source.publishedAt)}`);
|
|
@@ -190,7 +195,7 @@ function docsSourceSection(value) {
|
|
|
190
195
|
const source = value.sources[index];
|
|
191
196
|
if (source === undefined)
|
|
192
197
|
continue;
|
|
193
|
-
lines.push(`${index + 1}. ${inline(source
|
|
198
|
+
lines.push(`${index + 1}. ${inline(sourceDisplayLabel(source))}`);
|
|
194
199
|
lines.push(` URL: ${source.url}`);
|
|
195
200
|
if (source.publishedAt !== undefined)
|
|
196
201
|
lines.push(` Date: ${inline(source.publishedAt)}`);
|
|
@@ -247,7 +252,7 @@ function renderPageSources(page) {
|
|
|
247
252
|
const source = page.sources[index];
|
|
248
253
|
if (source === undefined)
|
|
249
254
|
continue;
|
|
250
|
-
lines.push(`${page.offset + index + 1}. ${inline(source
|
|
255
|
+
lines.push(`${page.offset + index + 1}. ${inline(sourceDisplayLabel(source))}`);
|
|
251
256
|
lines.push(` URL: ${source.url}`);
|
|
252
257
|
lines.push(` Category: ${source.category}`);
|
|
253
258
|
if (source.date !== undefined)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export const TOOL_DISCOVERY_GUIDANCE = [
|
|
2
2
|
'Search Enhance keeps a fixed model-facing surface: web_search, docs_search, web_extract, search_tools, and search_call.',
|
|
3
|
+
'For docs_search, pass library_id when an exact /org/project id is known, or pass library_name directly when the package or product is known; for example docs_search({ query: "JWT authentication middleware API", library_name: "FastAPI" }). Do not activate granular Context7 merely to resolve first.',
|
|
4
|
+
'For broad, cross-project, or unknown-library documentation discovery, use docs_search with provider: "auto" and no library identity; this uses Exa instead of guessing a Context7 library.',
|
|
3
5
|
'Use search_tools only when the resident search tools cannot complete the task. It returns append-only capability and operation manifests; do not activate every capability preemptively.',
|
|
4
6
|
'Run a manifested deferred operation with search_call({ operation, arguments }). In progressive mode, a newly disclosed capability is callable on the next model step; in all mode, deferred operations are active immediately. search_call fails closed while an operation is inactive.',
|
|
5
7
|
'Activate planning only for explicit deep research, multi-source verification, or complex comparison. Activate diagnostics only when the user asks about Provider configuration or connectivity.',
|
|
@@ -3,10 +3,14 @@ import type { CanonicalSource } from '../contracts/index.js';
|
|
|
3
3
|
import type { CachedContext7Library } from '../documentation/cache-domain.js';
|
|
4
4
|
import { type Context7DocsRemoteResult, type Context7ResolveRemoteResult } from '../documentation/context7-cache.js';
|
|
5
5
|
import { type ProviderHttpDependencies } from './http.js';
|
|
6
|
-
import type {
|
|
6
|
+
import type { DocumentationSnippet, SourceProviderSearchInput } from './types.js';
|
|
7
7
|
export interface Context7ProviderDependencies extends ProviderHttpDependencies {
|
|
8
8
|
readonly credentials: Pick<CredentialProvider, 'describe' | 'resolve'>;
|
|
9
9
|
}
|
|
10
|
+
/** Explicit Context7 library identity and independent documentation task. */
|
|
11
|
+
export interface Context7ResolveRemoteInput extends SourceProviderSearchInput {
|
|
12
|
+
readonly libraryName: string;
|
|
13
|
+
}
|
|
10
14
|
export type Context7Library = CachedContext7Library;
|
|
11
15
|
export interface ParsedContext7Libraries {
|
|
12
16
|
readonly libraries: readonly Context7Library[];
|
|
@@ -22,7 +26,7 @@ export interface ParsedContext7Snippets {
|
|
|
22
26
|
export declare function parseContext7LibraryResponse(body: string, maximumItems: number, maximumTextCharacters: number): Readonly<ParsedContext7Libraries>;
|
|
23
27
|
/** Compatibility parser with fixed defensive ceilings. */
|
|
24
28
|
export declare function parseContext7Libraries(body: string): readonly Context7Library[];
|
|
25
|
-
/** Stable selection by
|
|
29
|
+
/** Stable selection led by explicit library identity, then bounded secondary signals. */
|
|
26
30
|
export declare function selectContext7Library(libraries: readonly Context7Library[], libraryName: string, query?: string): Context7Library | undefined;
|
|
27
31
|
/** Parse JSON or plain-text Context7 v2 documentation bodies exactly once and bound every snippet. */
|
|
28
32
|
export declare function parseContext7SnippetResponse(body: string, maximumItems: number, maximumCharacters: number): Readonly<ParsedContext7Snippets>;
|
|
@@ -39,18 +43,9 @@ export declare class Context7RemoteClient {
|
|
|
39
43
|
private readonly credentials;
|
|
40
44
|
private readonly http;
|
|
41
45
|
constructor(dependencies: Context7ProviderDependencies);
|
|
42
|
-
resolve(input:
|
|
46
|
+
resolve(input: Context7ResolveRemoteInput): Promise<Readonly<Context7ResolveRemoteResult>>;
|
|
43
47
|
docs(input: SourceProviderSearchInput & {
|
|
44
48
|
readonly libraryId: string;
|
|
45
49
|
}): Promise<Readonly<Context7DocsRemoteResult>>;
|
|
46
50
|
}
|
|
47
|
-
/** Direct resolve-then-docs adapter retained for registration-free Provider use. */
|
|
48
|
-
export declare class Context7Provider implements BoundedSourceProvider {
|
|
49
|
-
readonly capability = "docs_search";
|
|
50
|
-
readonly provider = "context7";
|
|
51
|
-
private readonly remote;
|
|
52
|
-
constructor(dependencies: Context7ProviderDependencies);
|
|
53
|
-
configured(): Promise<boolean>;
|
|
54
|
-
search(input: SourceProviderSearchInput): Promise<SourceProviderSearchOutcome>;
|
|
55
|
-
}
|
|
56
51
|
//# sourceMappingURL=context7.d.ts.map
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { isContext7LibraryId, normalizeContext7LibraryId, } from '../documentation/context7-cache.js';
|
|
2
2
|
import { ProviderError, truncateCharacters } from '../provider-runtime/index.js';
|
|
3
|
-
import { boundSourceProviderResult } from './bounded-result.js';
|
|
4
3
|
import { canonicalHttpUrl, firstString, isRecord, nonEmptyQuery, positiveLimit, providerEndpoint, resolveOptionalCredential, } from './helpers.js';
|
|
5
4
|
import { ProviderHttpClient } from './http.js';
|
|
6
5
|
const PROVIDER = 'context7';
|
|
@@ -127,12 +126,6 @@ function normalizedTerms(value) {
|
|
|
127
126
|
.map(normalizedMatchText)
|
|
128
127
|
.filter(term => term.length > 0);
|
|
129
128
|
}
|
|
130
|
-
function queryMentionsName(queryTerms, value) {
|
|
131
|
-
const nameTerms = normalizedTerms(value ?? '');
|
|
132
|
-
if (nameTerms.length === 0)
|
|
133
|
-
return false;
|
|
134
|
-
return queryTerms.some((_term, index) => nameTerms.every((nameTerm, offset) => queryTerms[index + offset] === nameTerm));
|
|
135
|
-
}
|
|
136
129
|
function context7LibraryScore(item, libraryName, queryTerms) {
|
|
137
130
|
const wanted = normalizedMatchText(libraryName);
|
|
138
131
|
const rawText = `${item.id ?? ''} ${item.title ?? ''} ${item.description ?? ''}`.toLowerCase();
|
|
@@ -140,46 +133,42 @@ function context7LibraryScore(item, libraryName, queryTerms) {
|
|
|
140
133
|
const description = normalizedMatchText(item.description);
|
|
141
134
|
const idSegments = (item.id ?? '').split('/').filter(Boolean).map(normalizedMatchText);
|
|
142
135
|
let score = 0;
|
|
143
|
-
if (queryMentionsName(queryTerms, item.title))
|
|
144
|
-
score += 240;
|
|
145
|
-
for (const segment of (item.id ?? '').split('/').filter(Boolean)) {
|
|
146
|
-
if (queryMentionsName(queryTerms, segment))
|
|
147
|
-
score += 220;
|
|
148
|
-
}
|
|
149
|
-
if (/official.*documentation|documentation.*official|official docs/.test(rawText))
|
|
150
|
-
score += 260;
|
|
151
136
|
if (wanted.length > 0) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
137
|
+
const titleIdentity = title === wanted
|
|
138
|
+
? 1000
|
|
139
|
+
: title.startsWith(wanted)
|
|
140
|
+
? 200
|
|
141
|
+
: title.includes(wanted)
|
|
142
|
+
? 100
|
|
143
|
+
: 0;
|
|
144
|
+
const idIdentity = idSegments.some(segment => segment === wanted)
|
|
145
|
+
? 900 + (idSegments.at(-1) === wanted ? 100 : 0)
|
|
146
|
+
: 0;
|
|
147
|
+
score += Math.max(titleIdentity, idIdentity);
|
|
162
148
|
if (description.includes(wanted))
|
|
163
|
-
score +=
|
|
149
|
+
score += 15;
|
|
164
150
|
}
|
|
165
|
-
|
|
166
|
-
|
|
151
|
+
let queryRelevance = 0;
|
|
152
|
+
for (const term of queryTerms) {
|
|
153
|
+
if (term.length <= 1)
|
|
167
154
|
continue;
|
|
168
|
-
if (title ===
|
|
169
|
-
|
|
170
|
-
else if (title.includes(
|
|
171
|
-
|
|
172
|
-
if (idSegments.some(segment => segment ===
|
|
173
|
-
|
|
174
|
-
if (description.includes(
|
|
175
|
-
|
|
155
|
+
if (title === term)
|
|
156
|
+
queryRelevance += 3;
|
|
157
|
+
else if (title.includes(term))
|
|
158
|
+
queryRelevance += 1;
|
|
159
|
+
if (idSegments.some(segment => segment === term))
|
|
160
|
+
queryRelevance += 2;
|
|
161
|
+
if (description.includes(term))
|
|
162
|
+
queryRelevance += 1;
|
|
176
163
|
}
|
|
164
|
+
score += Math.min(30, queryRelevance);
|
|
177
165
|
if (/official|documentation|docs/.test(rawText))
|
|
178
166
|
score += 10;
|
|
179
167
|
if (item.trustScore !== undefined)
|
|
180
|
-
score += item.trustScore * 2;
|
|
181
|
-
if (item.benchmarkScore !== undefined)
|
|
182
|
-
score += item.benchmarkScore / 2;
|
|
168
|
+
score += Math.min(20, Math.max(0, item.trustScore) * 2);
|
|
169
|
+
if (item.benchmarkScore !== undefined) {
|
|
170
|
+
score += Math.min(50, Math.max(0, item.benchmarkScore) / 2);
|
|
171
|
+
}
|
|
183
172
|
if (item.totalSnippets !== undefined) {
|
|
184
173
|
score += Math.min(30, Math.log10(Math.max(1, item.totalSnippets)) * 8);
|
|
185
174
|
}
|
|
@@ -187,7 +176,7 @@ function context7LibraryScore(item, libraryName, queryTerms) {
|
|
|
187
176
|
score += Math.min(20, Math.log10(Math.max(1, item.stars)) * 4);
|
|
188
177
|
return score;
|
|
189
178
|
}
|
|
190
|
-
/** Stable selection by
|
|
179
|
+
/** Stable selection led by explicit library identity, then bounded secondary signals. */
|
|
191
180
|
export function selectContext7Library(libraries, libraryName, query = libraryName) {
|
|
192
181
|
const queryTerms = normalizedTerms(query);
|
|
193
182
|
return libraries
|
|
@@ -304,12 +293,14 @@ export class Context7RemoteClient {
|
|
|
304
293
|
this.http = new ProviderHttpClient(dependencies);
|
|
305
294
|
}
|
|
306
295
|
async resolve(input) {
|
|
296
|
+
const libraryName = nonEmptyQuery(input.libraryName, PROVIDER, CAPABILITY);
|
|
307
297
|
const query = nonEmptyQuery(input.query, PROVIDER, CAPABILITY);
|
|
308
298
|
const limit = positiveLimit(input.limit, PROVIDER, CAPABILITY);
|
|
309
299
|
const providerConfig = input.config.providers.context7;
|
|
310
300
|
const credential = await resolveOptionalCredential(this.credentials, providerConfig.credentialRef, input.signal, PROVIDER, CAPABILITY);
|
|
311
|
-
const resolveUrl = new URL(providerEndpoint(providerConfig.baseUrl, '/api/v2/search'));
|
|
301
|
+
const resolveUrl = new URL(providerEndpoint(providerConfig.baseUrl, '/api/v2/libs/search'));
|
|
312
302
|
resolveUrl.searchParams.set('query', query);
|
|
303
|
+
resolveUrl.searchParams.set('libraryName', libraryName);
|
|
313
304
|
const response = await this.http.requestText({
|
|
314
305
|
capability: CAPABILITY,
|
|
315
306
|
endpoint: resolveUrl.href,
|
|
@@ -362,60 +353,4 @@ export class Context7RemoteClient {
|
|
|
362
353
|
});
|
|
363
354
|
}
|
|
364
355
|
}
|
|
365
|
-
/** Direct resolve-then-docs adapter retained for registration-free Provider use. */
|
|
366
|
-
export class Context7Provider {
|
|
367
|
-
capability = CAPABILITY;
|
|
368
|
-
provider = PROVIDER;
|
|
369
|
-
remote;
|
|
370
|
-
constructor(dependencies) {
|
|
371
|
-
this.remote = new Context7RemoteClient(dependencies);
|
|
372
|
-
}
|
|
373
|
-
async configured() {
|
|
374
|
-
return true;
|
|
375
|
-
}
|
|
376
|
-
async search(input) {
|
|
377
|
-
const query = nonEmptyQuery(input.query, PROVIDER, CAPABILITY);
|
|
378
|
-
const limit = positiveLimit(input.limit, PROVIDER, CAPABILITY);
|
|
379
|
-
const resolved = await this.remote.resolve({ ...input, query, limit });
|
|
380
|
-
const selected = selectContext7Library(resolved.libraries, query, query);
|
|
381
|
-
if (selected?.id === undefined || !isContext7LibraryId(selected.id)) {
|
|
382
|
-
return Object.freeze({
|
|
383
|
-
attempts: resolved.attempts,
|
|
384
|
-
result: boundSourceProviderResult({
|
|
385
|
-
capability: CAPABILITY,
|
|
386
|
-
config: input.config,
|
|
387
|
-
inputTruncated: resolved.truncated,
|
|
388
|
-
provider: PROVIDER,
|
|
389
|
-
requestedSources: limit,
|
|
390
|
-
responseBytes: resolved.responseBytes,
|
|
391
|
-
sources: [],
|
|
392
|
-
}),
|
|
393
|
-
state: 'complete',
|
|
394
|
-
totalDelayMs: resolved.totalDelayMs,
|
|
395
|
-
});
|
|
396
|
-
}
|
|
397
|
-
const libraryId = normalizeContext7LibraryId(selected.id);
|
|
398
|
-
const docs = await this.remote.docs({ ...input, libraryId, query, limit });
|
|
399
|
-
const snippets = docs.snippets.map(snippet => Object.freeze({
|
|
400
|
-
...snippet,
|
|
401
|
-
libraryId,
|
|
402
|
-
}));
|
|
403
|
-
const source = context7LibrarySource(selected, input.config.providers.context7.baseUrl, input.config.webExtract.maxUrlCharacters, snippets[0]?.content ?? selected.description);
|
|
404
|
-
return Object.freeze({
|
|
405
|
-
attempts: resolved.attempts + docs.attempts,
|
|
406
|
-
result: boundSourceProviderResult({
|
|
407
|
-
capability: CAPABILITY,
|
|
408
|
-
config: input.config,
|
|
409
|
-
inputTruncated: resolved.truncated || docs.truncated,
|
|
410
|
-
provider: PROVIDER,
|
|
411
|
-
requestedSources: limit,
|
|
412
|
-
responseBytes: resolved.responseBytes + docs.responseBytes,
|
|
413
|
-
snippets,
|
|
414
|
-
sources: source === undefined ? [] : [source],
|
|
415
|
-
}),
|
|
416
|
-
state: 'complete',
|
|
417
|
-
totalDelayMs: resolved.totalDelayMs + docs.totalDelayMs,
|
|
418
|
-
});
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
356
|
//# sourceMappingURL=context7.js.map
|
package/lib/providers/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { boundSourceProviderResult, type BoundSourceProviderResultInput, } from './bounded-result.js';
|
|
2
|
-
export {
|
|
2
|
+
export { Context7RemoteClient, context7LibrarySource, context7LibraryUrl, parseContext7Libraries, parseContext7LibraryResponse, parseContext7Snippets, parseContext7SnippetResponse, selectContext7Library, type Context7Library, type Context7ProviderDependencies, type Context7ResolveRemoteInput, type ParsedContext7Libraries, type ParsedContext7Snippets, } from './context7.js';
|
|
3
3
|
export { SmartDirectAdapter, SmartDirectProvider, type SmartDirectProviderDependencies, } from './smart-direct.js';
|
|
4
4
|
export { SMART_DIRECT_BROWSER_PROFILES, SMART_DIRECT_OPERATING_SYSTEMS, type SmartDirectBrowserProfile, type SmartDirectOperatingSystem, } from './smart-direct-profiles.js';
|
|
5
5
|
export { DirectFetchAdapter, DirectFetchProvider, type DirectFetchProviderDependencies, } from './direct-fetch.js';
|
package/lib/providers/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { boundSourceProviderResult, } from './bounded-result.js';
|
|
2
|
-
export {
|
|
2
|
+
export { Context7RemoteClient, context7LibrarySource, context7LibraryUrl, parseContext7Libraries, parseContext7LibraryResponse, parseContext7Snippets, parseContext7SnippetResponse, selectContext7Library, } from './context7.js';
|
|
3
3
|
export { SmartDirectAdapter, SmartDirectProvider, } from './smart-direct.js';
|
|
4
4
|
export { SMART_DIRECT_BROWSER_PROFILES, SMART_DIRECT_OPERATING_SYSTEMS, } from './smart-direct-profiles.js';
|
|
5
5
|
export { DirectFetchAdapter, DirectFetchProvider, } from './direct-fetch.js';
|
package/lib/search/parse.d.ts
CHANGED
|
@@ -31,7 +31,7 @@ export interface ParsedSearchApiResponse {
|
|
|
31
31
|
}
|
|
32
32
|
/** Extract URL-validated inline Markdown citations in answer order for internal orchestration. */
|
|
33
33
|
export declare function extractMarkdownCitationUrls(text: string, maximumUrlCharacters?: number): readonly string[];
|
|
34
|
-
/** Convert Search API prose/source conventions
|
|
34
|
+
/** Convert Search API prose/source conventions with one bounded link scan and exact-URL enrichment. */
|
|
35
35
|
export declare function parseSearchAnswerText(text: string, limitOverrides?: Partial<SearchResponseParseLimits>): ParsedSearchApiResponse;
|
|
36
36
|
/**
|
|
37
37
|
* Parse a complete, already-bounded Search API body (SSE or non-streaming JSON).
|
package/lib/search/parse.js
CHANGED
|
@@ -28,8 +28,9 @@ export const DEFAULT_SEARCH_RESPONSE_PARSE_LIMITS = Object.freeze({
|
|
|
28
28
|
maxTitleCharacters: 1000,
|
|
29
29
|
maxUrlCharacters: 8192,
|
|
30
30
|
});
|
|
31
|
-
const
|
|
32
|
-
const
|
|
31
|
+
const BARE_URL_PATTERN = /https?:\/\/[^\s<>"'`,。、;:!?》)】]+/g;
|
|
32
|
+
const MARKDOWN_URL_BOUNDARIES = new Set(['<', '>', '"', "'", '`', ',', '。', '、', ';', ':', '!', '?', '》', ')', '】']);
|
|
33
|
+
const URL_TRAILING_PUNCTUATION = new Set(['.', ',', ';', ':', '!', '?']);
|
|
33
34
|
const SOURCES_HEADING_PATTERN = /(?:^|\n)(?:#{1,6}\s*)?(?:\*\*|__)?\s*(?:sources?|references?|citations?|信源|参考资料|参考|引用|来源列表|来源)(?:\s*[((][^)\n]*[))])?\s*(?:(?:\*\*|__)\s*)?[::]?\s*(?:(?:\*\*|__)\s*)?$/gim;
|
|
34
35
|
const SOURCES_FUNCTION_PATTERN = /(^|\n)\s*(sources|source|citations|citation|references|reference|citation_card|source_cards|source_card)\s*\(/gim;
|
|
35
36
|
function isRecord(value) {
|
|
@@ -77,8 +78,172 @@ function firstBoundedString(record, keys, maximum) {
|
|
|
77
78
|
}
|
|
78
79
|
return undefined;
|
|
79
80
|
}
|
|
81
|
+
function isAsciiDigit(character) {
|
|
82
|
+
return character !== undefined && character >= '0' && character <= '9';
|
|
83
|
+
}
|
|
84
|
+
function isCitationOrdinalLabel(label) {
|
|
85
|
+
const trimmed = label.trim();
|
|
86
|
+
if (trimmed.length === 0)
|
|
87
|
+
return false;
|
|
88
|
+
for (let index = 0; index < trimmed.length; index += 1) {
|
|
89
|
+
if (!isAsciiDigit(trimmed[index]))
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
function isMarkdownUrlBoundary(character) {
|
|
95
|
+
return /\s/u.test(character) || MARKDOWN_URL_BOUNDARIES.has(character);
|
|
96
|
+
}
|
|
97
|
+
/** Scan the supported Markdown HTTP(S) link subset once without recursive parsing. */
|
|
98
|
+
function scanMarkdownHttpLinks(text) {
|
|
99
|
+
const links = [];
|
|
100
|
+
const intervals = [];
|
|
101
|
+
const scanDestination = (urlStart) => {
|
|
102
|
+
if (!text.startsWith('http://', urlStart) && !text.startsWith('https://', urlStart)) {
|
|
103
|
+
return { resume: urlStart };
|
|
104
|
+
}
|
|
105
|
+
let depth = 0;
|
|
106
|
+
for (let index = urlStart; index < text.length; index += 1) {
|
|
107
|
+
const character = text[index];
|
|
108
|
+
if (character === undefined)
|
|
109
|
+
break;
|
|
110
|
+
if (isMarkdownUrlBoundary(character))
|
|
111
|
+
return { resume: index + 1 };
|
|
112
|
+
if (character === '(') {
|
|
113
|
+
depth += 1;
|
|
114
|
+
}
|
|
115
|
+
else if (character === ')') {
|
|
116
|
+
if (depth === 0) {
|
|
117
|
+
return {
|
|
118
|
+
end: index + 1,
|
|
119
|
+
resume: index + 1,
|
|
120
|
+
url: text.slice(urlStart, index),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
depth -= 1;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return { resume: text.length };
|
|
127
|
+
};
|
|
128
|
+
let cursor = 0;
|
|
129
|
+
while (cursor < text.length) {
|
|
130
|
+
const start = text.indexOf('[', cursor);
|
|
131
|
+
if (start === -1)
|
|
132
|
+
break;
|
|
133
|
+
if (start > 0 && text[start - 1] === '[') {
|
|
134
|
+
cursor = start + 1;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
let label;
|
|
138
|
+
let citationOrdinal;
|
|
139
|
+
let destinationOpen;
|
|
140
|
+
if (text[start + 1] === '[') {
|
|
141
|
+
const digitsStart = start + 2;
|
|
142
|
+
let labelEnd = digitsStart;
|
|
143
|
+
while (isAsciiDigit(text[labelEnd]))
|
|
144
|
+
labelEnd += 1;
|
|
145
|
+
if (labelEnd === digitsStart
|
|
146
|
+
|| text[labelEnd] !== ']'
|
|
147
|
+
|| text[labelEnd + 1] !== ']'
|
|
148
|
+
|| text[labelEnd + 2] !== '(') {
|
|
149
|
+
labelEnd = text[digitsStart] === '[' ? digitsStart + 1 : digitsStart;
|
|
150
|
+
while (labelEnd < text.length
|
|
151
|
+
&& text[labelEnd] !== ']'
|
|
152
|
+
&& text[labelEnd] !== '['
|
|
153
|
+
&& !isMarkdownUrlBoundary(text[labelEnd] ?? ''))
|
|
154
|
+
labelEnd += 1;
|
|
155
|
+
if (text[labelEnd] === ']'
|
|
156
|
+
&& text[labelEnd + 1] === ']'
|
|
157
|
+
&& text[labelEnd + 2] === '(') {
|
|
158
|
+
const rejected = scanDestination(labelEnd + 3);
|
|
159
|
+
if (rejected.end !== undefined) {
|
|
160
|
+
intervals.push(Object.freeze({ end: rejected.end, start }));
|
|
161
|
+
}
|
|
162
|
+
cursor = rejected.resume;
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
cursor = text[labelEnd] === '[' ? labelEnd : labelEnd + 1;
|
|
166
|
+
}
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
label = text.slice(digitsStart, labelEnd);
|
|
170
|
+
citationOrdinal = true;
|
|
171
|
+
destinationOpen = labelEnd + 2;
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
const labelStart = start + 1;
|
|
175
|
+
let labelEnd = labelStart;
|
|
176
|
+
while (labelEnd < text.length
|
|
177
|
+
&& text[labelEnd] !== ']'
|
|
178
|
+
&& text[labelEnd] !== '[')
|
|
179
|
+
labelEnd += 1;
|
|
180
|
+
if (labelEnd === labelStart
|
|
181
|
+
|| text[labelEnd] !== ']'
|
|
182
|
+
|| text[labelEnd + 1] !== '(') {
|
|
183
|
+
cursor = text[labelEnd] === '[' ? labelEnd : labelEnd + 1;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
label = text.slice(labelStart, labelEnd);
|
|
187
|
+
citationOrdinal = isCitationOrdinalLabel(label);
|
|
188
|
+
destinationOpen = labelEnd + 1;
|
|
189
|
+
}
|
|
190
|
+
const destination = scanDestination(destinationOpen + 1);
|
|
191
|
+
if (destination.end !== undefined && destination.url !== undefined) {
|
|
192
|
+
const link = Object.freeze({
|
|
193
|
+
citationOrdinal,
|
|
194
|
+
end: destination.end,
|
|
195
|
+
label,
|
|
196
|
+
start,
|
|
197
|
+
url: destination.url,
|
|
198
|
+
});
|
|
199
|
+
links.push(link);
|
|
200
|
+
intervals.push(link);
|
|
201
|
+
}
|
|
202
|
+
cursor = destination.resume;
|
|
203
|
+
}
|
|
204
|
+
return Object.freeze({
|
|
205
|
+
intervals: Object.freeze(intervals),
|
|
206
|
+
links: Object.freeze(links),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
/** Remove only sentence punctuation and unmatched ASCII closing parens at the URL tail. */
|
|
210
|
+
function trimUrlTail(raw) {
|
|
211
|
+
const value = raw.trim();
|
|
212
|
+
const unmatchedClosings = [];
|
|
213
|
+
let depth = 0;
|
|
214
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
215
|
+
const character = value[index];
|
|
216
|
+
if (character === '(')
|
|
217
|
+
depth += 1;
|
|
218
|
+
else if (character === ')') {
|
|
219
|
+
if (depth === 0)
|
|
220
|
+
unmatchedClosings.push(index);
|
|
221
|
+
else
|
|
222
|
+
depth -= 1;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
let end = value.length;
|
|
226
|
+
let unmatchedIndex = unmatchedClosings.length - 1;
|
|
227
|
+
let changed = true;
|
|
228
|
+
while (changed) {
|
|
229
|
+
changed = false;
|
|
230
|
+
while (end > 0 && URL_TRAILING_PUNCTUATION.has(value[end - 1] ?? '')) {
|
|
231
|
+
end -= 1;
|
|
232
|
+
changed = true;
|
|
233
|
+
}
|
|
234
|
+
while (unmatchedIndex >= 0 && (unmatchedClosings[unmatchedIndex] ?? -1) >= end) {
|
|
235
|
+
unmatchedIndex -= 1;
|
|
236
|
+
}
|
|
237
|
+
if (unmatchedClosings[unmatchedIndex] === end - 1) {
|
|
238
|
+
end -= 1;
|
|
239
|
+
unmatchedIndex -= 1;
|
|
240
|
+
changed = true;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return value.slice(0, end);
|
|
244
|
+
}
|
|
80
245
|
function validatedHttpUrl(raw, limits) {
|
|
81
|
-
const url = raw
|
|
246
|
+
const url = trimUrlTail(raw);
|
|
82
247
|
const actual = characterLength(url);
|
|
83
248
|
if (actual > limits.maxUrlCharacters) {
|
|
84
249
|
throw new SearchResponseParseError('limit', { actual, maximum: limits.maxUrlCharacters });
|
|
@@ -95,13 +260,31 @@ function validatedHttpUrl(raw, limits) {
|
|
|
95
260
|
}
|
|
96
261
|
function addSource(state, candidate, limits) {
|
|
97
262
|
const url = validatedHttpUrl(candidate.url, limits);
|
|
98
|
-
if (url === undefined
|
|
263
|
+
if (url === undefined)
|
|
99
264
|
return;
|
|
100
|
-
state.
|
|
265
|
+
const existingIndex = state.sourceIndexByUrl.get(url);
|
|
266
|
+
if (existingIndex !== undefined) {
|
|
267
|
+
const existing = state.sources[existingIndex];
|
|
268
|
+
const title = existing.title ?? candidate.title;
|
|
269
|
+
const snippet = existing.snippet ?? candidate.snippet;
|
|
270
|
+
const publishedAt = existing.publishedAt ?? candidate.publishedAt;
|
|
271
|
+
if (title !== existing.title
|
|
272
|
+
|| snippet !== existing.snippet
|
|
273
|
+
|| publishedAt !== existing.publishedAt) {
|
|
274
|
+
state.sources[existingIndex] = Object.freeze({
|
|
275
|
+
...existing,
|
|
276
|
+
...(title === undefined ? {} : { title }),
|
|
277
|
+
...(snippet === undefined ? {} : { snippet }),
|
|
278
|
+
...(publishedAt === undefined ? {} : { publishedAt }),
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
101
283
|
if (state.sources.length >= limits.maxSources) {
|
|
102
284
|
state.truncated = true;
|
|
103
285
|
return;
|
|
104
286
|
}
|
|
287
|
+
const index = state.sources.length;
|
|
105
288
|
state.sources.push(Object.freeze({
|
|
106
289
|
provider: 'search-api',
|
|
107
290
|
url,
|
|
@@ -109,40 +292,50 @@ function addSource(state, candidate, limits) {
|
|
|
109
292
|
...(candidate.snippet === undefined ? {} : { snippet: candidate.snippet }),
|
|
110
293
|
...(candidate.publishedAt === undefined ? {} : { publishedAt: candidate.publishedAt }),
|
|
111
294
|
}));
|
|
295
|
+
state.sourceIndexByUrl.set(url, index);
|
|
112
296
|
}
|
|
113
297
|
function extractUrls(text) {
|
|
114
298
|
const urls = [];
|
|
115
|
-
const
|
|
116
|
-
for (const match of text.matchAll(URL_PATTERN)) {
|
|
299
|
+
for (const match of text.matchAll(BARE_URL_PATTERN)) {
|
|
117
300
|
const raw = match[0];
|
|
118
301
|
if (raw === undefined)
|
|
119
302
|
continue;
|
|
120
|
-
|
|
121
|
-
if (seen.has(url))
|
|
122
|
-
continue;
|
|
123
|
-
seen.add(url);
|
|
124
|
-
urls.push(url);
|
|
303
|
+
urls.push(raw);
|
|
125
304
|
}
|
|
126
305
|
return urls;
|
|
127
306
|
}
|
|
128
|
-
function addMarkdownSources(
|
|
129
|
-
for (const
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
continue;
|
|
134
|
-
const title = boundedOptionalString(rawTitle, limits.maxTitleCharacters);
|
|
307
|
+
function addMarkdownSources(links, state, limits) {
|
|
308
|
+
for (const link of links) {
|
|
309
|
+
const title = link.citationOrdinal
|
|
310
|
+
? undefined
|
|
311
|
+
: boundedOptionalString(link.label, limits.maxTitleCharacters);
|
|
135
312
|
addSource(state, {
|
|
136
|
-
url:
|
|
313
|
+
url: link.url,
|
|
137
314
|
...(title === undefined ? {} : { title }),
|
|
138
315
|
}, limits);
|
|
139
316
|
}
|
|
140
317
|
}
|
|
318
|
+
function maskMarkdownLinkIntervals(text, intervals) {
|
|
319
|
+
if (intervals.length === 0)
|
|
320
|
+
return text;
|
|
321
|
+
const parts = [];
|
|
322
|
+
let cursor = 0;
|
|
323
|
+
for (const link of intervals) {
|
|
324
|
+
parts.push(text.slice(cursor, link.start), ' ');
|
|
325
|
+
cursor = link.end;
|
|
326
|
+
}
|
|
327
|
+
parts.push(text.slice(cursor));
|
|
328
|
+
return parts.join('');
|
|
329
|
+
}
|
|
141
330
|
/** Extract URL-validated inline Markdown citations in answer order for internal orchestration. */
|
|
142
331
|
export function extractMarkdownCitationUrls(text, maximumUrlCharacters = DEFAULT_SEARCH_RESPONSE_PARSE_LIMITS.maxUrlCharacters) {
|
|
143
332
|
assertSafeInteger(maximumUrlCharacters, 'maximumUrlCharacters', false);
|
|
144
|
-
const state = {
|
|
145
|
-
|
|
333
|
+
const state = {
|
|
334
|
+
sourceIndexByUrl: new Map(),
|
|
335
|
+
sources: [],
|
|
336
|
+
truncated: false,
|
|
337
|
+
};
|
|
338
|
+
addMarkdownSources(scanMarkdownHttpLinks(text).links, state, {
|
|
146
339
|
...DEFAULT_SEARCH_RESPONSE_PARSE_LIMITS,
|
|
147
340
|
maxSources: Number.MAX_SAFE_INTEGER,
|
|
148
341
|
maxUrlCharacters: maximumUrlCharacters,
|
|
@@ -150,8 +343,9 @@ export function extractMarkdownCitationUrls(text, maximumUrlCharacters = DEFAULT
|
|
|
150
343
|
return Object.freeze(state.sources.map(source => source.url));
|
|
151
344
|
}
|
|
152
345
|
function addTextSources(text, state, limits) {
|
|
153
|
-
|
|
154
|
-
|
|
346
|
+
const markdown = scanMarkdownHttpLinks(text);
|
|
347
|
+
addMarkdownSources(markdown.links, state, limits);
|
|
348
|
+
const bareText = maskMarkdownLinkIntervals(text, markdown.intervals);
|
|
155
349
|
for (const url of extractUrls(bareText))
|
|
156
350
|
addSource(state, { url }, limits);
|
|
157
351
|
}
|
|
@@ -322,7 +516,11 @@ function splitDetailsBlockSources(text, state, limits) {
|
|
|
322
516
|
const openIndex = lower.lastIndexOf('<details', closeIndex);
|
|
323
517
|
if (openIndex === -1)
|
|
324
518
|
return undefined;
|
|
325
|
-
const temporary = {
|
|
519
|
+
const temporary = {
|
|
520
|
+
sourceIndexByUrl: new Map(),
|
|
521
|
+
sources: [],
|
|
522
|
+
truncated: false,
|
|
523
|
+
};
|
|
326
524
|
addTextSources(text.slice(openIndex, closeIndex + '</details>'.length), temporary, limits);
|
|
327
525
|
if (temporary.sources.length < 2 && !temporary.truncated)
|
|
328
526
|
return undefined;
|
|
@@ -336,7 +534,7 @@ function isLinkOnlyLine(line) {
|
|
|
336
534
|
const stripped = line.replace(/^\s*(?:[-*]|\d+\.)\s*/, '').trim();
|
|
337
535
|
return stripped.length > 0 && (stripped.startsWith('http://')
|
|
338
536
|
|| stripped.startsWith('https://')
|
|
339
|
-
|| stripped.
|
|
537
|
+
|| scanMarkdownHttpLinks(stripped).links.length > 0);
|
|
340
538
|
}
|
|
341
539
|
function splitTailLinkBlock(text, state, limits) {
|
|
342
540
|
const lines = text.split(/\r?\n/);
|
|
@@ -367,7 +565,7 @@ function splitTailLinkBlock(text, state, limits) {
|
|
|
367
565
|
return undefined;
|
|
368
566
|
return lines.slice(0, tailStart).join('\n').trimEnd();
|
|
369
567
|
}
|
|
370
|
-
/** Convert Search API prose/source conventions
|
|
568
|
+
/** Convert Search API prose/source conventions with one bounded link scan and exact-URL enrichment. */
|
|
371
569
|
export function parseSearchAnswerText(text, limitOverrides = {}) {
|
|
372
570
|
const limits = { ...DEFAULT_SEARCH_RESPONSE_PARSE_LIMITS, ...limitOverrides };
|
|
373
571
|
validateLimits(limits);
|
|
@@ -375,14 +573,22 @@ export function parseSearchAnswerText(text, limitOverrides = {}) {
|
|
|
375
573
|
if (trimmed.length === 0) {
|
|
376
574
|
return Object.freeze({ answer: '', sources: Object.freeze([]), sourcesTruncated: false });
|
|
377
575
|
}
|
|
378
|
-
const trailingState = {
|
|
576
|
+
const trailingState = {
|
|
577
|
+
sourceIndexByUrl: new Map(),
|
|
578
|
+
sources: [],
|
|
579
|
+
truncated: false,
|
|
580
|
+
};
|
|
379
581
|
const answer = splitFunctionCallSources(trimmed, trailingState, limits)
|
|
380
582
|
?? splitHeadingSources(trimmed, trailingState, limits)
|
|
381
583
|
?? splitDetailsBlockSources(trimmed, trailingState, limits)
|
|
382
584
|
?? splitTailLinkBlock(trimmed, trailingState, limits)
|
|
383
585
|
?? trimmed;
|
|
384
|
-
const state = {
|
|
385
|
-
|
|
586
|
+
const state = {
|
|
587
|
+
sourceIndexByUrl: new Map(),
|
|
588
|
+
sources: [],
|
|
589
|
+
truncated: false,
|
|
590
|
+
};
|
|
591
|
+
addMarkdownSources(scanMarkdownHttpLinks(answer).links, state, limits);
|
|
386
592
|
for (const source of trailingState.sources)
|
|
387
593
|
addSource(state, source, limits);
|
|
388
594
|
state.truncated ||= trailingState.truncated;
|
package/lib/tools/docs-search.js
CHANGED
|
@@ -179,6 +179,7 @@ async function executeDocsSearch(args, exec, dependencies, signal) {
|
|
|
179
179
|
const result = await dependencies.documentation.search({
|
|
180
180
|
query: args.query,
|
|
181
181
|
...(args.provider === undefined ? {} : { provider: args.provider }),
|
|
182
|
+
...(args.library_name === undefined ? {} : { libraryName: args.library_name }),
|
|
182
183
|
...(args.library_id === undefined ? {} : { libraryId: args.library_id }),
|
|
183
184
|
maxResults,
|
|
184
185
|
...(args.force_refresh === undefined ? {} : { forceRefresh: args.force_refresh }),
|
|
@@ -197,7 +198,7 @@ async function executeDocsSearch(args, exec, dependencies, signal) {
|
|
|
197
198
|
export function createDocsSearchTool(dependencies) {
|
|
198
199
|
return defineTool({
|
|
199
200
|
name: 'docs_search',
|
|
200
|
-
description: 'High-level SDK/API/framework/README/release documentation search.
|
|
201
|
+
description: 'High-level SDK/API/framework/README/release documentation search. Pass library_id for an exact Context7 id or library_name for a package/product; auto without either uses Exa discovery, while context7/all require one. Returns bounded discovery snippets and durable source_ref pagination.',
|
|
201
202
|
parameters: DOCS_SEARCH_PARAMETERS,
|
|
202
203
|
output: {
|
|
203
204
|
schema: DOCS_SEARCH_OUTPUT_SCHEMA,
|
package/lib/tools/schemas.d.ts
CHANGED
|
@@ -153,11 +153,15 @@ export declare const DOCS_SEARCH_PARAMETERS: {
|
|
|
153
153
|
readonly provider: {
|
|
154
154
|
readonly type: "string";
|
|
155
155
|
readonly enum: readonly ["auto", "context7", "exa", "all"];
|
|
156
|
-
readonly description: "auto (default), Context7, Exa, or both
|
|
156
|
+
readonly description: "auto (default), Context7, Exa, or both. Without a Context7 library identity, auto is Exa-only.";
|
|
157
|
+
};
|
|
158
|
+
readonly library_name: {
|
|
159
|
+
readonly type: "string";
|
|
160
|
+
readonly description: "Optional Context7 package or product name, such as React or FastAPI. Required for Context7 resolve when library_id is absent; never sent to Exa.";
|
|
157
161
|
};
|
|
158
162
|
readonly library_id: {
|
|
159
163
|
readonly type: "string";
|
|
160
|
-
readonly description: "Optional exact Context7 /org/project or /org/project/version id;
|
|
164
|
+
readonly description: "Optional exact Context7 /org/project or /org/project/version id; takes priority over library_name and strictly skips resolve.";
|
|
161
165
|
};
|
|
162
166
|
readonly max_results: {
|
|
163
167
|
readonly type: "integer";
|
package/lib/tools/schemas.js
CHANGED
|
@@ -144,11 +144,15 @@ export const DOCS_SEARCH_PARAMETERS = {
|
|
|
144
144
|
provider: {
|
|
145
145
|
type: 'string',
|
|
146
146
|
enum: DOCUMENTATION_SEARCH_PROVIDERS,
|
|
147
|
-
description: 'auto (default), Context7, Exa, or both
|
|
147
|
+
description: 'auto (default), Context7, Exa, or both. Without a Context7 library identity, auto is Exa-only.',
|
|
148
|
+
},
|
|
149
|
+
library_name: {
|
|
150
|
+
type: 'string',
|
|
151
|
+
description: 'Optional Context7 package or product name, such as React or FastAPI. Required for Context7 resolve when library_id is absent; never sent to Exa.',
|
|
148
152
|
},
|
|
149
153
|
library_id: {
|
|
150
154
|
type: 'string',
|
|
151
|
-
description: 'Optional exact Context7 /org/project or /org/project/version id;
|
|
155
|
+
description: 'Optional exact Context7 /org/project or /org/project/version id; takes priority over library_name and strictly skips resolve.',
|
|
152
156
|
},
|
|
153
157
|
max_results: {
|
|
154
158
|
type: 'integer',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-search-enhance",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Bounded search, documentation, extraction, progressively disclosed site mapping/research planning, and read-only diagnostics for DeepSeek Harness",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|