dsh-search-enhance 0.1.3 → 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 +8 -14
- package/lib/documentation/service.js +51 -79
- 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 +41 -90
- 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 -31
- 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
|
|
@@ -2,8 +2,8 @@ import { Service, type Context } from '@deepseek-ai/cordis';
|
|
|
2
2
|
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
|
-
import { Context7RemoteClient, type Context7Library } from '../providers/context7.js';
|
|
6
|
-
import type { BoundedSourceProvider, DocumentationSnippet
|
|
5
|
+
import { type Context7RemoteClient, type Context7Library } from '../providers/context7.js';
|
|
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;
|
|
@@ -125,7 +128,7 @@ export declare class DocumentationSearchInfrastructureError extends Error {
|
|
|
125
128
|
constructor(attempts: readonly ProviderAttemptRecord[], warnings: readonly DocumentationWarning[], cache: Readonly<DocumentationCacheReport>, cause: unknown);
|
|
126
129
|
}
|
|
127
130
|
export interface DocumentationSearchDependencies {
|
|
128
|
-
readonly context7: Context7RemoteClient
|
|
131
|
+
readonly context7: Pick<Context7RemoteClient, 'resolve' | 'docs'>;
|
|
129
132
|
readonly context7Cache: Context7CachedOperations;
|
|
130
133
|
readonly exa: BoundedSourceProvider;
|
|
131
134
|
readonly getConfig: () => Config;
|
|
@@ -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,10 +1,10 @@
|
|
|
1
1
|
import { Service } from '@deepseek-ai/cordis';
|
|
2
2
|
import { createProviderAttemptRecord, isAbortError, isProviderError, ProviderError, throwIfAborted, } from '../provider-runtime/index.js';
|
|
3
|
-
import {
|
|
4
|
-
import { Context7RemoteClient, context7LibrarySource, selectContext7Library, } from '../providers/context7.js';
|
|
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';
|
|
7
6
|
import { CONTEXT7_CACHE_STATES, Context7CachedOperations, Context7OperationFailure, isContext7LibraryId, normalizeContext7LibraryId, } from './context7-cache.js';
|
|
7
|
+
const CONTEXT7_RESOLVE_CANDIDATE_LIMIT = 30;
|
|
8
8
|
export const DOCUMENTATION_SEARCH_SERVICE_KEY = 'searchEnhanceDocumentation';
|
|
9
9
|
export const DOCUMENTATION_SEARCH_PROVIDERS = ['auto', 'context7', 'exa', 'all'];
|
|
10
10
|
export const DOCUMENTATION_RESULT_PROVIDERS = ['context7', 'exa'];
|
|
@@ -189,17 +189,34 @@ function validateInput(input, config) {
|
|
|
189
189
|
provider: 'documentation-search',
|
|
190
190
|
});
|
|
191
191
|
}
|
|
192
|
-
|
|
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) {
|
|
193
211
|
throw new ProviderError({
|
|
194
212
|
capability: 'docs_search',
|
|
195
213
|
kind: 'invalid_request',
|
|
196
|
-
provider: 'context7',
|
|
214
|
+
provider: 'context7-library-name-or-id-required',
|
|
197
215
|
});
|
|
198
216
|
}
|
|
199
217
|
return {
|
|
200
|
-
...(
|
|
201
|
-
|
|
202
|
-
: { libraryId: normalizeContext7LibraryId(input.libraryId) }),
|
|
218
|
+
...(libraryId === undefined ? {} : { libraryId }),
|
|
219
|
+
...(libraryName === undefined ? {} : { libraryName }),
|
|
203
220
|
provider,
|
|
204
221
|
query,
|
|
205
222
|
};
|
|
@@ -238,8 +255,8 @@ function frozenPathOutcome(outcome) {
|
|
|
238
255
|
});
|
|
239
256
|
}
|
|
240
257
|
/**
|
|
241
|
-
* Lifecycle-bound documentation core shared by the high-level docs Consumer
|
|
242
|
-
*
|
|
258
|
+
* Lifecycle-bound documentation core shared by the high-level docs Consumer
|
|
259
|
+
* and the deferred granular Context7 Consumers.
|
|
243
260
|
*/
|
|
244
261
|
export class DocumentationSearchService extends Service {
|
|
245
262
|
context7;
|
|
@@ -347,6 +364,7 @@ export class DocumentationSearchService extends Service {
|
|
|
347
364
|
await Promise.allSettled([...this.active]);
|
|
348
365
|
}
|
|
349
366
|
async executeContext7Resolve(input) {
|
|
367
|
+
const candidateLimit = input.candidateLimit ?? input.maxResults;
|
|
350
368
|
const startedAt = safeClockValue(this.now);
|
|
351
369
|
let dispatches = 0;
|
|
352
370
|
const resolved = await this.context7Cache.resolve({
|
|
@@ -354,20 +372,22 @@ export class DocumentationSearchService extends Service {
|
|
|
354
372
|
forceRefresh: input.forceRefresh,
|
|
355
373
|
load: () => this.context7.resolve({
|
|
356
374
|
config: input.config,
|
|
357
|
-
limit:
|
|
375
|
+
limit: candidateLimit,
|
|
358
376
|
onDispatch: () => {
|
|
359
377
|
dispatches += 1;
|
|
360
378
|
input.onDispatch?.();
|
|
361
379
|
},
|
|
362
|
-
|
|
380
|
+
libraryName: input.libraryName,
|
|
381
|
+
query: input.query,
|
|
363
382
|
signal: input.signal,
|
|
364
383
|
}),
|
|
365
|
-
maxResults:
|
|
366
|
-
|
|
384
|
+
maxResults: candidateLimit,
|
|
385
|
+
libraryName: input.libraryName,
|
|
386
|
+
query: input.query,
|
|
367
387
|
signal: input.signal,
|
|
368
388
|
});
|
|
369
|
-
const candidates = Object.freeze(
|
|
370
|
-
const selected = selectContext7Library(
|
|
389
|
+
const candidates = Object.freeze(resolved.entry.libraries.slice(0, input.maxResults));
|
|
390
|
+
const selected = selectContext7Library(resolved.entry.libraries, input.libraryName, input.query);
|
|
371
391
|
return Object.freeze({
|
|
372
392
|
attempts: successfulCacheAttempts('resolve', resolved, dispatches, elapsed(startedAt, this.now)),
|
|
373
393
|
cache: cachePath(resolved.cache, resolved.evictedEntries),
|
|
@@ -417,8 +437,12 @@ export class DocumentationSearchService extends Service {
|
|
|
417
437
|
const config = input.config ?? this.getConfig();
|
|
418
438
|
const validated = validateInput(input, config);
|
|
419
439
|
const forceRefresh = input.forceRefresh === true;
|
|
420
|
-
const
|
|
421
|
-
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');
|
|
422
446
|
const considerExa = validated.provider === 'exa' || validated.provider === 'all' || autoExa;
|
|
423
447
|
let exaConfigured = false;
|
|
424
448
|
let exaProbeError;
|
|
@@ -438,6 +462,7 @@ export class DocumentationSearchService extends Service {
|
|
|
438
462
|
config,
|
|
439
463
|
forceRefresh,
|
|
440
464
|
...(validated.libraryId === undefined ? {} : { libraryId: validated.libraryId }),
|
|
465
|
+
...(validated.libraryName === undefined ? {} : { libraryName: validated.libraryName }),
|
|
441
466
|
maxResults: input.maxResults,
|
|
442
467
|
query: validated.query,
|
|
443
468
|
signal,
|
|
@@ -631,13 +656,21 @@ export class DocumentationSearchService extends Service {
|
|
|
631
656
|
attempts.push(skippedAttempt('context7-resolve'));
|
|
632
657
|
}
|
|
633
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
|
+
}
|
|
634
666
|
const startedAt = safeClockValue(this.now);
|
|
635
667
|
try {
|
|
636
668
|
const resolved = await this.executeContext7Resolve({
|
|
637
669
|
config: input.config,
|
|
638
670
|
forceRefresh: input.forceRefresh,
|
|
639
|
-
libraryName: input.
|
|
671
|
+
libraryName: input.libraryName,
|
|
640
672
|
maxResults: input.maxResults,
|
|
673
|
+
candidateLimit: Math.min(CONTEXT7_RESOLVE_CANDIDATE_LIMIT, input.config.retention.providerMaxSources),
|
|
641
674
|
onDispatch: () => { resolveDispatches += 1; },
|
|
642
675
|
query: input.query,
|
|
643
676
|
signal: input.signal,
|
|
@@ -835,65 +868,4 @@ export class DocumentationSearchService extends Service {
|
|
|
835
868
|
}
|
|
836
869
|
}
|
|
837
870
|
}
|
|
838
|
-
/** Context7-only adapter used by web_search while sharing the high-level service/cache. */
|
|
839
|
-
export class DocumentationContext7Provider {
|
|
840
|
-
documentation;
|
|
841
|
-
capability = 'docs_search';
|
|
842
|
-
provider = 'context7';
|
|
843
|
-
constructor(documentation) {
|
|
844
|
-
this.documentation = documentation;
|
|
845
|
-
}
|
|
846
|
-
async configured() {
|
|
847
|
-
return true;
|
|
848
|
-
}
|
|
849
|
-
async search(input) {
|
|
850
|
-
let result;
|
|
851
|
-
try {
|
|
852
|
-
result = await this.documentation.search({
|
|
853
|
-
config: input.config,
|
|
854
|
-
forceRefresh: false,
|
|
855
|
-
maxResults: input.limit,
|
|
856
|
-
provider: 'context7',
|
|
857
|
-
query: input.query,
|
|
858
|
-
signal: input.signal,
|
|
859
|
-
});
|
|
860
|
-
}
|
|
861
|
-
catch (error) {
|
|
862
|
-
if (error instanceof DocumentationSearchInfrastructureError && error.cause !== undefined) {
|
|
863
|
-
throw error.cause;
|
|
864
|
-
}
|
|
865
|
-
throw error;
|
|
866
|
-
}
|
|
867
|
-
const warnings = result.warnings.flatMap(warning => {
|
|
868
|
-
if (warning.code !== 'cache_stale'
|
|
869
|
-
&& warning.code !== 'cache_evicted'
|
|
870
|
-
&& warning.code !== 'provider_failed')
|
|
871
|
-
return [];
|
|
872
|
-
return [Object.freeze({
|
|
873
|
-
code: warning.code,
|
|
874
|
-
...(warning.errorKind === undefined ? {} : { errorKind: warning.errorKind }),
|
|
875
|
-
provider: warning.provider ?? 'context7',
|
|
876
|
-
})];
|
|
877
|
-
});
|
|
878
|
-
const attempts = result.attempts
|
|
879
|
-
.filter(attempt => !attempt.provider.startsWith('context7-cache-'))
|
|
880
|
-
.reduce((sum, attempt) => sum + attempt.attempts, 0);
|
|
881
|
-
return Object.freeze({
|
|
882
|
-
attempts: Math.max(1, attempts),
|
|
883
|
-
result: boundSourceProviderResult({
|
|
884
|
-
capability: 'docs_search',
|
|
885
|
-
config: input.config,
|
|
886
|
-
inputTruncated: result.truncated,
|
|
887
|
-
provider: 'context7',
|
|
888
|
-
requestedSources: input.limit,
|
|
889
|
-
responseBytes: result.providerResponseBytes,
|
|
890
|
-
snippets: result.snippets,
|
|
891
|
-
sources: result.sources,
|
|
892
|
-
}),
|
|
893
|
-
state: 'complete',
|
|
894
|
-
totalDelayMs: 0,
|
|
895
|
-
...(warnings.length === 0 ? {} : { warnings: Object.freeze(warnings) }),
|
|
896
|
-
});
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
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';
|
|
@@ -121,50 +120,55 @@ export function parseContext7Libraries(body) {
|
|
|
121
120
|
function normalizedMatchText(value) {
|
|
122
121
|
return (value ?? '').toLowerCase().replace(/^@/, '').replace(/[^a-z0-9]+/g, '');
|
|
123
122
|
}
|
|
124
|
-
function
|
|
123
|
+
function normalizedTerms(value) {
|
|
124
|
+
return value.toLowerCase()
|
|
125
|
+
.split(/[^a-z0-9@._-]+/)
|
|
126
|
+
.map(normalizedMatchText)
|
|
127
|
+
.filter(term => term.length > 0);
|
|
128
|
+
}
|
|
129
|
+
function context7LibraryScore(item, libraryName, queryTerms) {
|
|
125
130
|
const wanted = normalizedMatchText(libraryName);
|
|
126
131
|
const rawText = `${item.id ?? ''} ${item.title ?? ''} ${item.description ?? ''}`.toLowerCase();
|
|
127
132
|
const title = normalizedMatchText(item.title);
|
|
128
133
|
const description = normalizedMatchText(item.description);
|
|
129
134
|
const idSegments = (item.id ?? '').split('/').filter(Boolean).map(normalizedMatchText);
|
|
130
135
|
let score = 0;
|
|
131
|
-
if (/official.*documentation|documentation.*official|official docs|react\.dev/.test(rawText))
|
|
132
|
-
score += 260;
|
|
133
|
-
if (rawText.includes('react.dev'))
|
|
134
|
-
score += 120;
|
|
135
136
|
if (wanted.length > 0) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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);
|
|
146
148
|
if (description.includes(wanted))
|
|
147
|
-
score +=
|
|
149
|
+
score += 15;
|
|
148
150
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (
|
|
151
|
+
let queryRelevance = 0;
|
|
152
|
+
for (const term of queryTerms) {
|
|
153
|
+
if (term.length <= 1)
|
|
152
154
|
continue;
|
|
153
|
-
if (title ===
|
|
154
|
-
|
|
155
|
-
else if (title.includes(
|
|
156
|
-
|
|
157
|
-
if (idSegments.some(segment => segment ===
|
|
158
|
-
|
|
159
|
-
if (description.includes(
|
|
160
|
-
|
|
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;
|
|
161
163
|
}
|
|
164
|
+
score += Math.min(30, queryRelevance);
|
|
162
165
|
if (/official|documentation|docs/.test(rawText))
|
|
163
166
|
score += 10;
|
|
164
167
|
if (item.trustScore !== undefined)
|
|
165
|
-
score += item.trustScore * 2;
|
|
166
|
-
if (item.benchmarkScore !== undefined)
|
|
167
|
-
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
|
+
}
|
|
168
172
|
if (item.totalSnippets !== undefined) {
|
|
169
173
|
score += Math.min(30, Math.log10(Math.max(1, item.totalSnippets)) * 8);
|
|
170
174
|
}
|
|
@@ -172,13 +176,14 @@ function context7LibraryScore(item, libraryName, query) {
|
|
|
172
176
|
score += Math.min(20, Math.log10(Math.max(1, item.stars)) * 4);
|
|
173
177
|
return score;
|
|
174
178
|
}
|
|
175
|
-
/** Stable selection by
|
|
179
|
+
/** Stable selection led by explicit library identity, then bounded secondary signals. */
|
|
176
180
|
export function selectContext7Library(libraries, libraryName, query = libraryName) {
|
|
181
|
+
const queryTerms = normalizedTerms(query);
|
|
177
182
|
return libraries
|
|
178
183
|
.map((library, index) => ({
|
|
179
184
|
index,
|
|
180
185
|
library,
|
|
181
|
-
score: context7LibraryScore(library, libraryName,
|
|
186
|
+
score: context7LibraryScore(library, libraryName, queryTerms),
|
|
182
187
|
}))
|
|
183
188
|
.filter(candidate => isContext7LibraryId(candidate.library.id))
|
|
184
189
|
.sort((left, right) => right.score - left.score || left.index - right.index)[0]?.library;
|
|
@@ -288,12 +293,14 @@ export class Context7RemoteClient {
|
|
|
288
293
|
this.http = new ProviderHttpClient(dependencies);
|
|
289
294
|
}
|
|
290
295
|
async resolve(input) {
|
|
296
|
+
const libraryName = nonEmptyQuery(input.libraryName, PROVIDER, CAPABILITY);
|
|
291
297
|
const query = nonEmptyQuery(input.query, PROVIDER, CAPABILITY);
|
|
292
298
|
const limit = positiveLimit(input.limit, PROVIDER, CAPABILITY);
|
|
293
299
|
const providerConfig = input.config.providers.context7;
|
|
294
300
|
const credential = await resolveOptionalCredential(this.credentials, providerConfig.credentialRef, input.signal, PROVIDER, CAPABILITY);
|
|
295
|
-
const resolveUrl = new URL(providerEndpoint(providerConfig.baseUrl, '/api/v2/search'));
|
|
301
|
+
const resolveUrl = new URL(providerEndpoint(providerConfig.baseUrl, '/api/v2/libs/search'));
|
|
296
302
|
resolveUrl.searchParams.set('query', query);
|
|
303
|
+
resolveUrl.searchParams.set('libraryName', libraryName);
|
|
297
304
|
const response = await this.http.requestText({
|
|
298
305
|
capability: CAPABILITY,
|
|
299
306
|
endpoint: resolveUrl.href,
|
|
@@ -346,60 +353,4 @@ export class Context7RemoteClient {
|
|
|
346
353
|
});
|
|
347
354
|
}
|
|
348
355
|
}
|
|
349
|
-
/** Direct resolve-then-docs adapter retained for registration-free Provider use. */
|
|
350
|
-
export class Context7Provider {
|
|
351
|
-
capability = CAPABILITY;
|
|
352
|
-
provider = PROVIDER;
|
|
353
|
-
remote;
|
|
354
|
-
constructor(dependencies) {
|
|
355
|
-
this.remote = new Context7RemoteClient(dependencies);
|
|
356
|
-
}
|
|
357
|
-
async configured() {
|
|
358
|
-
return true;
|
|
359
|
-
}
|
|
360
|
-
async search(input) {
|
|
361
|
-
const query = nonEmptyQuery(input.query, PROVIDER, CAPABILITY);
|
|
362
|
-
const limit = positiveLimit(input.limit, PROVIDER, CAPABILITY);
|
|
363
|
-
const resolved = await this.remote.resolve({ ...input, query, limit });
|
|
364
|
-
const selected = selectContext7Library(resolved.libraries, query, query);
|
|
365
|
-
if (selected?.id === undefined || !isContext7LibraryId(selected.id)) {
|
|
366
|
-
return Object.freeze({
|
|
367
|
-
attempts: resolved.attempts,
|
|
368
|
-
result: boundSourceProviderResult({
|
|
369
|
-
capability: CAPABILITY,
|
|
370
|
-
config: input.config,
|
|
371
|
-
inputTruncated: resolved.truncated,
|
|
372
|
-
provider: PROVIDER,
|
|
373
|
-
requestedSources: limit,
|
|
374
|
-
responseBytes: resolved.responseBytes,
|
|
375
|
-
sources: [],
|
|
376
|
-
}),
|
|
377
|
-
state: 'complete',
|
|
378
|
-
totalDelayMs: resolved.totalDelayMs,
|
|
379
|
-
});
|
|
380
|
-
}
|
|
381
|
-
const libraryId = normalizeContext7LibraryId(selected.id);
|
|
382
|
-
const docs = await this.remote.docs({ ...input, libraryId, query, limit });
|
|
383
|
-
const snippets = docs.snippets.map(snippet => Object.freeze({
|
|
384
|
-
...snippet,
|
|
385
|
-
libraryId,
|
|
386
|
-
}));
|
|
387
|
-
const source = context7LibrarySource(selected, input.config.providers.context7.baseUrl, input.config.webExtract.maxUrlCharacters, snippets[0]?.content ?? selected.description);
|
|
388
|
-
return Object.freeze({
|
|
389
|
-
attempts: resolved.attempts + docs.attempts,
|
|
390
|
-
result: boundSourceProviderResult({
|
|
391
|
-
capability: CAPABILITY,
|
|
392
|
-
config: input.config,
|
|
393
|
-
inputTruncated: resolved.truncated || docs.truncated,
|
|
394
|
-
provider: PROVIDER,
|
|
395
|
-
requestedSources: limit,
|
|
396
|
-
responseBytes: resolved.responseBytes + docs.responseBytes,
|
|
397
|
-
snippets,
|
|
398
|
-
sources: source === undefined ? [] : [source],
|
|
399
|
-
}),
|
|
400
|
-
state: 'complete',
|
|
401
|
-
totalDelayMs: resolved.totalDelayMs + docs.totalDelayMs,
|
|
402
|
-
});
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
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,9 +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
|
|
33
|
-
const
|
|
31
|
+
const BARE_URL_PATTERN = /https?:\/\/[^\s<>"'`,。、;:!?》)】]+/g;
|
|
32
|
+
const MARKDOWN_URL_BOUNDARIES = new Set(['<', '>', '"', "'", '`', ',', '。', '、', ';', ':', '!', '?', '》', ')', '】']);
|
|
33
|
+
const URL_TRAILING_PUNCTUATION = new Set(['.', ',', ';', ':', '!', '?']);
|
|
34
34
|
const SOURCES_HEADING_PATTERN = /(?:^|\n)(?:#{1,6}\s*)?(?:\*\*|__)?\s*(?:sources?|references?|citations?|信源|参考资料|参考|引用|来源列表|来源)(?:\s*[((][^)\n]*[))])?\s*(?:(?:\*\*|__)\s*)?[::]?\s*(?:(?:\*\*|__)\s*)?$/gim;
|
|
35
35
|
const SOURCES_FUNCTION_PATTERN = /(^|\n)\s*(sources|source|citations|citation|references|reference|citation_card|source_cards|source_card)\s*\(/gim;
|
|
36
36
|
function isRecord(value) {
|
|
@@ -78,8 +78,172 @@ function firstBoundedString(record, keys, maximum) {
|
|
|
78
78
|
}
|
|
79
79
|
return undefined;
|
|
80
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
|
+
}
|
|
81
245
|
function validatedHttpUrl(raw, limits) {
|
|
82
|
-
const url = raw
|
|
246
|
+
const url = trimUrlTail(raw);
|
|
83
247
|
const actual = characterLength(url);
|
|
84
248
|
if (actual > limits.maxUrlCharacters) {
|
|
85
249
|
throw new SearchResponseParseError('limit', { actual, maximum: limits.maxUrlCharacters });
|
|
@@ -96,13 +260,31 @@ function validatedHttpUrl(raw, limits) {
|
|
|
96
260
|
}
|
|
97
261
|
function addSource(state, candidate, limits) {
|
|
98
262
|
const url = validatedHttpUrl(candidate.url, limits);
|
|
99
|
-
if (url === undefined
|
|
263
|
+
if (url === undefined)
|
|
100
264
|
return;
|
|
101
|
-
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
|
+
}
|
|
102
283
|
if (state.sources.length >= limits.maxSources) {
|
|
103
284
|
state.truncated = true;
|
|
104
285
|
return;
|
|
105
286
|
}
|
|
287
|
+
const index = state.sources.length;
|
|
106
288
|
state.sources.push(Object.freeze({
|
|
107
289
|
provider: 'search-api',
|
|
108
290
|
url,
|
|
@@ -110,40 +292,50 @@ function addSource(state, candidate, limits) {
|
|
|
110
292
|
...(candidate.snippet === undefined ? {} : { snippet: candidate.snippet }),
|
|
111
293
|
...(candidate.publishedAt === undefined ? {} : { publishedAt: candidate.publishedAt }),
|
|
112
294
|
}));
|
|
295
|
+
state.sourceIndexByUrl.set(url, index);
|
|
113
296
|
}
|
|
114
297
|
function extractUrls(text) {
|
|
115
298
|
const urls = [];
|
|
116
|
-
const
|
|
117
|
-
for (const match of text.matchAll(URL_PATTERN)) {
|
|
299
|
+
for (const match of text.matchAll(BARE_URL_PATTERN)) {
|
|
118
300
|
const raw = match[0];
|
|
119
301
|
if (raw === undefined)
|
|
120
302
|
continue;
|
|
121
|
-
|
|
122
|
-
if (seen.has(url))
|
|
123
|
-
continue;
|
|
124
|
-
seen.add(url);
|
|
125
|
-
urls.push(url);
|
|
303
|
+
urls.push(raw);
|
|
126
304
|
}
|
|
127
305
|
return urls;
|
|
128
306
|
}
|
|
129
|
-
function addMarkdownSources(
|
|
130
|
-
for (const
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
continue;
|
|
135
|
-
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);
|
|
136
312
|
addSource(state, {
|
|
137
|
-
url:
|
|
313
|
+
url: link.url,
|
|
138
314
|
...(title === undefined ? {} : { title }),
|
|
139
315
|
}, limits);
|
|
140
316
|
}
|
|
141
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
|
+
}
|
|
142
330
|
/** Extract URL-validated inline Markdown citations in answer order for internal orchestration. */
|
|
143
331
|
export function extractMarkdownCitationUrls(text, maximumUrlCharacters = DEFAULT_SEARCH_RESPONSE_PARSE_LIMITS.maxUrlCharacters) {
|
|
144
332
|
assertSafeInteger(maximumUrlCharacters, 'maximumUrlCharacters', false);
|
|
145
|
-
const state = {
|
|
146
|
-
|
|
333
|
+
const state = {
|
|
334
|
+
sourceIndexByUrl: new Map(),
|
|
335
|
+
sources: [],
|
|
336
|
+
truncated: false,
|
|
337
|
+
};
|
|
338
|
+
addMarkdownSources(scanMarkdownHttpLinks(text).links, state, {
|
|
147
339
|
...DEFAULT_SEARCH_RESPONSE_PARSE_LIMITS,
|
|
148
340
|
maxSources: Number.MAX_SAFE_INTEGER,
|
|
149
341
|
maxUrlCharacters: maximumUrlCharacters,
|
|
@@ -151,8 +343,9 @@ export function extractMarkdownCitationUrls(text, maximumUrlCharacters = DEFAULT
|
|
|
151
343
|
return Object.freeze(state.sources.map(source => source.url));
|
|
152
344
|
}
|
|
153
345
|
function addTextSources(text, state, limits) {
|
|
154
|
-
|
|
155
|
-
|
|
346
|
+
const markdown = scanMarkdownHttpLinks(text);
|
|
347
|
+
addMarkdownSources(markdown.links, state, limits);
|
|
348
|
+
const bareText = maskMarkdownLinkIntervals(text, markdown.intervals);
|
|
156
349
|
for (const url of extractUrls(bareText))
|
|
157
350
|
addSource(state, { url }, limits);
|
|
158
351
|
}
|
|
@@ -323,7 +516,11 @@ function splitDetailsBlockSources(text, state, limits) {
|
|
|
323
516
|
const openIndex = lower.lastIndexOf('<details', closeIndex);
|
|
324
517
|
if (openIndex === -1)
|
|
325
518
|
return undefined;
|
|
326
|
-
const temporary = {
|
|
519
|
+
const temporary = {
|
|
520
|
+
sourceIndexByUrl: new Map(),
|
|
521
|
+
sources: [],
|
|
522
|
+
truncated: false,
|
|
523
|
+
};
|
|
327
524
|
addTextSources(text.slice(openIndex, closeIndex + '</details>'.length), temporary, limits);
|
|
328
525
|
if (temporary.sources.length < 2 && !temporary.truncated)
|
|
329
526
|
return undefined;
|
|
@@ -337,7 +534,7 @@ function isLinkOnlyLine(line) {
|
|
|
337
534
|
const stripped = line.replace(/^\s*(?:[-*]|\d+\.)\s*/, '').trim();
|
|
338
535
|
return stripped.length > 0 && (stripped.startsWith('http://')
|
|
339
536
|
|| stripped.startsWith('https://')
|
|
340
|
-
||
|
|
537
|
+
|| scanMarkdownHttpLinks(stripped).links.length > 0);
|
|
341
538
|
}
|
|
342
539
|
function splitTailLinkBlock(text, state, limits) {
|
|
343
540
|
const lines = text.split(/\r?\n/);
|
|
@@ -368,7 +565,7 @@ function splitTailLinkBlock(text, state, limits) {
|
|
|
368
565
|
return undefined;
|
|
369
566
|
return lines.slice(0, tailStart).join('\n').trimEnd();
|
|
370
567
|
}
|
|
371
|
-
/** Convert Search API prose/source conventions
|
|
568
|
+
/** Convert Search API prose/source conventions with one bounded link scan and exact-URL enrichment. */
|
|
372
569
|
export function parseSearchAnswerText(text, limitOverrides = {}) {
|
|
373
570
|
const limits = { ...DEFAULT_SEARCH_RESPONSE_PARSE_LIMITS, ...limitOverrides };
|
|
374
571
|
validateLimits(limits);
|
|
@@ -376,14 +573,22 @@ export function parseSearchAnswerText(text, limitOverrides = {}) {
|
|
|
376
573
|
if (trimmed.length === 0) {
|
|
377
574
|
return Object.freeze({ answer: '', sources: Object.freeze([]), sourcesTruncated: false });
|
|
378
575
|
}
|
|
379
|
-
const trailingState = {
|
|
576
|
+
const trailingState = {
|
|
577
|
+
sourceIndexByUrl: new Map(),
|
|
578
|
+
sources: [],
|
|
579
|
+
truncated: false,
|
|
580
|
+
};
|
|
380
581
|
const answer = splitFunctionCallSources(trimmed, trailingState, limits)
|
|
381
582
|
?? splitHeadingSources(trimmed, trailingState, limits)
|
|
382
583
|
?? splitDetailsBlockSources(trimmed, trailingState, limits)
|
|
383
584
|
?? splitTailLinkBlock(trimmed, trailingState, limits)
|
|
384
585
|
?? trimmed;
|
|
385
|
-
const state = {
|
|
386
|
-
|
|
586
|
+
const state = {
|
|
587
|
+
sourceIndexByUrl: new Map(),
|
|
588
|
+
sources: [],
|
|
589
|
+
truncated: false,
|
|
590
|
+
};
|
|
591
|
+
addMarkdownSources(scanMarkdownHttpLinks(answer).links, state, limits);
|
|
387
592
|
for (const source of trailingState.sources)
|
|
388
593
|
addSource(state, source, limits);
|
|
389
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",
|