sh-mcp/packages/knowledge-base/src/client.ts

151 lines
5.6 KiB
TypeScript
Raw Normal View History

Phase 0b slice: monorepo scaffold + @sh-mcp/shared core + integration packages (#2) * Phase 0b slice: monorepo scaffold + shared core + integration packages The 0a-INDEPENDENT code slice (one-shot via af-0b-package-slice workflow: Haiku scaffold + Sonnet packages, Sonnet fix-to-green). Nothing deploys; no CDK/servers. - Monorepo scaffold: npm workspaces, strict TS (NodeNext), vitest (80% gate), eslint 9 flat config, prettier; ci.yaml/deploy.yaml callers (Node 24, enable-qemu). - @sh-mcp/shared: transport-agnostic core — Scope/AuthContext/ToolDef, ToolRegistry, redact()+maskValue() (PII), OpenAPI 3.1 generator. AUTH STUBBED behind an AuthProvider interface (TODO auth-layer-0a); JWT/aud/client_id/JWKS/deny-list deferred per design.md §2. - 9 integration packages (qbo, google-maps, internal-data, payments, knowledge-base, gmail, calendar, tasks, reminders): tools against shared, external deps mocked behind injected client interfaces; finance handlers call redact(). Verified green: tsc -b clean, vitest 245/245, eslint 0 errors. Auth mechanism intentionally deferred until the 0a spike resolves it (G16/§0.4). * Complete Cognito auth provider + Phase 1 build brief Finish the WIP CognitoAuthProvider (client_id allow-list as audience boundary, finance TTL ceiling, deny-list, scope-prefix stripping) with its test suite, and check in docs/build-plan-phase-1.md so the Phase 1 work has its governing brief in-tree (design.md §2.5). * ci: disable cdk synth for Phase 0b (no CDK app yet) The reusable ci-typescript-cdk workflow defaults run-cdk-synth: true, but the Phase 0b package scaffold has no cdk.json or stacks, so cdk synth fails with '--app is required'. Disable it here; Phase 1 re-enables it with the server CDK stubs.
2026-06-26 12:42:17 -04:00
/**
* Knowledge-base client interface.
*
* The production implementation calls Amazon Bedrock Knowledge Base Retrieve API.
* Swap the implementation by injecting a different KnowledgeBaseClient at the call
* site — tests pass a mock, production code uses BedrockKnowledgeBaseClient.
*
* Future note: when migrating to Salesforce Data Cloud, replace
* BedrockKnowledgeBaseClient with a DataCloudKnowledgeBaseClient that satisfies
* the same KnowledgeBaseClient interface.
*/
/** A single retrieval result returned by the knowledge base. */
export interface KnowledgeBaseResult {
/** Source document URI or human-readable reference (e.g. S3 URI, Notion page title). */
source: string;
/** Relevance score in the range [0, 1] as returned by the underlying retriever. */
score: number;
/** Text passage extracted from the source document. */
passage: string;
}
/** Options forwarded to the underlying retriever on each query. */
export interface RetrieveOptions {
/** Free-text query string. */
query: string;
/**
* Maximum number of results to return.
* Defaults to 5 if omitted; callers should not exceed 20.
*/
maxResults?: number;
}
/**
* The interface every knowledge-base client must satisfy.
* Production code uses BedrockKnowledgeBaseClient; tests supply a mock.
*/
export interface KnowledgeBaseClient {
retrieve(options: RetrieveOptions): Promise<KnowledgeBaseResult[]>;
}
// ---------------------------------------------------------------------------
// Production implementation — Bedrock Knowledge Base Retrieve API
// ---------------------------------------------------------------------------
/**
* Configuration for the production Bedrock KB client.
* All values come from environment variables; no defaults are hard-coded so
* that the module can be imported without triggering any AWS calls.
*/
export interface BedrockKnowledgeBaseClientConfig {
/** Bedrock Knowledge Base ID (e.g. "ABCD1234EF"). */
knowledgeBaseId: string;
/** AWS region (e.g. "us-east-1"). */
region: string;
}
/**
* Production Bedrock KB client.
*
* The @aws-sdk/client-bedrock-agent-runtime package is imported LAZILY inside
* retrieve() so that importing this module at the top level (e.g. in tests)
* does NOT trigger any network activity or AWS credential resolution.
*
* TODO(auth-layer): once the deferred auth layer is in place, thread the
* caller's AWS credentials / assumed role ARN through here if we want
* per-user IAM audit trails on Bedrock calls.
*/
export class BedrockKnowledgeBaseClient implements KnowledgeBaseClient {
private readonly config: BedrockKnowledgeBaseClientConfig;
constructor(config: BedrockKnowledgeBaseClientConfig) {
this.config = config;
}
/**
* Calls the Bedrock Retrieve API.
*
* The AWS SDK import is deferred to keep module load side-effect-free.
* If the environment lacks AWS credentials this will throw at call time,
* not at import time — which is the desired behaviour for testing.
*/
async retrieve(options: RetrieveOptions): Promise<KnowledgeBaseResult[]> {
// @aws-sdk/client-bedrock-agent-runtime is intentionally absent from package.json
// until the Lambda runtime bundle is assembled. The module specifier is stored in a
// variable so TypeScript skips static module resolution at build time.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const dynImport = (s: string): Promise<any> => new Function('s', 'return import(s)')(s) as Promise<any>;
interface BedrockRetrievalResult {
location?: { s3Location?: { uri?: string }; type?: string };
score?: number;
content?: { text?: string };
}
interface BedrockRetrieveResponse {
retrievalResults?: BedrockRetrievalResult[];
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { BedrockAgentRuntimeClient, RetrieveCommand } = (await dynImport('@aws-sdk/client-bedrock-agent-runtime')) as {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
BedrockAgentRuntimeClient: new (cfg: { region: string }) => { send: (cmd: any) => Promise<BedrockRetrieveResponse> };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
RetrieveCommand: new (input: any) => unknown;
};
const client = new BedrockAgentRuntimeClient({ region: this.config.region });
const command = new RetrieveCommand({
knowledgeBaseId: this.config.knowledgeBaseId,
retrievalQuery: { text: options.query },
retrievalConfiguration: {
vectorSearchConfiguration: {
numberOfResults: options.maxResults ?? 5,
},
},
});
const response = await client.send(command);
const rawResults = response.retrievalResults ?? [];
return rawResults.map((r: BedrockRetrievalResult) => ({
source: r.location?.s3Location?.uri ?? r.location?.type ?? 'unknown',
score: r.score ?? 0,
passage: r.content?.text ?? '',
}));
}
}
/**
* Build a production BedrockKnowledgeBaseClient from environment variables.
*
* Expected env vars:
* KNOWLEDGE_BASE_ID — Bedrock Knowledge Base ID
* AWS_REGION — AWS region (falls back to 'us-east-1')
*
* This factory is intentionally NOT called at module import time.
*/
export function createBedrockClientFromEnv(): BedrockKnowledgeBaseClient {
const knowledgeBaseId = process.env['KNOWLEDGE_BASE_ID'];
if (!knowledgeBaseId) {
throw new Error(
'KNOWLEDGE_BASE_ID environment variable is required for the production KB client'
);
}
return new BedrockKnowledgeBaseClient({
knowledgeBaseId,
region: process.env['AWS_REGION'] ?? 'us-east-1',
});
}