@forinda/kickjs-ai 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +87 -0
- package/dist/index.d.mts +1973 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +2474 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +98 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/constants.ts","../src/decorators.ts","../src/zod-to-json-schema.ts","../src/ai.adapter.ts","../src/providers/base.ts","../src/providers/openai.ts","../src/providers/anthropic.ts","../src/prompts/prompt.ts","../src/memory/in-memory.ts","../src/memory/sliding-window.ts","../src/rag/in-memory.ts","../src/rag/pgvector.ts","../src/rag/qdrant.ts","../src/rag/pinecone.ts","../src/rag/rag-service.ts"],"sourcesContent":["import { createToken } from '@forinda/kickjs'\nimport type { VectorStore } from './rag/types'\nimport type { AiProvider, AiToolOptions } from './types'\n\n/**\n * Metadata key for the `@AiTool` decorator.\n *\n * Using `createToken` for metadata keys (rather than a raw `Symbol`)\n * gives a collision-safe, type-carrying identifier: the phantom type\n * parameter flows through `getMethodMetaOrUndefined` so consumers get\n * `AiToolOptions` back without a manual cast, and reference-equality\n * guarantees that two separate definitions can never shadow each other\n * even if the package is loaded more than once.\n */\nexport const AI_TOOL_METADATA = createToken<AiToolOptions>('kickjs.ai.tool')\n\n/**\n * DI token for the active AI provider.\n *\n * Injected via `@Inject(AI_PROVIDER)` in services or use-cases that\n * need to call an LLM. The adapter registers the concrete provider\n * (OpenAI, Anthropic, Google, Ollama) during `beforeStart`.\n *\n * @example\n * ```ts\n * @Service()\n * export class SummarizeService {\n * constructor(@Inject(AI_PROVIDER) private ai: AiProvider) {}\n *\n * async summarize(text: string) {\n * const res = await this.ai.chat({\n * messages: [\n * { role: 'system', content: 'Summarize in 2 sentences.' },\n * { role: 'user', content: text },\n * ],\n * })\n * return res.content\n * }\n * }\n * ```\n */\nexport const AI_PROVIDER = createToken<AiProvider>('kickjs.ai.provider')\n\n/**\n * DI token for the active vector store backend.\n *\n * Injected via `@Inject(VECTOR_STORE)` in services that need\n * retrieval-augmented generation. The adapter does not register a\n * default — users bind the backend they want at bootstrap time,\n * typically `InMemoryVectorStore` for development/tests and\n * `PgVectorStore` / `QdrantStore` / `PineconeStore` for production.\n *\n * @example\n * ```ts\n * import { bootstrap, getEnv } from '@forinda/kickjs'\n * import { AiAdapter, InMemoryVectorStore, VECTOR_STORE } from '@forinda/kickjs-ai'\n *\n * export const app = await bootstrap({\n * modules,\n * adapters: [\n * new AiAdapter({\n * provider: new OpenAIProvider({ apiKey: getEnv('OPENAI_API_KEY') }),\n * }),\n * ],\n * plugins: [\n * {\n * name: 'vector-store',\n * register: (container) => {\n * container.registerInstance(VECTOR_STORE, new InMemoryVectorStore())\n * },\n * },\n * ],\n * })\n * ```\n */\nexport const VECTOR_STORE = createToken<VectorStore>('kickjs.ai.vector_store')\n","import { setMethodMeta, getMethodMetaOrUndefined } from '@forinda/kickjs'\nimport { AI_TOOL_METADATA } from './constants'\nimport type { AiToolOptions } from './types'\n\n/**\n * Mark a controller method as an AI-callable tool.\n *\n * At startup, the `AiAdapter` scans all `@Controller` classes in the\n * DI container for this decorator and builds a tool registry. When a\n * service calls `ai.chat({ ..., tools: 'auto' })`, the framework\n * passes the registered tools to the provider, the model may call\n * them, and the framework dispatches back through the normal Express\n * pipeline — so tool calls go through auth, validation, and logging\n * just like external HTTP requests.\n *\n * The input schema is derived from the route's `body` Zod schema:\n *\n * @example\n * ```ts\n * import { Controller, Post, type Ctx } from '@forinda/kickjs'\n * import { AiTool } from '@forinda/kickjs-ai'\n * import { createTaskSchema } from './dtos/create-task.dto'\n *\n * @Controller('/tasks')\n * export class TaskController {\n * @Post('/', { body: createTaskSchema, name: 'CreateTask' })\n * @AiTool({ description: 'Create a new task' })\n * create(ctx: Ctx<KickRoutes.TaskController['create']>) {\n * return this.createTaskUseCase.execute(ctx.body)\n * }\n * }\n * ```\n */\nexport function AiTool(options: AiToolOptions): MethodDecorator {\n return (target, propertyKey) => {\n setMethodMeta(AI_TOOL_METADATA, options, target, propertyKey as string)\n }\n}\n\n/** Read the AI tool metadata attached to a method, if any. */\nexport function getAiToolMeta(target: object, method: string): AiToolOptions | undefined {\n return getMethodMetaOrUndefined<AiToolOptions>(AI_TOOL_METADATA, target, method)\n}\n\n/** Check whether a method was decorated with `@AiTool`. */\nexport function isAiTool(target: object, method: string): boolean {\n return getAiToolMeta(target, method) !== undefined\n}\n","/**\n * Minimal Zod v4+ schema parser.\n *\n * Mirrors the helper in `@forinda/kickjs-mcp` and the `zodSchemaParser`\n * in `@forinda/kickjs-swagger`. Zod v4 ships with a native\n * `.toJSONSchema()` instance method, so this is a type guard + a call.\n *\n * Kept in-package so the AI adapter has no cross-package dependency\n * on MCP or Swagger. If KickJS ever extracts a shared\n * `@forinda/kickjs-schema` utility, all three packages can switch\n * to it in one PR.\n */\n\n/**\n * Check whether a value looks like a Zod v4+ schema.\n *\n * Uses structural duck-typing: the object has `safeParse` (all Zod\n * versions) AND `toJSONSchema` (Zod v4+). This avoids importing Zod\n * as a value, which would force it to become a runtime dep.\n */\nexport function isZodSchema(schema: unknown): boolean {\n return (\n schema != null &&\n typeof schema === 'object' &&\n typeof (schema as { safeParse?: unknown }).safeParse === 'function' &&\n typeof (schema as { toJSONSchema?: unknown }).toJSONSchema === 'function'\n )\n}\n\n/**\n * Convert a Zod v4+ schema to a JSON Schema object, stripping the\n * top-level `$schema` key so the output can be embedded inside an\n * AI tool definition directly.\n *\n * Returns `null` if the input doesn't look like a Zod schema. Callers\n * should fall back to an empty-object input schema in that case.\n */\nexport function zodToJsonSchema(schema: unknown): Record<string, unknown> | null {\n if (!isZodSchema(schema)) return null\n const { $schema: _ignored, ...rest } = (\n schema as { toJSONSchema: () => Record<string, unknown> }\n ).toJSONSchema() as Record<string, unknown>\n return rest\n}\n","import {\n Logger,\n METADATA,\n Scope,\n getClassMeta,\n type AdapterContext,\n type AppAdapter,\n type Constructor,\n type RouteDefinition,\n} from '@forinda/kickjs'\nimport { AI_PROVIDER } from './constants'\nimport { getAiToolMeta } from './decorators'\nimport type { RunAgentWithMemoryOptions } from './memory/types'\nimport { zodToJsonSchema } from './zod-to-json-schema'\nimport type {\n AiAdapterOptions,\n AiProvider,\n AiToolDefinition,\n ChatMessage,\n ChatToolDefinition,\n RunAgentOptions,\n RunAgentResult,\n} from './types'\n\nconst log = Logger.for('AiAdapter')\n\n/**\n * Register an AI provider in the DI container, discover every\n * `@AiTool`-decorated controller method, and run agent loops that\n * dispatch tool calls through the Express pipeline.\n *\n * The adapter plays the same role for AI as the MCP adapter plays for\n * external clients: it's the glue between the framework's metadata\n * (Zod schemas, route decorators, DI container) and a runtime that\n * can actually call LLMs and execute tools. Both adapters reuse the\n * framework's `onRouteMount` hook to discover tools at startup.\n *\n * @example\n * ```ts\n * import { bootstrap, getEnv } from '@forinda/kickjs'\n * import { AiAdapter, OpenAIProvider } from '@forinda/kickjs-ai'\n *\n * export const app = await bootstrap({\n * modules,\n * adapters: [\n * new AiAdapter({\n * provider: new OpenAIProvider({ apiKey: getEnv('OPENAI_API_KEY') }),\n * }),\n * ],\n * })\n * ```\n *\n * Then in any service:\n *\n * ```ts\n * @Service()\n * class AgentService {\n * @Autowired() private readonly ai!: AiAdapter\n *\n * async handleQuery(userPrompt: string) {\n * const result = await this.ai.runAgent({\n * messages: [\n * { role: 'system', content: 'You can create tasks via tools.' },\n * { role: 'user', content: userPrompt },\n * ],\n * tools: 'auto', // use every @AiTool-decorated method\n * })\n * return result.content\n * }\n * }\n * ```\n */\nexport class AiAdapter implements AppAdapter {\n readonly name = 'AiAdapter'\n\n private readonly provider: AiProvider\n\n /** Controllers collected during the mount phase, in insertion order. */\n private readonly mountedControllers: Array<{\n controller: Constructor\n mountPath: string\n }> = []\n\n /** Tool definitions built during `beforeStart` from `@AiTool` metadata. */\n private readonly tools: AiToolDefinition[] = []\n\n /**\n * Base URL of the running KickJS HTTP server, captured in `afterStart`.\n * Agent tool dispatch makes internal HTTP requests against this base\n * URL so calls flow through the normal Express pipeline (middleware,\n * validation, auth, logging, error handling).\n */\n private serverBaseUrl: string | null = null\n\n constructor(options: AiAdapterOptions) {\n this.provider = options.provider\n }\n\n /** Return the active provider. Useful for services that want the raw API. */\n getProvider(): AiProvider {\n return this.provider\n }\n\n /** Return the discovered tool registry. Primarily for tests and debug UIs. */\n getTools(): readonly AiToolDefinition[] {\n return this.tools\n }\n\n /**\n * Override the server base URL. Used by tests that spin up an\n * ephemeral http.Server and can't rely on the framework's\n * `afterStart` hook to supply it.\n */\n setServerBaseUrl(url: string | null): void {\n this.serverBaseUrl = url\n }\n\n // ── Lifecycle ──────────────────────────────────────────────────────────\n\n /**\n * Record every mounted controller so `beforeStart` can walk them\n * looking for `@AiTool` decorations. We don't scan here because\n * onRouteMount fires per-controller and we want the scan to run\n * once against the full set.\n */\n onRouteMount(controller: Constructor, mountPath: string): void {\n this.mountedControllers.push({ controller, mountPath })\n }\n\n /**\n * Register the provider in the DI container and run the tool scan.\n *\n * The adapter itself is also registered under its class constructor\n * so services can inject the adapter directly (to call `runAgent`)\n * while other services inject just the provider via `AI_PROVIDER`\n * for plain `chat` / `embed` calls.\n */\n beforeStart({ container }: AdapterContext): void {\n container.registerFactory(AI_PROVIDER, () => this.provider, Scope.SINGLETON)\n container.registerInstance(AiAdapter, this)\n\n for (const { controller, mountPath } of this.mountedControllers) {\n const routes = getClassMeta<RouteDefinition[]>(METADATA.ROUTES, controller, [])\n for (const route of routes) {\n const tool = this.tryBuildTool(controller, mountPath, route)\n if (tool) this.tools.push(tool)\n }\n }\n\n log.info(\n `AiAdapter ready — provider: ${this.provider.name}, ${this.tools.length} tool(s) discovered`,\n )\n }\n\n /**\n * Capture the running server's address so agent dispatch can make\n * internal HTTP requests against the actual port. Runs after the\n * HTTP server is listening, so `server.address()` returns a real\n * `AddressInfo` here.\n */\n afterStart(ctx: AdapterContext): void {\n this.serverBaseUrl = this.resolveServerBaseUrl(ctx.server)\n log.debug(`AiAdapter agent dispatch target: ${this.serverBaseUrl ?? '(unknown)'}`)\n }\n\n /** Best-effort cleanup. Providers are currently stateless HTTP clients. */\n async shutdown(): Promise<void> {\n this.serverBaseUrl = null\n log.debug('AiAdapter shutdown complete')\n }\n\n // ── Agent loop ──────────────────────────────────────────────────────────\n\n /**\n * Run a tool-calling agent loop.\n *\n * Calls the provider with the given messages and tools, dispatches\n * any tool calls the model emits, feeds the results back into the\n * conversation, and repeats until the model responds with plain text\n * (no more tool calls) or `maxSteps` is reached.\n *\n * Tool dispatch goes through the Express pipeline via internal HTTP\n * requests — same pattern as the MCP adapter — so middleware, auth,\n * validation, logging, and error handling all apply to tool calls\n * the same way they apply to external client requests.\n *\n * @example\n * ```ts\n * const result = await adapter.runAgent({\n * messages: [\n * { role: 'system', content: 'Create tasks the user asks for.' },\n * { role: 'user', content: 'Create a high-priority task titled Ship v3.' },\n * ],\n * tools: 'auto',\n * maxSteps: 5,\n * })\n * console.log(result.content) // assistant's final reply\n * console.log(result.messages) // full history including tool calls\n * console.log(result.steps) // how many rounds it took\n * ```\n */\n async runAgent(options: RunAgentOptions): Promise<RunAgentResult> {\n const maxSteps = options.maxSteps ?? 8\n const resolvedTools = this.resolveTools(options.tools ?? 'auto')\n\n const messages: ChatMessage[] = [...options.messages]\n let steps = 0\n const usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 }\n\n for (let i = 0; i < maxSteps; i++) {\n steps++\n\n const response = await this.provider.chat(\n {\n messages,\n model: options.model,\n tools: resolvedTools.length > 0 ? resolvedTools : undefined,\n },\n {\n temperature: options.temperature,\n maxTokens: options.maxTokens,\n topP: options.topP,\n stopSequences: options.stopSequences,\n signal: options.signal,\n },\n )\n\n if (response.usage) {\n usage.promptTokens += response.usage.promptTokens\n usage.completionTokens += response.usage.completionTokens\n usage.totalTokens += response.usage.totalTokens\n }\n\n // If the model didn't request any tools, the loop is done.\n if (!response.toolCalls || response.toolCalls.length === 0) {\n messages.push({ role: 'assistant', content: response.content })\n return {\n content: response.content,\n messages,\n steps,\n usage: usage.totalTokens > 0 ? usage : undefined,\n }\n }\n\n // Record the assistant's tool-calling turn in the transcript.\n messages.push({\n role: 'assistant',\n content: response.content,\n toolCalls: response.toolCalls,\n })\n\n // Dispatch every tool call in parallel and feed the results back\n // to the model in the next iteration. Parallel dispatch is safe\n // because each tool call hits an independent HTTP route; any\n // side-effect ordering is the model's responsibility to request\n // sequentially.\n const results = await Promise.all(\n response.toolCalls.map((call) => this.dispatchToolCall(call)),\n )\n for (const result of results) {\n messages.push(result)\n }\n }\n\n // maxSteps exhausted — return whatever we have so the caller can react.\n const lastAssistant = messages\n .slice()\n .reverse()\n .find((m) => m.role === 'assistant')\n return {\n content: lastAssistant?.content ?? '',\n messages,\n steps,\n usage: usage.totalTokens > 0 ? usage : undefined,\n maxStepsReached: true,\n }\n }\n\n /**\n * Memory-aware agent turn.\n *\n * Wraps `runAgent` with an automatic \"read history → append user\n * message → run loop → persist assistant response\" cycle. Services\n * that want multi-turn conversations don't need to manage the\n * plumbing themselves — pass a `ChatMemory` and a user message,\n * get back the agent's response, and the memory is updated.\n *\n * System prompt handling:\n * - If the memory is empty AND `systemPrompt` is provided, the\n * system prompt is persisted as the first message in the\n * session. It stays put for every subsequent turn.\n * - On follow-up turns, the existing system prompt is reused\n * from memory; the `systemPrompt` option is ignored to keep\n * the session persona stable.\n *\n * Tool result persistence:\n * - By default, tool messages are NOT persisted to memory —\n * they're usually large API responses the user doesn't need\n * on later turns, and including them blows up prompt tokens\n * unnecessarily. Set `persistToolResults: true` to keep them\n * (useful for debugging / full-transcript replay).\n * - Assistant messages with tool calls ARE persisted so the\n * conversation shows what the agent did.\n *\n * @example\n * ```ts\n * @Service()\n * class ChatService {\n * @Autowired() private ai!: AiAdapter\n * private readonly memory = new InMemoryChatMemory()\n *\n * async handle(userMessage: string) {\n * const result = await this.ai.runAgentWithMemory({\n * memory: this.memory,\n * userMessage,\n * systemPrompt: 'You are a helpful assistant.',\n * tools: 'auto',\n * })\n * return result.content\n * }\n * }\n * ```\n */\n async runAgentWithMemory(options: RunAgentWithMemoryOptions): Promise<RunAgentResult> {\n const history = await options.memory.get()\n const messages: ChatMessage[] = [...history]\n\n // First-turn system prompt — only injected if the memory is\n // empty. Later turns rely on the persisted system prompt.\n const isFirstTurn = messages.length === 0\n if (isFirstTurn && options.systemPrompt) {\n const systemMessage: ChatMessage = { role: 'system', content: options.systemPrompt }\n messages.push(systemMessage)\n await options.memory.add(systemMessage)\n }\n\n const userMessage: ChatMessage = { role: 'user', content: options.userMessage }\n messages.push(userMessage)\n await options.memory.add(userMessage)\n\n const result = await this.runAgent({\n messages,\n model: options.model,\n tools: options.tools,\n maxSteps: options.maxSteps,\n temperature: options.temperature,\n maxTokens: options.maxTokens,\n topP: options.topP,\n stopSequences: options.stopSequences,\n signal: options.signal,\n })\n\n // Persist every message the loop produced AFTER the user turn.\n // Slice starts at messages.length because everything up to there\n // is already in memory.\n const newMessages = result.messages.slice(messages.length)\n const toPersist = options.persistToolResults\n ? newMessages\n : newMessages.filter((m) => m.role !== 'tool')\n if (toPersist.length > 0) {\n await options.memory.add(toPersist)\n }\n\n return result\n }\n\n // ── Tool resolution and dispatch ────────────────────────────────────────\n\n /**\n * Expand an agent `tools` option to an explicit array. `'auto'`\n * resolves to the full discovered registry; an explicit array is\n * passed through unchanged (so callers can restrict the agent to a\n * subset of tools).\n */\n private resolveTools(spec: 'auto' | ChatToolDefinition[]): ChatToolDefinition[] {\n if (spec === 'auto') return this.tools\n return spec\n }\n\n /**\n * Dispatch a single tool call through the Express pipeline by\n * making an internal HTTP request matching the underlying route's\n * method + path + body/query.\n *\n * Returns a `ChatMessage` with `role: 'tool'` suitable for feeding\n * back into the next `provider.chat` call. Non-2xx responses are\n * surfaced as tool error messages rather than throwing, so the\n * agent loop can let the model recover.\n */\n private async dispatchToolCall(call: {\n id: string\n name: string\n arguments: Record<string, unknown>\n }): Promise<ChatMessage> {\n const tool = this.tools.find((t) => t.name === call.name)\n if (!tool) {\n return {\n role: 'tool',\n toolCallId: call.id,\n content: JSON.stringify({ error: `Tool not found: ${call.name}` }),\n }\n }\n if (!this.serverBaseUrl) {\n return {\n role: 'tool',\n toolCallId: call.id,\n content: JSON.stringify({\n error: `Cannot dispatch ${call.name}: HTTP server address not yet captured`,\n }),\n }\n }\n\n const args = call.arguments ?? {}\n const { path, remainingArgs } = this.substitutePathParams(tool.mountPath, args)\n const method = tool.httpMethod.toUpperCase()\n const hasBody = method === 'POST' || method === 'PUT' || method === 'PATCH'\n\n let url = `${this.serverBaseUrl}${path}`\n const init: RequestInit = {\n method,\n headers: {\n accept: 'application/json',\n 'x-ai-tool': tool.name,\n },\n }\n if (hasBody) {\n ;(init.headers as Record<string, string>)['content-type'] = 'application/json'\n init.body = JSON.stringify(remainingArgs)\n } else if (Object.keys(remainingArgs).length > 0) {\n const qs = new URLSearchParams()\n for (const [key, value] of Object.entries(remainingArgs)) {\n if (value === undefined || value === null) continue\n qs.append(key, typeof value === 'string' ? value : JSON.stringify(value))\n }\n const sep = url.includes('?') ? '&' : '?'\n url = `${url}${sep}${qs.toString()}`\n }\n\n try {\n const res = await fetch(url, init)\n const text = await res.text()\n const content = res.ok\n ? text || `(${res.status} ${res.statusText})`\n : JSON.stringify({\n error: `Tool ${call.name} returned ${res.status}`,\n body: text,\n })\n return { role: 'tool', toolCallId: call.id, content }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n log.error(err as Error, `AiAdapter: tool dispatch failed for ${call.name}`)\n return {\n role: 'tool',\n toolCallId: call.id,\n content: JSON.stringify({ error: `Dispatch error: ${message}` }),\n }\n }\n }\n\n // ── Scan helpers (mirror McpAdapter for consistency) ───────────────────\n\n /**\n * Build an `AiToolDefinition` for a route decorated with `@AiTool`.\n * Skips routes without the decorator so the registry only exposes\n * deliberately opted-in methods.\n */\n private tryBuildTool(\n controller: Constructor,\n mountPath: string,\n route: RouteDefinition,\n ): AiToolDefinition | null {\n const meta = getAiToolMeta(controller.prototype, route.handlerName)\n if (!meta) return null\n\n const candidateSchema = meta.inputSchema ?? route.validation?.body ?? route.validation?.query\n const inputSchema = zodToJsonSchema(candidateSchema) ?? {\n type: 'object',\n properties: {},\n additionalProperties: false,\n }\n\n return {\n name: meta.name ?? `${controller.name}.${route.handlerName}`,\n description: meta.description,\n inputSchema,\n httpMethod: route.method.toUpperCase(),\n mountPath: this.joinMountPath(mountPath, route.path),\n }\n }\n\n /**\n * Join a module mount path with the route-level sub-path. Same\n * helper as McpAdapter's — kept local so the two packages don't\n * couple via a shared util file.\n */\n private joinMountPath(mountPath: string, routePath: string): string {\n const base = mountPath.endsWith('/') ? mountPath.slice(0, -1) : mountPath\n if (!routePath || routePath === '/') return base\n const sub = routePath.startsWith('/') ? routePath : `/${routePath}`\n return `${base}${sub}`\n }\n\n /**\n * Substitute Express-style `:param` placeholders in the mount path\n * with values pulled from the tool call arguments. Consumed keys\n * are removed from the remaining args so they aren't sent twice\n * (once in the path, once in the body/query).\n */\n private substitutePathParams(\n mountPath: string,\n args: Record<string, unknown>,\n ): { path: string; remainingArgs: Record<string, unknown> } {\n const remaining: Record<string, unknown> = { ...args }\n const path = mountPath.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_match, param: string) => {\n if (param in remaining) {\n const value = remaining[param]\n delete remaining[param]\n return encodeURIComponent(String(value))\n }\n return `:${param}`\n })\n return { path, remainingArgs: remaining }\n }\n\n /**\n * Resolve the running server's base URL from a Node `http.Server`\n * instance. Same handling as McpAdapter: IPv6 bracketing, rewrite\n * of 0.0.0.0/:: to 127.0.0.1.\n */\n private resolveServerBaseUrl(server: AdapterContext['server']): string | null {\n if (!server) return null\n const address = server.address()\n if (!address || typeof address === 'string') return null\n let host = address.address\n if (host === '::' || host === '0.0.0.0' || host === '') host = '127.0.0.1'\n if (host.includes(':') && !host.startsWith('[')) host = `[${host}]`\n return `http://${host}:${address.port}`\n }\n}\n","/**\n * Provider-side helpers shared by every built-in `AiProvider`\n * implementation.\n *\n * Each provider in `packages/ai/src/providers/` implements the\n * `AiProvider` interface from `../types`. This file holds the bits\n * that all of them need: HTTP error mapping, JSON parsing, SSE line\n * splitting for streaming responses. Keeping these here means each\n * provider's main file stays focused on the wire-format translation\n * specific to its vendor.\n */\n\n/**\n * Error thrown by built-in providers when the upstream API returns a\n * non-2xx status. Carries the HTTP status, the raw response body, and\n * a parsed error object when available, so callers can branch on\n * specific failure modes (auth, rate limit, content filter, etc.).\n */\nexport class ProviderError extends Error {\n readonly status: number\n readonly body: string\n readonly parsedBody?: unknown\n\n constructor(status: number, body: string, message?: string) {\n super(message ?? `Provider request failed with status ${status}`)\n this.name = 'ProviderError'\n this.status = status\n this.body = body\n try {\n this.parsedBody = JSON.parse(body)\n } catch {\n // Body wasn't JSON; leave parsedBody undefined\n }\n }\n}\n\n/**\n * POST a JSON payload to a URL and parse the JSON response. Throws a\n * `ProviderError` on non-2xx status codes so the caller never has to\n * check `res.ok` itself.\n *\n * Auth headers are the caller's responsibility. Different providers\n * use different conventions — OpenAI uses `Authorization: Bearer ...`,\n * Anthropic uses `x-api-key: ...`, Google uses `?key=...` in the URL —\n * so this helper stays neutral and lets each provider build exactly\n * the headers it needs.\n */\nexport async function postJson<T>(\n url: string,\n body: unknown,\n options: {\n headers?: Record<string, string>\n signal?: AbortSignal\n } = {},\n): Promise<T> {\n const res = await fetch(url, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n ...options.headers,\n },\n body: JSON.stringify(body),\n signal: options.signal,\n })\n\n if (!res.ok) {\n const text = await res.text()\n throw new ProviderError(res.status, text)\n }\n\n return (await res.json()) as T\n}\n\n/**\n * POST a JSON payload and stream the response body as a sequence of\n * SSE-style `data: ...` events. Each yielded value is the raw payload\n * after the `data: ` prefix is stripped — provider code is responsible\n * for parsing it as JSON (or detecting the `[DONE]` sentinel that\n * OpenAI uses to signal end-of-stream).\n *\n * Implementation notes:\n * - Uses the global `fetch` ReadableStream so it works in Node 20+\n * without depending on `node-fetch` or `eventsource-parser`.\n * - Buffers partial lines across chunk boundaries; an SSE event can\n * arrive split across two TCP packets.\n * - Skips empty lines and lines that don't start with `data: ` per\n * the SSE spec.\n * - Aborts cleanly via the optional AbortSignal — the caller's\n * `for await` loop will throw `AbortError` if the signal fires.\n */\nexport async function* postJsonStream(\n url: string,\n body: unknown,\n options: {\n headers?: Record<string, string>\n signal?: AbortSignal\n } = {},\n): AsyncGenerator<string> {\n const res = await fetch(url, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n accept: 'text/event-stream',\n ...options.headers,\n },\n body: JSON.stringify(body),\n signal: options.signal,\n })\n\n if (!res.ok) {\n const text = await res.text()\n throw new ProviderError(res.status, text)\n }\n if (!res.body) {\n throw new ProviderError(res.status, '', 'Provider streaming response had no body')\n }\n\n const reader = res.body.getReader()\n const decoder = new TextDecoder('utf-8')\n let buffer = ''\n\n try {\n while (true) {\n const { value, done } = await reader.read()\n if (done) break\n buffer += decoder.decode(value, { stream: true })\n\n // Process complete lines; keep any partial trailing line in the\n // buffer for the next iteration.\n let newlineIdx: number\n while ((newlineIdx = buffer.indexOf('\\n')) !== -1) {\n const line = buffer.slice(0, newlineIdx).trim()\n buffer = buffer.slice(newlineIdx + 1)\n if (line.length === 0) continue\n if (!line.startsWith('data:')) continue\n const payload = line.slice(5).trim()\n if (payload.length === 0) continue\n yield payload\n }\n }\n\n // Flush any final line that didn't end with a newline.\n const tail = buffer.trim()\n if (tail.startsWith('data:')) {\n const payload = tail.slice(5).trim()\n if (payload.length > 0) yield payload\n }\n } finally {\n // Defensive: release the reader so the underlying socket can be\n // closed even if the consumer broke out of its for-await loop early.\n try {\n reader.releaseLock()\n } catch {\n // Reader might already be released; ignore.\n }\n }\n}\n","import type {\n AiProvider,\n ChatChunk,\n ChatInput,\n ChatMessage,\n ChatOptions,\n ChatResponse,\n EmbedInput,\n} from '../types'\nimport { postJson, postJsonStream, ProviderError } from './base'\n\n/**\n * Configuration for the built-in OpenAI provider.\n *\n * The base URL is configurable so the same provider class can target\n * any OpenAI-compatible endpoint — Azure OpenAI, Ollama's\n * `/v1/chat/completions` shim, OpenRouter, vLLM, and so on. The\n * provider only assumes the wire shape, not the hostname.\n */\nexport interface OpenAIProviderOptions {\n /** API key sent as `Authorization: Bearer <apiKey>`. Required. */\n apiKey: string\n /** Override base URL. Defaults to `https://api.openai.com/v1`. */\n baseURL?: string\n /** Default chat model used when `ChatInput.model` is not set. */\n defaultChatModel?: string\n /** Default embedding model used by `embed()`. */\n defaultEmbedModel?: string\n /**\n * OpenAI organization header. Optional. Some accounts need it; most\n * don't. If unset, the header is omitted entirely.\n */\n organization?: string\n /**\n * Provider name to expose on `provider.name`. Defaults to `'openai'`\n * but can be overridden to label compatible endpoints — e.g.\n * `'ollama'` if pointing baseURL at a local Ollama instance.\n */\n name?: string\n}\n\n/**\n * Built-in OpenAI provider.\n *\n * Implements the framework's `AiProvider` interface using nothing but\n * the global `fetch` API (Node 20+). Translates the framework's\n * normalized chat shape to OpenAI's `/chat/completions` wire format\n * and back, including streaming via SSE.\n *\n * Tool calling is wired in this provider but the agent loop that\n * actually invokes tools and feeds results back to the model lives in\n * a later phase — for now, `chat()` and `stream()` surface tool calls\n * via `ChatResponse.toolCalls` so callers can react.\n *\n * @example\n * ```ts\n * import { bootstrap, getEnv } from '@forinda/kickjs'\n * import { AiAdapter, OpenAIProvider } from '@forinda/kickjs-ai'\n *\n * export const app = await bootstrap({\n * modules,\n * adapters: [\n * new AiAdapter({\n * provider: new OpenAIProvider({\n * apiKey: getEnv('OPENAI_API_KEY'),\n * defaultChatModel: 'gpt-4o-mini',\n * }),\n * }),\n * ],\n * })\n * ```\n */\nexport class OpenAIProvider implements AiProvider {\n readonly name: string\n\n private readonly baseURL: string\n private readonly defaultChatModel: string\n private readonly defaultEmbedModel: string\n /**\n * Full header map passed to every request. Includes the bearer auth\n * header and the optional openai-organization header. Constructed\n * once in the constructor so per-call code just spreads it into the\n * fetch init.\n */\n private readonly headers: Record<string, string>\n\n constructor(options: OpenAIProviderOptions) {\n if (!options.apiKey) {\n throw new Error('OpenAIProvider: apiKey is required')\n }\n this.baseURL = (options.baseURL ?? 'https://api.openai.com/v1').replace(/\\/$/, '')\n this.defaultChatModel = options.defaultChatModel ?? 'gpt-4o-mini'\n this.defaultEmbedModel = options.defaultEmbedModel ?? 'text-embedding-3-small'\n this.name = options.name ?? 'openai'\n this.headers = {\n authorization: `Bearer ${options.apiKey}`,\n ...(options.organization ? { 'openai-organization': options.organization } : {}),\n }\n }\n\n /**\n * Non-streaming chat completion.\n *\n * Translates the framework's `ChatInput` to OpenAI's chat completion\n * payload, posts it, and normalizes the response back to a\n * `ChatResponse`. Tool calls are surfaced on the response so callers\n * can decide whether to feed them back into a tool registry.\n */\n async chat(input: ChatInput, options: ChatOptions = {}): Promise<ChatResponse> {\n const payload = this.buildChatPayload(input, options, /* stream */ false)\n const data = await postJson<OpenAIChatResponse>(`${this.baseURL}/chat/completions`, payload, {\n headers: this.headers,\n signal: options.signal,\n })\n return this.normalizeChatResponse(data)\n }\n\n /**\n * Streaming chat completion. Yields `ChatChunk`s as deltas arrive\n * over the wire and emits one final chunk with `done: true` after\n * the upstream `[DONE]` sentinel.\n *\n * Cancellation via `options.signal` is supported end-to-end — the\n * underlying fetch is aborted and the consumer's `for await` loop\n * throws `AbortError`.\n */\n async *stream(input: ChatInput, options: ChatOptions = {}): AsyncIterable<ChatChunk> {\n const payload = this.buildChatPayload(input, options, /* stream */ true)\n const events = postJsonStream(`${this.baseURL}/chat/completions`, payload, {\n headers: this.headers,\n signal: options.signal,\n })\n\n let sawAnyChunk = false\n\n for await (const raw of events) {\n // OpenAI signals end-of-stream with a literal `[DONE]` payload\n // (not JSON). Translate to a final framework chunk.\n if (raw === '[DONE]') {\n yield { content: '', done: true }\n return\n }\n\n let parsed: OpenAIStreamChunk\n try {\n parsed = JSON.parse(raw) as OpenAIStreamChunk\n } catch {\n // Malformed chunk — ignore rather than crash the stream.\n continue\n }\n\n const choice = parsed.choices?.[0]\n if (!choice) continue\n\n const deltaContent = choice.delta?.content ?? ''\n const toolCallDelta = this.firstToolCallDelta(choice.delta?.tool_calls)\n\n sawAnyChunk = true\n const chunk: ChatChunk = {\n content: deltaContent,\n done: false,\n }\n if (toolCallDelta) chunk.toolCallDelta = toolCallDelta\n yield chunk\n }\n\n // If the stream closed without a [DONE] sentinel, still emit a\n // terminating chunk so consumers know to stop reading.\n if (sawAnyChunk) {\n yield { content: '', done: true }\n }\n }\n\n /**\n * Generate embeddings for a string or array of strings.\n *\n * Returns vectors in input order. Single-string input still gets a\n * length-1 array back, so callers can use the same indexed access\n * pattern regardless of input shape.\n */\n async embed(input: EmbedInput): Promise<number[][]> {\n const inputs = Array.isArray(input) ? input : [input]\n if (inputs.length === 0) return []\n\n const data = await postJson<OpenAIEmbeddingResponse>(\n `${this.baseURL}/embeddings`,\n {\n model: this.defaultEmbedModel,\n input: inputs,\n },\n {\n headers: this.headers,\n },\n )\n\n if (!data.data || !Array.isArray(data.data)) {\n throw new ProviderError(200, JSON.stringify(data), 'OpenAI embedding response had no data')\n }\n\n // Sort by index so we always return vectors in the order we sent\n // them, even if the API decides to interleave responses.\n return [...data.data].sort((a, b) => a.index - b.index).map((d) => d.embedding)\n }\n\n // ── Internal: payload construction ──────────────────────────────────\n\n private buildChatPayload(\n input: ChatInput,\n options: ChatOptions,\n stream: boolean,\n ): OpenAIChatRequest {\n const payload: OpenAIChatRequest = {\n model: input.model ?? this.defaultChatModel,\n messages: input.messages.map((m) => this.toOpenAIMessage(m)),\n stream,\n }\n if (options.temperature !== undefined) payload.temperature = options.temperature\n if (options.maxTokens !== undefined) payload.max_tokens = options.maxTokens\n if (options.topP !== undefined) payload.top_p = options.topP\n if (options.stopSequences && options.stopSequences.length > 0) {\n payload.stop = options.stopSequences\n }\n\n // Tools: only include when the caller passes an explicit array.\n // `'auto'` at the provider level is a no-op — it's only meaningful\n // inside `AiAdapter.runAgent`, which expands it against the\n // `@AiTool` registry before calling the provider.\n if (Array.isArray(input.tools) && input.tools.length > 0) {\n payload.tools = input.tools.map((t) => ({\n type: 'function',\n function: {\n name: t.name,\n description: t.description,\n parameters: t.inputSchema,\n },\n }))\n }\n\n return payload\n }\n\n /**\n * Translate a framework `ChatMessage` to OpenAI's wire format.\n * Handles the `tool` role and the `tool_calls` field on assistant\n * messages, both of which use slightly different shapes than the\n * normalized form on `ChatMessage`.\n */\n private toOpenAIMessage(m: ChatMessage): OpenAIMessage {\n if (m.role === 'tool') {\n return {\n role: 'tool',\n tool_call_id: m.toolCallId ?? '',\n content: m.content,\n }\n }\n if (m.role === 'assistant' && m.toolCalls && m.toolCalls.length > 0) {\n return {\n role: 'assistant',\n content: m.content,\n tool_calls: m.toolCalls.map((tc) => ({\n id: tc.id,\n type: 'function',\n function: { name: tc.name, arguments: JSON.stringify(tc.arguments) },\n })),\n }\n }\n return { role: m.role as 'system' | 'user' | 'assistant', content: m.content }\n }\n\n /**\n * Normalize an OpenAI chat completion response back to the\n * framework's `ChatResponse` shape.\n */\n private normalizeChatResponse(data: OpenAIChatResponse): ChatResponse {\n const choice = data.choices?.[0]\n const message = choice?.message\n const content = typeof message?.content === 'string' ? message.content : ''\n\n const toolCalls = message?.tool_calls\n ?.filter((tc): tc is OpenAIToolCall & { function: NonNullable<OpenAIToolCall['function']> } =>\n Boolean(tc.function?.name),\n )\n .map((tc) => {\n let args: Record<string, unknown> = {}\n try {\n args = tc.function!.arguments ? JSON.parse(tc.function!.arguments) : {}\n } catch {\n // OpenAI is supposed to send valid JSON in arguments, but if\n // it doesn't, surface the raw string under a generic key\n // rather than dropping the call entirely.\n args = { _raw: tc.function!.arguments }\n }\n return { id: tc.id, name: tc.function!.name, arguments: args }\n })\n\n const result: ChatResponse = { content }\n if (toolCalls && toolCalls.length > 0) result.toolCalls = toolCalls\n if (data.usage) {\n result.usage = {\n promptTokens: data.usage.prompt_tokens,\n completionTokens: data.usage.completion_tokens,\n totalTokens: data.usage.total_tokens,\n }\n }\n if (choice?.finish_reason) result.finishReason = choice.finish_reason\n return result\n }\n\n /**\n * Extract the first tool-call delta from an OpenAI streaming chunk.\n *\n * The `tool_calls` array in a delta chunk can contain partial state\n * for multiple parallel tool calls; this method picks the first one\n * with a non-empty payload, which is enough for the v0 streaming\n * surface. Multi-tool streaming is a follow-up.\n */\n private firstToolCallDelta(\n toolCalls?: OpenAIStreamChunk['choices'][number]['delta']['tool_calls'],\n ): ChatChunk['toolCallDelta'] {\n if (!toolCalls || toolCalls.length === 0) return undefined\n const first = toolCalls[0]\n if (!first) return undefined\n const result: NonNullable<ChatChunk['toolCallDelta']> = {\n id: first.id ?? '',\n }\n if (first.function?.name) result.name = first.function.name\n if (first.function?.arguments !== undefined) result.argumentsDelta = first.function.arguments\n return result\n }\n}\n\n// ── OpenAI wire types ─────────────────────────────────────────────────────\n//\n// These mirror the parts of the OpenAI Chat Completions and Embeddings\n// API responses we actually consume. They're intentionally narrower\n// than the full API surface so the provider stays focused on what the\n// framework needs, and so we don't accidentally couple to fields that\n// might change in future API versions.\n\ninterface OpenAIChatRequest {\n model: string\n messages: OpenAIMessage[]\n stream?: boolean\n temperature?: number\n max_tokens?: number\n top_p?: number\n stop?: string[]\n tools?: Array<{\n type: 'function'\n function: {\n name: string\n description: string\n parameters: Record<string, unknown>\n }\n }>\n}\n\ntype OpenAIMessage =\n | { role: 'system' | 'user' | 'assistant'; content: string }\n | {\n role: 'assistant'\n content: string\n tool_calls: Array<{\n id: string\n type: 'function'\n function: { name: string; arguments: string }\n }>\n }\n | { role: 'tool'; content: string; tool_call_id: string }\n\ninterface OpenAIToolCall {\n id: string\n type?: 'function'\n function?: { name: string; arguments: string }\n}\n\ninterface OpenAIChatResponse {\n choices?: Array<{\n message?: { content?: string; tool_calls?: OpenAIToolCall[] }\n finish_reason?: string\n }>\n usage?: {\n prompt_tokens: number\n completion_tokens: number\n total_tokens: number\n }\n}\n\ninterface OpenAIStreamChunk {\n choices: Array<{\n delta: {\n content?: string\n tool_calls?: Array<{\n index?: number\n id?: string\n function?: { name?: string; arguments?: string }\n }>\n }\n finish_reason?: string | null\n }>\n}\n\ninterface OpenAIEmbeddingResponse {\n data: Array<{ index: number; embedding: number[] }>\n}\n","import type {\n AiProvider,\n ChatChunk,\n ChatInput,\n ChatMessage,\n ChatOptions,\n ChatResponse,\n EmbedInput,\n} from '../types'\nimport { postJson, postJsonStream } from './base'\n\n/**\n * Configuration for the Anthropic provider.\n *\n * The base URL is configurable so the same class can target an\n * Anthropic-compatible proxy, an internal gateway that adds auth\n * headers, or an air-gapped deployment. The provider only assumes\n * Anthropic's Messages API wire shape, not the hostname.\n */\nexport interface AnthropicProviderOptions {\n /** API key sent as `x-api-key`. Required. */\n apiKey: string\n /** Override base URL. Defaults to `https://api.anthropic.com/v1`. */\n baseURL?: string\n /** Default chat model used when `ChatInput.model` is not set. */\n defaultChatModel?: string\n /** Anthropic API version header. Defaults to `'2023-06-01'`. */\n apiVersion?: string\n /**\n * Default `max_tokens` for responses. Anthropic requires an explicit\n * max_tokens on every request; the framework's ChatOptions.maxTokens\n * takes precedence when set, but this supplies a fallback so callers\n * don't have to set it every time.\n */\n defaultMaxTokens?: number\n /** Provider name override. Defaults to `'anthropic'`. */\n name?: string\n}\n\n/**\n * Built-in Anthropic provider.\n *\n * Implements the framework's `AiProvider` interface using Anthropic's\n * Messages API (`/v1/messages`). Translates the normalized\n * `ChatInput` shape to and from Anthropic's content-block format,\n * including tool calling and streaming.\n *\n * ### Differences from OpenAI\n *\n * Anthropic's API has a few quirks the provider translates away:\n *\n * - **System prompt is separated.** The framework puts system\n * messages in the `messages` array; Anthropic wants them in a\n * top-level `system` field. The provider extracts the first system\n * message and filters out any others.\n * - **Content is always a block array.** Even simple text replies\n * are wrapped in `[{ type: 'text', text: '...' }]`. The provider\n * flattens text blocks to a single string on the response.\n * - **Tool calls use `tool_use` content blocks, not a separate\n * `tool_calls` field.** Normalization pulls them out of the\n * response content and into `ChatResponse.toolCalls`.\n * - **Tool results are `user` messages with `tool_result` content\n * blocks**, not a `'tool'` role. The provider handles the\n * translation both ways.\n * - **`max_tokens` is required on every request.** Framework\n * `ChatOptions.maxTokens` wins; otherwise falls back to\n * `defaultMaxTokens` (default 4096).\n *\n * ### Embeddings\n *\n * Anthropic does not ship an embeddings API. Calling `embed()` on\n * this provider throws a descriptive error — users who need\n * embeddings should construct a separate provider (OpenAI's\n * `text-embedding-3-small` is a good default) and bind it\n * alongside the Anthropic chat provider.\n *\n * @example\n * ```ts\n * import { bootstrap, getEnv } from '@forinda/kickjs'\n * import { AiAdapter, AnthropicProvider } from '@forinda/kickjs-ai'\n *\n * export const app = await bootstrap({\n * modules,\n * adapters: [\n * new AiAdapter({\n * provider: new AnthropicProvider({\n * apiKey: getEnv('ANTHROPIC_API_KEY'),\n * defaultChatModel: 'claude-opus-4-6',\n * }),\n * }),\n * ],\n * })\n * ```\n */\nexport class AnthropicProvider implements AiProvider {\n readonly name: string\n\n private readonly baseURL: string\n private readonly defaultChatModel: string\n private readonly defaultMaxTokens: number\n private readonly headers: Record<string, string>\n\n constructor(options: AnthropicProviderOptions) {\n if (!options.apiKey) {\n throw new Error('AnthropicProvider: apiKey is required')\n }\n this.baseURL = (options.baseURL ?? 'https://api.anthropic.com/v1').replace(/\\/$/, '')\n this.defaultChatModel = options.defaultChatModel ?? 'claude-opus-4-6'\n this.defaultMaxTokens = options.defaultMaxTokens ?? 4096\n this.name = options.name ?? 'anthropic'\n this.headers = {\n 'x-api-key': options.apiKey,\n 'anthropic-version': options.apiVersion ?? '2023-06-01',\n }\n }\n\n /**\n * Non-streaming chat completion.\n *\n * Builds the Anthropic Messages payload, posts it, and normalizes\n * the response back to the framework's `ChatResponse` shape.\n */\n async chat(input: ChatInput, options: ChatOptions = {}): Promise<ChatResponse> {\n const payload = this.buildMessagesPayload(input, options, /* stream */ false)\n const data = await postJson<AnthropicMessagesResponse>(`${this.baseURL}/messages`, payload, {\n headers: this.headers,\n signal: options.signal,\n })\n return this.normalizeResponse(data)\n }\n\n /**\n * Streaming chat completion. Yields `ChatChunk`s as Anthropic\n * events arrive and emits a final chunk with `done: true` after\n * the `message_stop` event.\n *\n * Anthropic's SSE stream uses distinct event types instead of the\n * single-channel deltas OpenAI sends:\n *\n * - `message_start` — session init, carries model + id\n * - `content_block_start` — new text or tool_use block begins\n * - `content_block_delta` — incremental text or partial tool JSON\n * - `content_block_stop` — block complete\n * - `message_delta` — stop_reason + final usage\n * - `message_stop` — end of stream\n *\n * The provider cares about text deltas (for streaming content) and\n * input_json deltas (for tool call argument streaming). Everything\n * else is noise for our purposes and gets filtered.\n */\n async *stream(input: ChatInput, options: ChatOptions = {}): AsyncIterable<ChatChunk> {\n const payload = this.buildMessagesPayload(input, options, /* stream */ true)\n const events = postJsonStream(`${this.baseURL}/messages`, payload, {\n headers: this.headers,\n signal: options.signal,\n })\n\n // Track the current tool block index + id so tool argument\n // deltas can be routed to the right `toolCallDelta` payload.\n let currentToolBlock: { id: string; name: string } | null = null\n let sawAnyChunk = false\n\n for await (const raw of events) {\n let parsed: AnthropicStreamEvent\n try {\n parsed = JSON.parse(raw) as AnthropicStreamEvent\n } catch {\n // Malformed chunk — skip rather than crashing the stream.\n continue\n }\n\n if (parsed.type === 'content_block_start') {\n const block = parsed.content_block\n if (block?.type === 'tool_use') {\n currentToolBlock = { id: block.id ?? '', name: block.name ?? '' }\n sawAnyChunk = true\n yield {\n content: '',\n done: false,\n toolCallDelta: { id: currentToolBlock.id, name: currentToolBlock.name },\n }\n }\n continue\n }\n\n if (parsed.type === 'content_block_delta') {\n const delta = parsed.delta\n if (delta?.type === 'text_delta' && typeof delta.text === 'string') {\n sawAnyChunk = true\n yield { content: delta.text, done: false }\n continue\n }\n if (delta?.type === 'input_json_delta' && typeof delta.partial_json === 'string') {\n if (!currentToolBlock) continue\n sawAnyChunk = true\n yield {\n content: '',\n done: false,\n toolCallDelta: {\n id: currentToolBlock.id,\n argumentsDelta: delta.partial_json,\n },\n }\n continue\n }\n continue\n }\n\n if (parsed.type === 'content_block_stop') {\n currentToolBlock = null\n continue\n }\n\n if (parsed.type === 'message_stop') {\n yield { content: '', done: true }\n return\n }\n }\n\n // Stream closed without an explicit message_stop — still emit a\n // terminating chunk so consumers know to stop reading.\n if (sawAnyChunk) {\n yield { content: '', done: true }\n }\n }\n\n /**\n * Anthropic does not ship an embeddings API. Throws a descriptive\n * error rather than silently returning an empty vector — embedding\n * workflows should use a dedicated provider (OpenAI text-embedding-3-*\n * is the common pick) and bind it alongside this one in the\n * `AI_PROVIDER` token registry if needed.\n */\n async embed(_input: EmbedInput): Promise<number[][]> {\n throw new Error(\n 'AnthropicProvider.embed is not available — Anthropic does not provide an embeddings API. ' +\n 'Use OpenAIProvider (or another embeddings-capable provider) for embed calls, ' +\n 'and keep Anthropic for chat.',\n )\n }\n\n // ── Internal: payload construction ──────────────────────────────────\n\n private buildMessagesPayload(\n input: ChatInput,\n options: ChatOptions,\n stream: boolean,\n ): AnthropicMessagesRequest {\n const { systemPrompt, messages } = this.splitSystemMessage(input.messages)\n\n const payload: AnthropicMessagesRequest = {\n model: input.model ?? this.defaultChatModel,\n max_tokens: options.maxTokens ?? this.defaultMaxTokens,\n messages: messages.map((m) => this.toAnthropicMessage(m)),\n }\n if (systemPrompt) payload.system = systemPrompt\n if (options.temperature !== undefined) payload.temperature = options.temperature\n if (options.topP !== undefined) payload.top_p = options.topP\n if (options.stopSequences && options.stopSequences.length > 0) {\n payload.stop_sequences = options.stopSequences\n }\n if (stream) payload.stream = true\n\n if (Array.isArray(input.tools) && input.tools.length > 0) {\n payload.tools = input.tools.map((t) => ({\n name: t.name,\n description: t.description,\n input_schema: t.inputSchema,\n }))\n }\n\n return payload\n }\n\n /**\n * Extract the first system message from the framework's messages\n * array and return it separately — Anthropic puts system prompts\n * in a top-level `system` field, not in `messages`. Any additional\n * system messages are dropped on the grounds that models handle\n * one persona prompt per call and concatenating them silently\n * would produce confusing behavior.\n */\n private splitSystemMessage(messages: ChatMessage[]): {\n systemPrompt: string | null\n messages: ChatMessage[]\n } {\n let systemPrompt: string | null = null\n const rest: ChatMessage[] = []\n for (const m of messages) {\n if (m.role === 'system') {\n systemPrompt ??= m.content\n continue\n }\n rest.push(m)\n }\n return { systemPrompt, messages: rest }\n }\n\n /**\n * Translate a framework `ChatMessage` to Anthropic's wire format.\n *\n * User and plain assistant messages become content blocks with a\n * single `text` entry. Assistant messages with tool calls become\n * a block list mixing `text` and `tool_use` entries. Framework\n * `'tool'` role messages become Anthropic `'user'` messages with\n * a `tool_result` block — that's how Anthropic represents tool\n * call responses.\n */\n private toAnthropicMessage(m: ChatMessage): AnthropicMessage {\n if (m.role === 'tool') {\n return {\n role: 'user',\n content: [\n {\n type: 'tool_result',\n tool_use_id: m.toolCallId ?? '',\n content: m.content,\n },\n ],\n }\n }\n\n if (m.role === 'assistant' && m.toolCalls && m.toolCalls.length > 0) {\n const blocks: AnthropicContentBlock[] = []\n if (m.content) {\n blocks.push({ type: 'text', text: m.content })\n }\n for (const tc of m.toolCalls) {\n blocks.push({\n type: 'tool_use',\n id: tc.id,\n name: tc.name,\n input: tc.arguments,\n })\n }\n return { role: 'assistant', content: blocks }\n }\n\n // user or plain assistant\n return {\n role: m.role === 'assistant' ? 'assistant' : 'user',\n content: [{ type: 'text', text: m.content }],\n }\n }\n\n /**\n * Normalize an Anthropic response back to the framework's\n * `ChatResponse`. Flattens text content blocks into a single\n * string and pulls `tool_use` blocks out into `toolCalls`.\n */\n private normalizeResponse(data: AnthropicMessagesResponse): ChatResponse {\n const blocks = data.content ?? []\n const textParts: string[] = []\n const toolCalls: NonNullable<ChatResponse['toolCalls']> = []\n\n for (const block of blocks) {\n if (block.type === 'text' && typeof block.text === 'string') {\n textParts.push(block.text)\n }\n if (block.type === 'tool_use' && block.name && block.id) {\n toolCalls.push({\n id: block.id,\n name: block.name,\n arguments:\n block.input && typeof block.input === 'object'\n ? (block.input as Record<string, unknown>)\n : {},\n })\n }\n }\n\n const result: ChatResponse = { content: textParts.join('') }\n if (toolCalls.length > 0) result.toolCalls = toolCalls\n if (data.usage) {\n result.usage = {\n promptTokens: data.usage.input_tokens,\n completionTokens: data.usage.output_tokens,\n totalTokens: data.usage.input_tokens + data.usage.output_tokens,\n }\n }\n if (data.stop_reason) result.finishReason = data.stop_reason\n return result\n }\n}\n\n// ── Anthropic wire types ──────────────────────────────────────────────────\n//\n// Narrowed to the fields we actually consume. Anthropic's full API\n// surface is richer — vision, document inputs, extended thinking,\n// prompt caching — but the provider only commits to what it uses.\n\ninterface AnthropicMessagesRequest {\n model: string\n max_tokens: number\n messages: AnthropicMessage[]\n system?: string\n temperature?: number\n top_p?: number\n stop_sequences?: string[]\n stream?: boolean\n tools?: Array<{\n name: string\n description: string\n input_schema: Record<string, unknown>\n }>\n}\n\ninterface AnthropicMessage {\n role: 'user' | 'assistant'\n content: AnthropicContentBlock[]\n}\n\ntype AnthropicContentBlock =\n | { type: 'text'; text: string }\n | { type: 'tool_use'; id: string; name: string; input: unknown }\n | { type: 'tool_result'; tool_use_id: string; content: string }\n\ninterface AnthropicMessagesResponse {\n id?: string\n type?: string\n role?: string\n content?: Array<{\n type: 'text' | 'tool_use'\n text?: string\n id?: string\n name?: string\n input?: unknown\n }>\n stop_reason?: string\n usage?: {\n input_tokens: number\n output_tokens: number\n }\n}\n\n/**\n * Anthropic streaming event shapes. Each event arrives as a JSON\n * object on its own `data: ` line. We only care about a handful:\n * start / delta / stop for content blocks, and the final message_stop.\n */\ntype AnthropicStreamEvent =\n | {\n type: 'message_start'\n message?: unknown\n }\n | {\n type: 'content_block_start'\n index?: number\n content_block?: {\n type: 'text' | 'tool_use'\n text?: string\n id?: string\n name?: string\n }\n }\n | {\n type: 'content_block_delta'\n index?: number\n delta?: {\n type: 'text_delta' | 'input_json_delta'\n text?: string\n partial_json?: string\n }\n }\n | {\n type: 'content_block_stop'\n index?: number\n }\n | {\n type: 'message_delta'\n delta?: {\n stop_reason?: string\n }\n usage?: {\n input_tokens?: number\n output_tokens?: number\n }\n }\n | {\n type: 'message_stop'\n }\n | {\n type: 'ping'\n }\n","import type { ChatMessage } from '../types'\n\n/**\n * Options for `createPrompt`.\n */\nexport interface CreatePromptOptions {\n /**\n * Short identifier used in logs, errors, and typegen output.\n * Defaults to `'prompt'` if not provided — give every non-trivial\n * template a real name so error messages point to the right place.\n */\n name?: string\n /**\n * Message role the rendered prompt produces. Defaults to `'user'`.\n * Set to `'system'` for persona / instruction prompts.\n */\n role?: ChatMessage['role']\n /**\n * How missing variables at render time are handled:\n * - `'throw'` (default): throw a descriptive error. Catches bugs\n * early and matches what most users expect.\n * - `'warn'`: leave the placeholder as-is and log a warning via\n * console.warn. Useful for templates with optional sections\n * that the caller might not fill in.\n * - `'silent'`: leave the placeholder as-is and don't warn.\n */\n onMissing?: 'throw' | 'warn' | 'silent'\n}\n\n/**\n * A reusable prompt template with `{{variable}}` placeholders and\n * a typed variables object at the render site.\n *\n * The type parameter `TVars` is a record of the variables the\n * template expects. Callers pass it explicitly:\n *\n * ```ts\n * const summarize = createPrompt<{ text: string; sentenceCount: number }>(\n * 'Summarize the following in {{sentenceCount}} sentences:\\n\\n{{text}}',\n * { name: 'summarize' },\n * )\n *\n * const msg = summarize.render({ text: 'Long article...', sentenceCount: 3 })\n * // → { role: 'user', content: 'Summarize the following in 3 sentences:\\n\\nLong article...' }\n * ```\n *\n * TypeScript catches missing or mistyped variables at compile time:\n *\n * ```ts\n * summarize.render({ text: 'x' }) // ✗ missing sentenceCount\n * summarize.render({ text: 'x', count: 3 }) // ✗ wrong key name\n * ```\n *\n * @remarks\n * Runtime-only in v0 — the type parameter is opt-in and has to be\n * provided explicitly. Workstream 5 adds a `kick typegen` pass that\n * scans `createPrompt` call sites and generates the TVars shape\n * automatically, so you can write `createPrompt('...')` and get\n * the types for free.\n */\nexport class Prompt<TVars extends Record<string, unknown> = Record<string, unknown>> {\n readonly name: string\n readonly role: ChatMessage['role']\n private readonly template: string\n private readonly onMissing: 'throw' | 'warn' | 'silent'\n\n constructor(template: string, options: CreatePromptOptions = {}) {\n if (typeof template !== 'string') {\n throw new Error('createPrompt: template must be a string')\n }\n this.template = template\n this.name = options.name ?? 'prompt'\n this.role = options.role ?? 'user'\n this.onMissing = options.onMissing ?? 'throw'\n }\n\n /**\n * Substitute variables into the template and return a\n * ready-to-use `ChatMessage`.\n *\n * Placeholder syntax is `{{name}}` — double curly braces around\n * the variable name. Whitespace inside the braces is ignored\n * (`{{ name }}` works too). Unknown variables in the template\n * are left as-is, so Markdown or code blocks that happen to use\n * `{{` for their own reasons don't break.\n *\n * @throws If `onMissing === 'throw'` and a required variable is absent\n */\n render(vars: TVars): ChatMessage {\n return {\n role: this.role,\n content: this.renderString(vars),\n }\n }\n\n /**\n * Same as `render` but returns the raw string instead of wrapping\n * it in a `ChatMessage`. Useful for building composite messages\n * where several templates contribute to a single string.\n */\n renderString(vars: TVars): string {\n return this.template.replace(\n /\\{\\{\\s*([a-zA-Z_][a-zA-Z0-9_.]*)\\s*\\}\\}/g,\n (_match, key: string) => {\n if (!(key in vars)) {\n return this.handleMissing(key, _match)\n }\n const value = (vars as Record<string, unknown>)[key]\n if (value === undefined || value === null) {\n return this.handleMissing(key, _match)\n }\n return String(value)\n },\n )\n }\n\n /** Return the raw template string. Useful for debugging and snapshot tests. */\n getTemplate(): string {\n return this.template\n }\n\n /**\n * Return the set of placeholder names the template references.\n *\n * Mostly useful for testing and for tooling that wants to show\n * users what variables a prompt takes. Not a substitute for the\n * compile-time type check — templates can always reference\n * variables that aren't in TVars; this helper reads the string,\n * not the type.\n */\n getPlaceholders(): string[] {\n const matches = this.template.matchAll(/\\{\\{\\s*([a-zA-Z_][a-zA-Z0-9_.]*)\\s*\\}\\}/g)\n const names = new Set<string>()\n for (const m of matches) {\n const name = m[1]\n if (name) names.add(name)\n }\n return [...names]\n }\n\n private handleMissing(key: string, original: string): string {\n if (this.onMissing === 'throw') {\n throw new Error(`Prompt(${this.name}): variable \"${key}\" is missing from the render call`)\n }\n if (this.onMissing === 'warn') {\n // eslint-disable-next-line no-console\n console.warn(\n `Prompt(${this.name}): variable \"${key}\" is missing from the render call; leaving placeholder`,\n )\n }\n return original\n }\n}\n\n/**\n * Construct a reusable prompt template.\n *\n * Thin factory for the `Prompt` class — keeps call sites short and\n * matches the naming convention of other kickjs-ai factories\n * (`createToken`, etc.). Use the class form directly if you need\n * subclassing or custom rendering logic.\n *\n * @example\n * ```ts\n * import { createPrompt } from '@forinda/kickjs-ai'\n *\n * const persona = createPrompt<{ name: string; tone: string }>(\n * 'You are {{name}}, a {{tone}} assistant.',\n * { role: 'system', name: 'persona' },\n * )\n *\n * const msg = persona.render({ name: 'Claude', tone: 'concise' })\n * ```\n */\nexport function createPrompt<TVars extends Record<string, unknown> = Record<string, unknown>>(\n template: string,\n options: CreatePromptOptions = {},\n): Prompt<TVars> {\n return new Prompt<TVars>(template, options)\n}\n","import type { ChatMessage } from '../types'\nimport type { ChatMemory } from './types'\n\n/**\n * Zero-dependency in-memory chat memory.\n *\n * Backed by a plain array. Each instance represents ONE conversation\n * — services that serve multiple sessions construct one instance per\n * session, typically via a `sessionId → memory` map in a parent\n * service or a request-scoped DI factory.\n *\n * Good for:\n * - Tests and prototypes\n * - Single-process CLI tools\n * - Short-lived request handlers that don't outlive the HTTP response\n *\n * Not good for:\n * - Multi-replica deployments (memory isn't shared across pods)\n * - Sessions that need to survive a restart\n * - Anything with a compliance retention policy\n *\n * For any of those, swap in a persistent backend (Drizzle, Redis,\n * Postgres) that implements the same `ChatMemory` interface — the\n * calling service doesn't change.\n *\n * @example\n * ```ts\n * import { InMemoryChatMemory } from '@forinda/kickjs-ai'\n *\n * const memory = new InMemoryChatMemory()\n * await memory.add({ role: 'user', content: 'hello' })\n * const history = await memory.get()\n * ```\n */\nexport class InMemoryChatMemory implements ChatMemory {\n readonly name = 'in-memory'\n\n private messages: ChatMessage[] = []\n\n async get(): Promise<ChatMessage[]> {\n // Return a shallow copy so callers can't mutate internal state\n // by pushing to the returned array. Important: ChatMessage.toolCalls\n // is an object — we don't deep-clone, so external mutation of\n // nested properties is still possible. Callers who need true\n // isolation should structuredClone the result themselves.\n return [...this.messages]\n }\n\n async add(message: ChatMessage | ChatMessage[]): Promise<void> {\n const list = Array.isArray(message) ? message : [message]\n for (const m of list) {\n this.messages.push(m)\n }\n }\n\n async clear(): Promise<void> {\n this.messages = []\n }\n\n async size(): Promise<number> {\n return this.messages.length\n }\n}\n","import type { ChatMessage } from '../types'\nimport type { ChatMemory } from './types'\n\n/**\n * Options for `SlidingWindowChatMemory`.\n */\nexport interface SlidingWindowChatMemoryOptions {\n /** Underlying memory to wrap. */\n inner: ChatMemory\n /**\n * Maximum number of messages to keep in the sliding window. The\n * LAST `maxMessages` messages are retained; anything older is\n * dropped on every `get()` call and on every `add()` that pushes\n * the count past the cap.\n *\n * A typical value is 20 — enough for several user/assistant\n * exchanges with tool call overhead, short enough to keep prompt\n * tokens under control. Tune up or down based on model context\n * window and cost sensitivity.\n */\n maxMessages: number\n /**\n * Whether to treat the FIRST system message as pinned — i.e. never\n * evict it, even when the window would otherwise cap it out.\n *\n * This matches the common pattern of putting a single persona /\n * instruction prompt at the start of every conversation. Without\n * pinning, a long session would eventually drop the system prompt\n * and the model would lose its instructions.\n *\n * Defaults to `true` because forgetting the system prompt is\n * almost never what users want.\n */\n pinSystemPrompt?: boolean\n}\n\n/**\n * Sliding-window memory wrapper.\n *\n * Wraps any `ChatMemory` implementation with a bounded history: only\n * the most recent N messages survive. Older messages are evicted on\n * every `get()` and after every `add()` that pushes the count past\n * the cap. The first system message is pinned by default so long\n * sessions don't lose their persona.\n *\n * Use this to keep prompt token usage predictable without writing\n * eviction logic in every service. It composes with any backend —\n * in-memory, Drizzle, Redis — because it only touches the inner\n * memory through its public interface.\n *\n * @example\n * ```ts\n * import { InMemoryChatMemory, SlidingWindowChatMemory } from '@forinda/kickjs-ai'\n *\n * const memory = new SlidingWindowChatMemory({\n * inner: new InMemoryChatMemory(),\n * maxMessages: 20,\n * pinSystemPrompt: true,\n * })\n * ```\n *\n * @remarks\n * Eviction writes back to the inner memory via `clear()` + `add()`.\n * That's fine for in-memory backends where clearing is O(1), but\n * costs a round-trip for network-backed stores. If you're wrapping\n * a remote backend, consider an inner memory that supports native\n * trimming — the wrapper's contract assumes clear+add is cheap.\n */\nexport class SlidingWindowChatMemory implements ChatMemory {\n readonly name: string\n private readonly inner: ChatMemory\n private readonly maxMessages: number\n private readonly pinSystemPrompt: boolean\n\n constructor(options: SlidingWindowChatMemoryOptions) {\n if (!options.inner) {\n throw new Error('SlidingWindowChatMemory: `inner` memory is required')\n }\n if (!Number.isInteger(options.maxMessages) || options.maxMessages <= 0) {\n throw new Error('SlidingWindowChatMemory: `maxMessages` must be a positive integer')\n }\n this.inner = options.inner\n this.maxMessages = options.maxMessages\n this.pinSystemPrompt = options.pinSystemPrompt ?? true\n this.name = `sliding-window(${options.inner.name})`\n }\n\n async get(): Promise<ChatMessage[]> {\n const raw = await this.inner.get()\n return this.applyWindow(raw)\n }\n\n async add(message: ChatMessage | ChatMessage[]): Promise<void> {\n await this.inner.add(message)\n // Trim eagerly after every add so subsequent gets see a bounded\n // history. Eager eviction keeps the stored state and the visible\n // state in sync — lazy eviction would leave the inner store\n // unbounded, which defeats the point of the wrapper for\n // persistent backends.\n const raw = await this.inner.get()\n const windowed = this.applyWindow(raw)\n if (windowed.length !== raw.length) {\n await this.inner.clear()\n await this.inner.add(windowed)\n }\n }\n\n async clear(): Promise<void> {\n await this.inner.clear()\n }\n\n async size(): Promise<number> {\n if (this.inner.size) return this.inner.size()\n const raw = await this.inner.get()\n return raw.length\n }\n\n /**\n * Apply the sliding window to an array of messages, returning the\n * bounded view. Pure function so both `get()` and `add()` can use\n * the same logic.\n *\n * When `pinSystemPrompt` is set and the first message is a system\n * message, we keep it AND fill the remaining `maxMessages - 1`\n * slots with the most recent messages after it. Otherwise we just\n * take the tail of the array.\n */\n private applyWindow(messages: ChatMessage[]): ChatMessage[] {\n if (messages.length <= this.maxMessages) return messages\n\n if (this.pinSystemPrompt && messages[0]?.role === 'system') {\n const head = messages[0]\n const tail = messages.slice(-(this.maxMessages - 1))\n return [head, ...tail]\n }\n\n return messages.slice(-this.maxMessages)\n }\n}\n","import type { VectorDocument, VectorQueryOptions, VectorSearchHit, VectorStore } from './types'\n\n/**\n * Zero-dependency in-memory vector store.\n *\n * Backed by a plain `Map<string, VectorDocument>` with a linear-scan\n * cosine-similarity search. Perfect for tests, prototypes, CLI tools,\n * and any project with a bounded corpus (roughly < 10k documents\n * before the scan starts taking more than a handful of milliseconds).\n *\n * For production workloads with larger corpora, swap in the pgvector,\n * Qdrant, or Pinecone store — the `VectorStore` interface is the same,\n * so services that consume `VECTOR_STORE` don't need to change.\n *\n * @example\n * ```ts\n * import { InMemoryVectorStore, VECTOR_STORE } from '@forinda/kickjs-ai'\n *\n * container.registerInstance(VECTOR_STORE, new InMemoryVectorStore())\n * ```\n *\n * The class is entirely synchronous under the hood but wraps each\n * method in a Promise so it matches the async interface every other\n * backend implements. This keeps the calling code uniform regardless\n * of which backend is wired in.\n */\nexport class InMemoryVectorStore<\n M extends Record<string, unknown> = Record<string, unknown>,\n> implements VectorStore<M> {\n readonly name = 'in-memory'\n\n private readonly docs = new Map<string, VectorDocument<M>>()\n\n async upsert(doc: VectorDocument<M> | VectorDocument<M>[]): Promise<void> {\n const list = Array.isArray(doc) ? doc : [doc]\n for (const d of list) {\n if (!d.id) throw new Error('InMemoryVectorStore.upsert: document id is required')\n if (!Array.isArray(d.vector)) {\n throw new Error(`InMemoryVectorStore.upsert: vector must be an array (id=${d.id})`)\n }\n // Shallow-copy so callers can't mutate stored state after the fact.\n this.docs.set(d.id, {\n id: d.id,\n content: d.content,\n vector: [...d.vector],\n metadata: d.metadata,\n })\n }\n }\n\n async query(options: VectorQueryOptions): Promise<VectorSearchHit<M>[]> {\n if (!Array.isArray(options.vector) || options.vector.length === 0) {\n throw new Error('InMemoryVectorStore.query: vector is required')\n }\n const topK = options.topK ?? 5\n const minScore = options.minScore ?? -Infinity\n const filter = options.filter\n\n const scored: VectorSearchHit<M>[] = []\n\n for (const doc of this.docs.values()) {\n if (filter && !matchesFilter(doc.metadata, filter)) continue\n\n const score = cosineSimilarity(options.vector, doc.vector)\n if (score < minScore) continue\n\n scored.push({\n id: doc.id,\n content: doc.content,\n score,\n metadata: doc.metadata,\n })\n }\n\n // Sort descending by score; ties broken by id for deterministic ordering.\n scored.sort((a, b) => {\n if (b.score !== a.score) return b.score - a.score\n return a.id.localeCompare(b.id)\n })\n\n return scored.slice(0, topK)\n }\n\n async delete(id: string | string[]): Promise<void> {\n const ids = Array.isArray(id) ? id : [id]\n for (const i of ids) this.docs.delete(i)\n }\n\n async deleteAll(): Promise<void> {\n this.docs.clear()\n }\n\n async count(): Promise<number> {\n return this.docs.size\n }\n}\n\n// ── Helpers ───────────────────────────────────────────────────────────────\n\n/**\n * Cosine similarity between two vectors. Returns a value in [-1, 1]\n * where 1 means identical direction, 0 means orthogonal, -1 means\n * opposite. The function is symmetric and scale-invariant.\n *\n * Returns 0 for length mismatches or zero-magnitude vectors rather\n * than throwing — callers get a useless hit they can filter out via\n * `minScore`, but the store doesn't crash on bad input.\n */\nexport function cosineSimilarity(a: number[], b: number[]): number {\n if (a.length !== b.length || a.length === 0) return 0\n\n let dot = 0\n let magA = 0\n let magB = 0\n for (let i = 0; i < a.length; i++) {\n const x = a[i]\n const y = b[i]\n dot += x * y\n magA += x * x\n magB += y * y\n }\n\n if (magA === 0 || magB === 0) return 0\n return dot / (Math.sqrt(magA) * Math.sqrt(magB))\n}\n\n/**\n * Simple equality-based metadata filter. Every key in `filter` must\n * exist on the metadata and be strictly equal. Array values on the\n * filter are treated as an `IN` clause — the metadata value must be\n * one of the listed values.\n *\n * This covers 90% of metadata filtering use cases without pulling in\n * a query-language dependency. Backends that support richer filters\n * (pgvector's WHERE, Qdrant's conditions, Pinecone's filter DSL) can\n * pass through their native syntax via the same `filter` field,\n * since the type is `Record<string, unknown>`.\n */\nfunction matchesFilter(\n metadata: Record<string, unknown> | undefined,\n filter: Record<string, unknown>,\n): boolean {\n if (!metadata) return false\n for (const [key, expected] of Object.entries(filter)) {\n const actual = metadata[key]\n if (Array.isArray(expected)) {\n if (!expected.includes(actual)) return false\n } else if (actual !== expected) {\n return false\n }\n }\n return true\n}\n","import type { VectorDocument, VectorQueryOptions, VectorSearchHit, VectorStore } from './types'\n\n/**\n * Minimal SQL executor contract.\n *\n * Covers everything `PgVectorStore` needs from a Postgres client: a\n * single `query(text, params)` call that returns rows. The shape is\n * deliberately narrower than node-postgres's `Pool.query` so it can\n * be satisfied by any of:\n *\n * - `pg.Pool` / `pg.Client` (node-postgres)\n * - `drizzle.$client` (the underlying pool on the Drizzle adapter)\n * - `postgres.js` (by @porsager, via a small adapter)\n * - A unit-test fake that records calls\n *\n * Users who already have a Postgres connection somewhere in their\n * app can hand it to the store without installing `pg` twice.\n */\nexport interface SqlExecutor {\n query<T = unknown>(text: string, params?: unknown[]): Promise<{ rows: T[] }>\n}\n\n/**\n * Options for `PgVectorStore`.\n *\n * Exactly one of `client` or `connectionString` must be provided. If\n * `connectionString` is set, the store dynamically imports `pg` on\n * first use and creates its own pool; `pg` must be installed as a\n * peer dep in that case. If `client` is set, the store uses the\n * supplied executor and never touches `pg` directly.\n */\nexport interface PgVectorStoreOptions {\n /** Pre-made SQL executor — any object with a `query(text, params)` method. */\n client?: SqlExecutor\n /** Connection string used to create a pg.Pool if `client` is not provided. */\n connectionString?: string\n /** Vector dimensionality. Must match the embedding model. Required. */\n dimensions: number\n /** Postgres schema. Defaults to `'public'`. */\n schema?: string\n /** Table name. Defaults to `'kickjs_embeddings'`. */\n table?: string\n /**\n * Skip the first-use schema bootstrap (`CREATE EXTENSION IF NOT\n * EXISTS vector; CREATE TABLE IF NOT EXISTS ...`). Set this to true\n * if you manage migrations manually or run in a read-only role.\n */\n skipSetup?: boolean\n /**\n * Provider name to expose on `store.name`. Defaults to `'pgvector'`\n * but can be overridden to label a Postgres-compatible backend\n * (e.g. `'timescale'`, `'cockroach-vector'`).\n */\n name?: string\n}\n\n/**\n * pgvector-backed `VectorStore` implementation.\n *\n * Stores documents in a single table with a `vector` column indexed\n * via pgvector's native operators. Cosine similarity is the scoring\n * metric — computed as `1 - (vector <=> query_vector)` because the\n * `<=>` operator returns cosine DISTANCE, not similarity.\n *\n * ### Lazy initialization\n *\n * The Postgres pool and schema are set up on first use, not in the\n * constructor. That keeps the constructor synchronous, matches the\n * rest of the `VectorStore` implementations, and lets users construct\n * the store inside a module's `register(container)` method without\n * awaiting inside DI resolution.\n *\n * ### Schema\n *\n * The default schema is:\n *\n * ```sql\n * CREATE EXTENSION IF NOT EXISTS vector;\n * CREATE TABLE IF NOT EXISTS <schema>.<table> (\n * id TEXT PRIMARY KEY,\n * content TEXT NOT NULL,\n * vector vector(<dimensions>) NOT NULL,\n * metadata JSONB\n * );\n * ```\n *\n * No index is created by default — pgvector's IVFFlat and HNSW\n * indexes benefit from being created AFTER data is loaded, and the\n * right choice depends on corpus size. Users should add an index\n * themselves in a real migration when they're ready:\n *\n * ```sql\n * CREATE INDEX ON kickjs_embeddings\n * USING hnsw (vector vector_cosine_ops);\n * ```\n *\n * ### Metadata filtering\n *\n * Filters are translated to JSONB WHERE clauses:\n * - Scalar: `metadata->>'key' = $N` (coerced to text)\n * - Array: `metadata->>'key' = ANY($N::text[])`\n *\n * Keys are validated against `[a-zA-Z0-9_.-]+` before being\n * interpolated into SQL — anything else throws. Values go through\n * parameter binding, so SQL injection via values is not possible.\n *\n * @example\n * ```ts\n * import { Pool } from 'pg'\n * import { getEnv } from '@forinda/kickjs'\n * import { AiAdapter, PgVectorStore, VECTOR_STORE } from '@forinda/kickjs-ai'\n *\n * const pool = new Pool({ connectionString: getEnv('DATABASE_URL') })\n * const store = new PgVectorStore({ client: pool, dimensions: 1536 })\n *\n * export const app = await bootstrap({\n * modules,\n * adapters: [new AiAdapter({ provider })],\n * plugins: [\n * {\n * name: 'pgvector',\n * register: (container) => {\n * container.registerInstance(VECTOR_STORE, store)\n * },\n * },\n * ],\n * })\n * ```\n */\nexport class PgVectorStore<\n M extends Record<string, unknown> = Record<string, unknown>,\n> implements VectorStore<M> {\n readonly name: string\n\n private readonly dimensions: number\n private readonly schema: string\n private readonly table: string\n private readonly fullyQualified: string\n private readonly skipSetup: boolean\n\n private client: SqlExecutor | null\n private readonly connectionString: string | null\n private setupPromise: Promise<void> | null = null\n\n constructor(options: PgVectorStoreOptions) {\n if (!options.client && !options.connectionString) {\n throw new Error('PgVectorStore: either `client` or `connectionString` must be provided')\n }\n if (!Number.isInteger(options.dimensions) || options.dimensions <= 0) {\n throw new Error('PgVectorStore: `dimensions` must be a positive integer')\n }\n\n this.dimensions = options.dimensions\n this.schema = options.schema ?? 'public'\n this.table = options.table ?? 'kickjs_embeddings'\n this.fullyQualified = `${quoteIdent(this.schema)}.${quoteIdent(this.table)}`\n this.skipSetup = options.skipSetup ?? false\n this.name = options.name ?? 'pgvector'\n\n this.client = options.client ?? null\n this.connectionString = options.connectionString ?? null\n }\n\n async upsert(doc: VectorDocument<M> | VectorDocument<M>[]): Promise<void> {\n const list = Array.isArray(doc) ? doc : [doc]\n if (list.length === 0) return\n\n for (const d of list) {\n if (!d.id) throw new Error('PgVectorStore.upsert: document id is required')\n if (!Array.isArray(d.vector)) {\n throw new Error(`PgVectorStore.upsert: vector must be an array (id=${d.id})`)\n }\n if (d.vector.length !== this.dimensions) {\n throw new Error(\n `PgVectorStore.upsert: vector length ${d.vector.length} does not match ` +\n `configured dimensions ${this.dimensions} (id=${d.id})`,\n )\n }\n }\n\n const client = await this.ensureReady()\n\n // Batched insert: one INSERT per call with every document's row.\n // A single round-trip is substantially faster than per-doc inserts\n // when embedding thousands of documents in one pass.\n const values: string[] = []\n const params: unknown[] = []\n let p = 1\n for (const d of list) {\n values.push(`($${p++}, $${p++}, $${p++}::vector, $${p++}::jsonb)`)\n params.push(d.id, d.content, toPgVector(d.vector), JSON.stringify(d.metadata ?? {}))\n }\n\n const sql =\n `INSERT INTO ${this.fullyQualified} (id, content, vector, metadata) VALUES ` +\n values.join(', ') +\n ' ON CONFLICT (id) DO UPDATE SET ' +\n 'content = EXCLUDED.content, ' +\n 'vector = EXCLUDED.vector, ' +\n 'metadata = EXCLUDED.metadata'\n\n await client.query(sql, params)\n }\n\n async query(options: VectorQueryOptions): Promise<VectorSearchHit<M>[]> {\n if (!Array.isArray(options.vector) || options.vector.length === 0) {\n throw new Error('PgVectorStore.query: vector is required')\n }\n if (options.vector.length !== this.dimensions) {\n throw new Error(\n `PgVectorStore.query: vector length ${options.vector.length} does not match ` +\n `configured dimensions ${this.dimensions}`,\n )\n }\n\n const client = await this.ensureReady()\n const topK = options.topK ?? 5\n const minScore = options.minScore ?? -Infinity\n\n const { whereSql, whereParams } = buildWhereClause(options.filter, /* startAt */ 2)\n // Param positions:\n // $1 — the query vector (vector type)\n // $2..$N — metadata filter params\n // $(N+1) — topK\n const limitParamIdx = whereParams.length + 2\n const sql =\n `SELECT id, content, metadata, ` +\n `(1 - (vector <=> $1::vector)) AS score ` +\n `FROM ${this.fullyQualified} ` +\n whereSql +\n ` ORDER BY vector <=> $1::vector ` +\n `LIMIT $${limitParamIdx}`\n\n const params: unknown[] = [toPgVector(options.vector), ...whereParams, topK]\n const { rows } = await client.query<PgVectorRow>(sql, params)\n\n const hits: VectorSearchHit<M>[] = []\n for (const row of rows) {\n if (row.score < minScore) continue\n hits.push({\n id: row.id,\n content: row.content,\n score: row.score,\n metadata: (row.metadata as M | undefined) ?? undefined,\n })\n }\n return hits\n }\n\n async delete(id: string | string[]): Promise<void> {\n const ids = Array.isArray(id) ? id : [id]\n if (ids.length === 0) return\n\n const client = await this.ensureReady()\n await client.query(`DELETE FROM ${this.fullyQualified} WHERE id = ANY($1::text[])`, [ids])\n }\n\n async deleteAll(): Promise<void> {\n const client = await this.ensureReady()\n // TRUNCATE is faster than DELETE FROM for a full wipe and is still\n // transactional on Postgres, so callers that open a transaction\n // around deleteAll() stay consistent.\n await client.query(`TRUNCATE ${this.fullyQualified}`)\n }\n\n async count(): Promise<number> {\n const client = await this.ensureReady()\n const { rows } = await client.query<{ count: string }>(\n `SELECT COUNT(*)::text AS count FROM ${this.fullyQualified}`,\n )\n const raw = rows[0]?.count ?? '0'\n return Number.parseInt(raw, 10)\n }\n\n /**\n * Release the internal connection pool, if the store created one.\n *\n * If the caller supplied their own `client`, this is a no-op —\n * lifecycle of a user-owned pool stays with the user. This method\n * is intentionally not on the `VectorStore` interface because most\n * backends don't need explicit teardown; services that want to\n * clean up call it via an adapter.shutdown hook.\n */\n async close(): Promise<void> {\n if (this.connectionString && this.client) {\n // Only close pools we created ourselves. Detect via the presence\n // of an `end` method — both pg.Pool and pg.Client expose it.\n const withEnd = this.client as { end?: () => Promise<void> }\n if (typeof withEnd.end === 'function') {\n await withEnd.end()\n }\n this.client = null\n }\n }\n\n // ── Internal ────────────────────────────────────────────────────────────\n\n /**\n * Ensure the pool exists and the schema is set up. Called by every\n * public method before running any SQL. The setup migration runs\n * at most once per store instance — subsequent calls reuse the\n * cached promise.\n */\n private async ensureReady(): Promise<SqlExecutor> {\n if (!this.client) {\n this.client = await this.createPoolFromConnectionString()\n }\n if (!this.skipSetup) {\n if (!this.setupPromise) {\n this.setupPromise = this.runSchemaSetup(this.client)\n }\n await this.setupPromise\n }\n return this.client\n }\n\n /**\n * Dynamically import `pg` and create a Pool from the configured\n * connection string. Imported lazily so users who supply their own\n * `client` never force `pg` to be installed.\n *\n * Throws a friendly error if `pg` is not installed — the same\n * graceful-degradation pattern the CLI uses for optional packages.\n */\n private async createPoolFromConnectionString(): Promise<SqlExecutor> {\n if (!this.connectionString) {\n throw new Error(\n 'PgVectorStore: no client or connectionString configured (this should never happen)',\n )\n }\n // Indirect import specifier so TypeScript skips static type\n // resolution — `pg` is an optional peer and we don't want the\n // AI package to carry @types/pg just for this one call site.\n // Runtime behavior is unchanged; Node resolves 'pg' via normal\n // module lookup against the user's node_modules.\n const pgSpec = 'pg'\n let pgModule: {\n Pool: new (opts: { connectionString: string }) => SqlExecutor\n default?: { Pool: new (opts: { connectionString: string }) => SqlExecutor }\n }\n try {\n pgModule = (await import(pgSpec)) as unknown as typeof pgModule\n } catch {\n throw new Error(\n 'PgVectorStore: the `pg` package is not installed. Run `pnpm add pg` (or ' +\n 'pass a pre-made executor via the `client` option) to use the pgvector store.',\n )\n }\n // Some CJS builds expose the exports on .default — handle both.\n const Pool = pgModule.default?.Pool ?? pgModule.Pool\n if (!Pool) {\n throw new Error(\n 'PgVectorStore: the `pg` module did not export a `Pool` class (unexpected version).',\n )\n }\n return new Pool({ connectionString: this.connectionString })\n }\n\n /**\n * Run the schema bootstrap: enable the pgvector extension, create\n * the embeddings table if it doesn't exist, and nothing else.\n *\n * Indexes are deliberately not created here — pgvector's IVFFlat\n * and HNSW indexes perform best when created after data is loaded,\n * and the right choice depends on corpus size. Users should add\n * their index in a real migration when they're ready.\n */\n private async runSchemaSetup(client: SqlExecutor): Promise<void> {\n await client.query('CREATE EXTENSION IF NOT EXISTS vector')\n await client.query(\n `CREATE TABLE IF NOT EXISTS ${this.fullyQualified} (` +\n `id TEXT PRIMARY KEY, ` +\n `content TEXT NOT NULL, ` +\n `vector vector(${this.dimensions}) NOT NULL, ` +\n `metadata JSONB ` +\n `)`,\n )\n }\n}\n\n// ── Helpers ───────────────────────────────────────────────────────────────\n\ninterface PgVectorRow {\n id: string\n content: string\n metadata: unknown\n score: number\n}\n\n/**\n * Serialize a JS number array to pgvector's wire format: a string\n * like `'[0.1,0.2,0.3]'`. The `pg` driver doesn't know about vectors\n * so we have to stringify ourselves and cast with `::vector` in the\n * SQL. Non-finite values become `0` rather than `null` or `NaN` —\n * pgvector rejects non-finite values in inserts.\n */\nexport function toPgVector(vector: number[]): string {\n const safe = vector.map((n) => (Number.isFinite(n) ? n : 0))\n return `[${safe.join(',')}]`\n}\n\n/**\n * Double-quote a Postgres identifier and escape any embedded quotes.\n * Used for schema and table names so users can pass lowercase\n * identifiers without worrying about reserved words.\n */\nfunction quoteIdent(ident: string): string {\n return `\"${ident.replace(/\"/g, '\"\"')}\"`\n}\n\n/**\n * Translate a metadata filter into a WHERE clause + bound parameters.\n *\n * - Scalar values become `metadata->>'key' = $N`\n * - Array values become `metadata->>'key' = ANY($N::text[])`\n *\n * Keys must match `[a-zA-Z0-9_.-]+` — anything else is rejected. All\n * values are coerced to string before binding, because `->>` returns\n * text. Callers that need numeric range queries should issue raw SQL\n * via their own executor; this helper covers the equality-case 90%.\n *\n * Exported for unit testing.\n */\nexport function buildWhereClause(\n filter: Record<string, unknown> | undefined,\n startAt: number,\n): { whereSql: string; whereParams: unknown[] } {\n if (!filter || Object.keys(filter).length === 0) {\n return { whereSql: '', whereParams: [] }\n }\n\n const keyPattern = /^[a-zA-Z0-9_.\\-]+$/\n const clauses: string[] = []\n const params: unknown[] = []\n let p = startAt\n\n for (const [key, value] of Object.entries(filter)) {\n if (!keyPattern.test(key)) {\n throw new Error(\n `PgVectorStore: metadata filter key \"${key}\" contains unsupported characters ` +\n `(allowed: letters, digits, underscore, dot, dash)`,\n )\n }\n if (Array.isArray(value)) {\n clauses.push(`metadata->>'${key}' = ANY($${p}::text[])`)\n params.push(value.map(String))\n } else {\n clauses.push(`metadata->>'${key}' = $${p}`)\n params.push(value === null || value === undefined ? '' : String(value))\n }\n p++\n }\n\n return {\n whereSql: 'WHERE ' + clauses.join(' AND '),\n whereParams: params,\n }\n}\n","import type { VectorDocument, VectorQueryOptions, VectorSearchHit, VectorStore } from './types'\n\n/**\n * Options for `QdrantVectorStore`.\n *\n * Qdrant exposes a REST API under `/collections/{name}` — this store\n * talks to it directly with `fetch`, so no client SDK is needed. A\n * bearer `apiKey` is optional because self-hosted Qdrant instances\n * often run without auth; managed Qdrant Cloud always requires one.\n */\nexport interface QdrantVectorStoreOptions {\n /** Base URL of the Qdrant HTTP API. Defaults to `http://localhost:6333`. */\n url?: string\n /** API key sent as `api-key` header. Optional for local/self-hosted. */\n apiKey?: string\n /** Collection name. Required — Qdrant does not have a default collection. */\n collection: string\n /** Vector dimensionality. Must match the embedding model. Required. */\n dimensions: number\n /**\n * Distance metric for the collection on first create. Qdrant supports\n * `Cosine`, `Dot`, `Euclid`, and `Manhattan`. Defaults to `'Cosine'`\n * since that's what every OpenAI/Anthropic-compatible embedding\n * model ships.\n */\n distance?: 'Cosine' | 'Dot' | 'Euclid' | 'Manhattan'\n /**\n * Skip the first-use collection bootstrap. Turn this on if the\n * collection is managed by your infra team or provisioned via\n * Terraform, and the runtime role doesn't have create permission.\n */\n skipSetup?: boolean\n /** Provider name override. Defaults to `'qdrant'`. */\n name?: string\n}\n\n/**\n * Qdrant REST API response for `/collections/{name}/points/search`.\n *\n * Narrowed to the fields we consume — Qdrant's full response also\n * carries vector data (optional), version info, and payload schema,\n * none of which the store needs to surface.\n */\ninterface QdrantSearchResult {\n result: Array<{\n id: string | number\n score: number\n payload?: {\n content?: string\n metadata?: Record<string, unknown>\n }\n }>\n}\n\n/**\n * Qdrant-backed `VectorStore` implementation.\n *\n * Qdrant stores vectors as \"points\" inside a named \"collection\". Each\n * point has an id, a dense vector, and an arbitrary JSON \"payload\" —\n * the store uses the payload to carry both the original `content`\n * string (so RAG retrieval can feed text back to the LLM) and the\n * `metadata` record.\n *\n * ### Filtering\n *\n * The framework's equality-map filter (`{ key: value }` or\n * `{ key: [v1, v2] }`) is translated into Qdrant's `filter.must`\n * conditions against `payload.metadata.<key>`. Scalar values become\n * `match: { value }`, arrays become `match: { any: [...] }`. Users\n * who need richer queries (nested, range, should/must_not) can bypass\n * this by extending the class, but equality covers the 90% case.\n *\n * ### Lazy collection creation\n *\n * On first write, the store calls `PUT /collections/{name}` with\n * `vectors: { size, distance }` — idempotent, so it's safe to run on\n * every boot. Pass `skipSetup: true` if your cluster is provisioned\n * externally and the runtime API key doesn't have create permission.\n *\n * @example\n * ```ts\n * import { bootstrap, getEnv } from '@forinda/kickjs'\n * import { AiAdapter, QdrantVectorStore, VECTOR_STORE } from '@forinda/kickjs-ai'\n *\n * const store = new QdrantVectorStore({\n * url: getEnv('QDRANT_URL'),\n * apiKey: getEnv('QDRANT_API_KEY'),\n * collection: 'docs',\n * dimensions: 1536,\n * })\n *\n * export const app = await bootstrap({\n * modules,\n * adapters: [new AiAdapter({ provider })],\n * plugins: [\n * {\n * name: 'qdrant',\n * register: (container) => {\n * container.registerInstance(VECTOR_STORE, store)\n * },\n * },\n * ],\n * })\n * ```\n */\nexport class QdrantVectorStore<\n M extends Record<string, unknown> = Record<string, unknown>,\n> implements VectorStore<M> {\n readonly name: string\n\n private readonly url: string\n private readonly collection: string\n private readonly dimensions: number\n private readonly distance: 'Cosine' | 'Dot' | 'Euclid' | 'Manhattan'\n private readonly headers: Record<string, string>\n private readonly skipSetup: boolean\n /**\n * Cached bootstrap promise. The first method call triggers collection\n * creation; every subsequent call awaits the same promise so the\n * check happens exactly once per process. On failure we clear the\n * cache so the next call can retry (networks blink, DNS flaps).\n */\n private setupPromise: Promise<void> | null = null\n\n constructor(options: QdrantVectorStoreOptions) {\n if (!options.collection) {\n throw new Error('QdrantVectorStore: collection is required')\n }\n if (!Number.isInteger(options.dimensions) || options.dimensions <= 0) {\n throw new Error('QdrantVectorStore: dimensions must be a positive integer')\n }\n this.url = (options.url ?? 'http://localhost:6333').replace(/\\/$/, '')\n this.collection = options.collection\n this.dimensions = options.dimensions\n this.distance = options.distance ?? 'Cosine'\n this.skipSetup = options.skipSetup ?? false\n this.name = options.name ?? 'qdrant'\n this.headers = {\n 'content-type': 'application/json',\n ...(options.apiKey ? { 'api-key': options.apiKey } : {}),\n }\n }\n\n async upsert(doc: VectorDocument<M> | VectorDocument<M>[]): Promise<void> {\n const list = Array.isArray(doc) ? doc : [doc]\n if (list.length === 0) return\n\n for (const d of list) {\n if (!d.id) throw new Error('QdrantVectorStore.upsert: document id is required')\n if (!Array.isArray(d.vector) || d.vector.length !== this.dimensions) {\n throw new Error(\n `QdrantVectorStore.upsert: vector length ${d.vector?.length ?? 0} ` +\n `does not match collection dimensions ${this.dimensions} (id=${d.id})`,\n )\n }\n }\n\n await this.ensureCollection()\n\n const points = list.map((d) => ({\n id: d.id,\n vector: d.vector,\n payload: {\n content: d.content,\n metadata: d.metadata ?? {},\n },\n }))\n\n await this.request('PUT', `/collections/${this.collection}/points?wait=true`, {\n points,\n })\n }\n\n async query(options: VectorQueryOptions): Promise<VectorSearchHit<M>[]> {\n if (!Array.isArray(options.vector) || options.vector.length === 0) {\n throw new Error('QdrantVectorStore.query: vector is required')\n }\n if (options.vector.length !== this.dimensions) {\n throw new Error(\n `QdrantVectorStore.query: vector length ${options.vector.length} ` +\n `does not match collection dimensions ${this.dimensions}`,\n )\n }\n\n await this.ensureCollection()\n\n const topK = options.topK ?? 5\n const minScore = options.minScore\n\n const body: Record<string, unknown> = {\n vector: options.vector,\n limit: topK,\n with_payload: true,\n }\n if (options.filter && Object.keys(options.filter).length > 0) {\n body.filter = buildQdrantFilter(options.filter)\n }\n if (minScore !== undefined) {\n body.score_threshold = minScore\n }\n\n const data = await this.request<QdrantSearchResult>(\n 'POST',\n `/collections/${this.collection}/points/search`,\n body,\n )\n\n return data.result.map((hit) => ({\n id: String(hit.id),\n content: hit.payload?.content ?? '',\n score: hit.score,\n metadata: (hit.payload?.metadata ?? {}) as M,\n }))\n }\n\n async delete(id: string | string[]): Promise<void> {\n const ids = Array.isArray(id) ? id : [id]\n if (ids.length === 0) return\n\n await this.ensureCollection()\n\n await this.request('POST', `/collections/${this.collection}/points/delete?wait=true`, {\n points: ids,\n })\n }\n\n async deleteAll(): Promise<void> {\n // Qdrant doesn't have a \"truncate points\" endpoint — the canonical\n // way is to drop and recreate the collection. Recreating is cheap\n // since the schema is declarative, and it's the same operation the\n // Qdrant web UI performs on \"clear collection\".\n await this.request('DELETE', `/collections/${this.collection}`, undefined)\n // Force a fresh setup next call so the collection reappears.\n this.setupPromise = null\n if (!this.skipSetup) {\n await this.ensureCollection()\n }\n }\n\n async count(): Promise<number> {\n await this.ensureCollection()\n const data = await this.request<{ result: { count: number } }>(\n 'POST',\n `/collections/${this.collection}/points/count`,\n { exact: true },\n )\n return data.result.count\n }\n\n // ── Internal: HTTP plumbing ──────────────────────────────────────────\n\n /**\n * Thin wrapper around `fetch` that applies the shared headers, JSON\n * encodes the body, and maps non-2xx responses to `Error` instances\n * with the response body attached for debugging. Matches the shape\n * used by `providers/base.ts`, kept local here so the RAG module has\n * no dependency on the provider internals.\n */\n private async request<T = unknown>(\n method: 'GET' | 'POST' | 'PUT' | 'DELETE',\n path: string,\n body: unknown,\n ): Promise<T> {\n const res = await fetch(`${this.url}${path}`, {\n method,\n headers: this.headers,\n body: body === undefined ? undefined : JSON.stringify(body),\n })\n if (!res.ok) {\n const text = await res.text().catch(() => '')\n throw new Error(`QdrantVectorStore: ${method} ${path} failed with ${res.status}: ${text}`)\n }\n // Some endpoints (DELETE collection) return JSON; some return empty\n // body. Parse defensively so callers that don't need the payload\n // can still await the promise without hitting a JSON error.\n const text = await res.text()\n if (!text) return undefined as T\n try {\n return JSON.parse(text) as T\n } catch {\n return undefined as T\n }\n }\n\n /**\n * Create the collection on first use. The `PUT /collections/{name}`\n * endpoint is idempotent — calling it on an existing collection is a\n * no-op with status 200. We cache the promise so concurrent callers\n * share the same in-flight request and every subsequent call resolves\n * immediately.\n */\n private ensureCollection(): Promise<void> {\n if (this.skipSetup) return Promise.resolve()\n this.setupPromise ??= this.runSetup().catch((err) => {\n // Clear the cache on failure so a later call can retry — network\n // blips shouldn't permanently wedge the store.\n this.setupPromise = null\n throw err\n })\n return this.setupPromise\n }\n\n private async runSetup(): Promise<void> {\n await this.request('PUT', `/collections/${this.collection}`, {\n vectors: {\n size: this.dimensions,\n distance: this.distance,\n },\n })\n }\n}\n\n// ── Helpers ───────────────────────────────────────────────────────────────\n\n/**\n * Translate the framework's equality-map filter into Qdrant's\n * `must` condition format.\n *\n * Scalars become `{ key, match: { value } }`. Arrays become\n * `{ key, match: { any: [...] } }`. Keys are interpreted as paths into\n * `payload.metadata`, matching how `upsert` nests the metadata record.\n *\n * Exported so tests (and future richer filter builders) can verify the\n * translation without going through a live Qdrant instance.\n */\nexport function buildQdrantFilter(filter: Record<string, unknown>): {\n must: Array<Record<string, unknown>>\n} {\n const must: Array<Record<string, unknown>> = []\n for (const [key, value] of Object.entries(filter)) {\n const qdrantKey = `metadata.${key}`\n if (Array.isArray(value)) {\n must.push({ key: qdrantKey, match: { any: value } })\n } else {\n must.push({ key: qdrantKey, match: { value } })\n }\n }\n return { must }\n}\n","import type { VectorDocument, VectorQueryOptions, VectorSearchHit, VectorStore } from './types'\n\n/**\n * Options for `PineconeVectorStore`.\n *\n * Unlike Qdrant, Pinecone does not have a \"create collection on first\n * use\" endpoint that's cheap to call — the index must be provisioned\n * separately (via the Pinecone dashboard, API, or Terraform) before\n * the store can use it. Every Pinecone index has its own hostname,\n * which the SDK normally looks up; this store requires the caller to\n * pass it directly via `indexHost` so there's zero runtime dependency\n * on the Pinecone client.\n */\nexport interface PineconeVectorStoreOptions {\n /** Required API key, sent as `Api-Key` header. */\n apiKey: string\n /**\n * Fully qualified hostname for the Pinecone index, e.g.\n * `my-index-abcdef1.svc.us-east-1-aws.pinecone.io`. Find it in\n * the Pinecone dashboard or via the `describe_index` API. The\n * scheme is optional — the store adds `https://` if it's missing.\n */\n indexHost: string\n /**\n * Namespace for all operations. Pinecone partitions indexes with\n * namespaces; omitting this uses the default (empty) namespace.\n * Most multi-tenant apps use one namespace per tenant.\n */\n namespace?: string\n /** Vector dimensionality. Required — used to validate upsert shapes. */\n dimensions: number\n /** Provider name override. Defaults to `'pinecone'`. */\n name?: string\n}\n\n/**\n * Pinecone REST API response for `POST /query`.\n *\n * Narrowed to the fields we consume. Pinecone's full response also\n * includes `namespace` and `usage` objects; the store ignores them\n * because neither is needed to produce framework-shape hits.\n */\ninterface PineconeQueryResult {\n matches: Array<{\n id: string\n score: number\n metadata?: Record<string, unknown> & { content?: string }\n }>\n}\n\n/**\n * Pinecone-backed `VectorStore` implementation.\n *\n * Pinecone stores vectors with a flat id, a dense vector, and an\n * arbitrary metadata object. Like Qdrant the store uses metadata to\n * carry both the original `content` (for RAG retrieval) and the\n * application's own metadata fields — they're merged into one\n * Pinecone metadata record at write time and split back apart at\n * read time.\n *\n * ### Filtering\n *\n * Pinecone has a native filter DSL that looks almost identical to\n * MongoDB's — `{ key: { $eq: value } }`, `{ key: { $in: [...] } }`,\n * etc. The framework's equality-map filter is translated directly:\n * scalars become `$eq` and arrays become `$in`. Users who need the\n * full DSL (range, $ne, $or) can pass a raw Pinecone filter through\n * the same `filter` field — the translator is a no-op when the keys\n * start with `$`, so advanced filters pass through unchanged.\n *\n * ### Index provisioning\n *\n * Pinecone indexes must be created out-of-band. This store does NOT\n * provision indexes automatically — the dimensionality, metric, and\n * pod type are infrastructure decisions that should live in\n * Terraform or the Pinecone dashboard, not in runtime code.\n *\n * @example\n * ```ts\n * import { bootstrap, getEnv } from '@forinda/kickjs'\n * import { AiAdapter, PineconeVectorStore, VECTOR_STORE } from '@forinda/kickjs-ai'\n *\n * const store = new PineconeVectorStore({\n * apiKey: getEnv('PINECONE_API_KEY'),\n * indexHost: getEnv('PINECONE_INDEX_HOST'),\n * dimensions: 1536,\n * namespace: 'docs',\n * })\n *\n * export const app = await bootstrap({\n * modules,\n * adapters: [new AiAdapter({ provider })],\n * plugins: [\n * {\n * name: 'pinecone',\n * register: (container) => {\n * container.registerInstance(VECTOR_STORE, store)\n * },\n * },\n * ],\n * })\n * ```\n */\nexport class PineconeVectorStore<\n M extends Record<string, unknown> = Record<string, unknown>,\n> implements VectorStore<M> {\n readonly name: string\n\n private readonly baseURL: string\n private readonly namespace: string | undefined\n private readonly dimensions: number\n private readonly headers: Record<string, string>\n\n constructor(options: PineconeVectorStoreOptions) {\n if (!options.apiKey) {\n throw new Error('PineconeVectorStore: apiKey is required')\n }\n if (!options.indexHost) {\n throw new Error('PineconeVectorStore: indexHost is required')\n }\n if (!Number.isInteger(options.dimensions) || options.dimensions <= 0) {\n throw new Error('PineconeVectorStore: dimensions must be a positive integer')\n }\n\n // Accept hosts with or without a scheme — the Pinecone dashboard\n // shows them bare, but developers often paste the full URL.\n const host = options.indexHost.replace(/\\/$/, '')\n this.baseURL = host.startsWith('http') ? host : `https://${host}`\n this.namespace = options.namespace\n this.dimensions = options.dimensions\n this.name = options.name ?? 'pinecone'\n this.headers = {\n 'content-type': 'application/json',\n 'Api-Key': options.apiKey,\n // Pinecone pins clients to a stable API version via this header.\n // 2024-10 is the current GA version; bump when Pinecone ships a\n // new stable release and users need the new fields.\n 'X-Pinecone-API-Version': '2024-10',\n }\n }\n\n async upsert(doc: VectorDocument<M> | VectorDocument<M>[]): Promise<void> {\n const list = Array.isArray(doc) ? doc : [doc]\n if (list.length === 0) return\n\n for (const d of list) {\n if (!d.id) throw new Error('PineconeVectorStore.upsert: document id is required')\n if (!Array.isArray(d.vector) || d.vector.length !== this.dimensions) {\n throw new Error(\n `PineconeVectorStore.upsert: vector length ${d.vector?.length ?? 0} ` +\n `does not match index dimensions ${this.dimensions} (id=${d.id})`,\n )\n }\n }\n\n const vectors = list.map((d) => ({\n id: d.id,\n values: d.vector,\n // Pinecone flattens `content` + user metadata into one record\n // because Pinecone doesn't support nested objects in metadata.\n // We unflatten on read — see `query` below.\n metadata: {\n content: d.content,\n ...(d.metadata ?? {}),\n },\n }))\n\n const body: Record<string, unknown> = { vectors }\n if (this.namespace) body.namespace = this.namespace\n\n await this.request('/vectors/upsert', body)\n }\n\n async query(options: VectorQueryOptions): Promise<VectorSearchHit<M>[]> {\n if (!Array.isArray(options.vector) || options.vector.length === 0) {\n throw new Error('PineconeVectorStore.query: vector is required')\n }\n if (options.vector.length !== this.dimensions) {\n throw new Error(\n `PineconeVectorStore.query: vector length ${options.vector.length} ` +\n `does not match index dimensions ${this.dimensions}`,\n )\n }\n\n const topK = options.topK ?? 5\n\n const body: Record<string, unknown> = {\n vector: options.vector,\n topK,\n includeMetadata: true,\n }\n if (this.namespace) body.namespace = this.namespace\n if (options.filter && Object.keys(options.filter).length > 0) {\n body.filter = buildPineconeFilter(options.filter)\n }\n\n const data = await this.request<PineconeQueryResult>('/query', body)\n\n const minScore = options.minScore ?? -Infinity\n return data.matches\n .filter((m) => m.score >= minScore)\n .map((match) => {\n const { content, ...metadata } = match.metadata ?? {}\n return {\n id: match.id,\n content: typeof content === 'string' ? content : '',\n score: match.score,\n metadata: metadata as M,\n }\n })\n }\n\n async delete(id: string | string[]): Promise<void> {\n const ids = Array.isArray(id) ? id : [id]\n if (ids.length === 0) return\n\n const body: Record<string, unknown> = { ids }\n if (this.namespace) body.namespace = this.namespace\n\n await this.request('/vectors/delete', body)\n }\n\n async deleteAll(): Promise<void> {\n // Pinecone exposes `deleteAll: true` inside the namespace to wipe\n // the whole namespace in a single call.\n const body: Record<string, unknown> = { deleteAll: true }\n if (this.namespace) body.namespace = this.namespace\n await this.request('/vectors/delete', body)\n }\n\n async count(): Promise<number> {\n const data = await this.request<{\n namespaces?: Record<string, { vectorCount: number }>\n totalVectorCount?: number\n }>('/describe_index_stats', this.namespace ? { filter: {} } : {})\n\n if (this.namespace) {\n return data.namespaces?.[this.namespace]?.vectorCount ?? 0\n }\n return data.totalVectorCount ?? 0\n }\n\n // ── Internal: HTTP plumbing ──────────────────────────────────────────\n\n /**\n * POST a JSON body to the Pinecone data-plane and return the parsed\n * JSON response. Every Pinecone data-plane endpoint uses POST even\n * for reads (`/query`, `/describe_index_stats`), so the helper\n * doesn't bother parameterizing the method.\n */\n private async request<T = unknown>(path: string, body: unknown): Promise<T> {\n const res = await fetch(`${this.baseURL}${path}`, {\n method: 'POST',\n headers: this.headers,\n body: JSON.stringify(body),\n })\n if (!res.ok) {\n const text = await res.text().catch(() => '')\n throw new Error(`PineconeVectorStore: POST ${path} failed with ${res.status}: ${text}`)\n }\n const text = await res.text()\n if (!text) return undefined as T\n try {\n return JSON.parse(text) as T\n } catch {\n return undefined as T\n }\n }\n}\n\n// ── Helpers ───────────────────────────────────────────────────────────────\n\n/**\n * Translate the framework's equality-map filter into Pinecone's\n * MongoDB-style filter DSL.\n *\n * Rules:\n * - Scalar value → `{ key: { $eq: value } }`\n * - Array value → `{ key: { $in: [...] } }`\n * - Key that starts with $ → passed through untouched, letting\n * callers hand-craft `{ $or: [...] }` or range conditions\n * without the translator mangling them\n * - Value already shaped like `{ $eq, $in, $gt, ... }` → passed\n * through untouched for the same reason\n *\n * Exported so tests can verify the translation offline.\n */\nexport function buildPineconeFilter(filter: Record<string, unknown>): Record<string, unknown> {\n const result: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(filter)) {\n if (key.startsWith('$')) {\n // Top-level operators (`$and`, `$or`) — user knows what they're doing.\n result[key] = value\n continue\n }\n if (isOperatorRecord(value)) {\n result[key] = value\n continue\n }\n if (Array.isArray(value)) {\n result[key] = { $in: value }\n } else {\n result[key] = { $eq: value }\n }\n }\n return result\n}\n\nfunction isOperatorRecord(value: unknown): boolean {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return false\n for (const key of Object.keys(value as Record<string, unknown>)) {\n if (key.startsWith('$')) return true\n }\n return false\n}\n","import type { AiProvider, ChatInput, ChatMessage } from '../types'\nimport type {\n RagAugmentOptions,\n RagIndexInput,\n RagSearchOptions,\n VectorDocument,\n VectorSearchHit,\n VectorStore,\n} from './types'\n\nconst DEFAULT_SYSTEM_TEMPLATE =\n 'You have access to the following context documents. Use them when they are' +\n ' relevant to the user question; ignore them otherwise.\\n\\n' +\n '{documents}\\n\\n' +\n \"If the context doesn't contain enough information, say so plainly — don't\" +\n ' invent answers.'\n\n/**\n * High-level RAG helper that ties an `AiProvider` (for embeddings)\n * to a `VectorStore` (for retrieval) and produces the three operations\n * every RAG-powered service needs: index documents, search by query,\n * and augment a chat input with retrieved context.\n *\n * The service itself is a thin orchestrator — all the storage and\n * model calls go through the injected interfaces, so swapping\n * backends (in-memory → pgvector, OpenAI → Ollama) is a DI binding\n * change, not a code change.\n *\n * @example\n * ```ts\n * import { Service, Autowired, Inject } from '@forinda/kickjs'\n * import { AI_PROVIDER, VECTOR_STORE, RagService } from '@forinda/kickjs-ai'\n * import type { AiProvider, VectorStore } from '@forinda/kickjs-ai'\n *\n * @Service()\n * class DocsService {\n * private readonly rag: RagService\n *\n * constructor(\n * @Inject(AI_PROVIDER) provider: AiProvider,\n * @Inject(VECTOR_STORE) store: VectorStore,\n * ) {\n * this.rag = new RagService(provider, store)\n * }\n *\n * async ingest(articles: Array<{ id: string; body: string }>) {\n * await this.rag.index(articles.map((a) => ({ id: a.id, content: a.body })))\n * }\n *\n * async ask(question: string) {\n * const input = await this.rag.augmentChatInput(\n * { messages: [{ role: 'user', content: question }] },\n * question,\n * { topK: 3 },\n * )\n * const res = await provider.chat(input)\n * return res.content\n * }\n * }\n * ```\n */\nexport class RagService<M extends Record<string, unknown> = Record<string, unknown>> {\n constructor(\n private readonly provider: AiProvider,\n private readonly store: VectorStore<M>,\n ) {}\n\n /** Underlying provider — exposed for services that want to reuse it for chat. */\n getProvider(): AiProvider {\n return this.provider\n }\n\n /** Underlying store — useful for admin tools that want raw access. */\n getStore(): VectorStore<M> {\n return this.store\n }\n\n /**\n * Index a batch of documents: embed each one's content via the\n * provider, then upsert into the store. Embedding happens in a\n * single batched call, which is both faster and cheaper than one\n * call per document for most providers.\n *\n * Documents with empty content are skipped rather than failing the\n * whole batch — the store can't meaningfully retrieve empty strings\n * and silently dropping them matches what users usually expect when\n * a content field turns out to be blank.\n */\n async index(docs: RagIndexInput<M>[]): Promise<void> {\n const nonEmpty = docs.filter((d) => d.content && d.content.trim().length > 0)\n if (nonEmpty.length === 0) return\n\n const vectors = await this.provider.embed(nonEmpty.map((d) => d.content))\n if (vectors.length !== nonEmpty.length) {\n throw new Error(\n `RagService.index: provider returned ${vectors.length} vectors for ${nonEmpty.length} inputs`,\n )\n }\n\n const toUpsert: VectorDocument<M>[] = nonEmpty.map((doc, i) => ({\n id: doc.id,\n content: doc.content,\n vector: vectors[i],\n metadata: doc.metadata,\n }))\n\n await this.store.upsert(toUpsert)\n }\n\n /**\n * Search the store for documents relevant to a natural-language\n * query. Embeds the query once, then delegates to the store's\n * `query` method with the resolved vector.\n */\n async search(query: string, options: RagSearchOptions = {}): Promise<VectorSearchHit<M>[]> {\n const [queryVector] = await this.provider.embed(query)\n if (!queryVector) return []\n\n return this.store.query({\n vector: queryVector,\n topK: options.topK ?? 5,\n filter: options.filter,\n minScore: options.minScore,\n })\n }\n\n /**\n * Retrieve relevant documents for a query and inject them into a\n * `ChatInput` as a system message. Returns a new input — the\n * original is not mutated.\n *\n * Two injection modes:\n * - Merge (default): prepend the context to the first existing\n * system message if one exists, otherwise add a new one. Avoids\n * producing chat histories with competing system prompts.\n * - Separate (`asSeparateSystemMessage: true`): always insert a\n * new system message at the start. Useful when the existing\n * system prompt is small and you want to keep roles distinct.\n *\n * If no documents are retrieved, the input is returned unchanged.\n */\n async augmentChatInput(\n input: ChatInput,\n query: string,\n options: RagAugmentOptions = {},\n ): Promise<ChatInput> {\n const hits = await this.search(query, {\n topK: options.topK ?? 5,\n filter: options.filter,\n minScore: options.minScore,\n })\n if (hits.length === 0) return input\n\n const template = options.systemTemplate ?? DEFAULT_SYSTEM_TEMPLATE\n const documentBlock = hits\n .map((h, i) => `[Document ${i + 1} (id=${h.id}, score=${h.score.toFixed(3)})]\\n${h.content}`)\n .join('\\n\\n')\n const contextMessage = template.replace('{documents}', documentBlock)\n\n // Rebuild the messages array with the context injected. Never\n // mutate the caller's input — ChatInput can be reused across\n // multiple provider calls and mutation would lead to compounding\n // context on each retry.\n const newMessages: ChatMessage[] = []\n\n const existingSystemIdx = input.messages.findIndex((m) => m.role === 'system')\n const shouldMerge = !options.asSeparateSystemMessage && existingSystemIdx !== -1\n\n if (shouldMerge) {\n for (let i = 0; i < input.messages.length; i++) {\n const msg = input.messages[i]\n if (i === existingSystemIdx) {\n newMessages.push({\n ...msg,\n content: `${contextMessage}\\n\\n---\\n\\n${msg.content}`,\n })\n } else {\n newMessages.push(msg)\n }\n }\n } else {\n newMessages.push({ role: 'system', content: contextMessage })\n newMessages.push(...input.messages)\n }\n\n return { ...input, messages: newMessages }\n }\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@forinda/kickjs-ai",
|
|
3
|
+
"version": "2.3.0",
|
|
4
|
+
"description": "AI runtime for KickJS — providers, @AiTool decorator, streaming, RAG, and agent loops",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"kickjs",
|
|
7
|
+
"nodejs",
|
|
8
|
+
"typescript",
|
|
9
|
+
"express",
|
|
10
|
+
"decorators",
|
|
11
|
+
"dependency-injection",
|
|
12
|
+
"backend",
|
|
13
|
+
"api",
|
|
14
|
+
"framework",
|
|
15
|
+
"ai",
|
|
16
|
+
"llm",
|
|
17
|
+
"openai",
|
|
18
|
+
"anthropic",
|
|
19
|
+
"gemini",
|
|
20
|
+
"ollama",
|
|
21
|
+
"tool-calling",
|
|
22
|
+
"streaming",
|
|
23
|
+
"rag",
|
|
24
|
+
"vector-search",
|
|
25
|
+
"agent",
|
|
26
|
+
"@forinda/kickjs",
|
|
27
|
+
"@forinda/kickjs-mcp"
|
|
28
|
+
],
|
|
29
|
+
"type": "module",
|
|
30
|
+
"main": "dist/index.mjs",
|
|
31
|
+
"types": "dist/index.d.mts",
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"import": "./dist/index.mjs",
|
|
35
|
+
"types": "./dist/index.d.mts"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"dist"
|
|
40
|
+
],
|
|
41
|
+
"wireit": {
|
|
42
|
+
"build": {
|
|
43
|
+
"command": "tsdown",
|
|
44
|
+
"files": [
|
|
45
|
+
"src/**/*.ts",
|
|
46
|
+
"tsdown.config.ts",
|
|
47
|
+
"tsconfig.json",
|
|
48
|
+
"package.json"
|
|
49
|
+
],
|
|
50
|
+
"output": [
|
|
51
|
+
"dist/**"
|
|
52
|
+
],
|
|
53
|
+
"dependencies": [
|
|
54
|
+
"../kickjs:build"
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"reflect-metadata": "^0.2.2",
|
|
60
|
+
"@forinda/kickjs": "2.3.0"
|
|
61
|
+
},
|
|
62
|
+
"peerDependencies": {
|
|
63
|
+
"zod": "^4.3.6"
|
|
64
|
+
},
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"@swc/core": "^1.15.21",
|
|
67
|
+
"@types/node": "^25.0.0",
|
|
68
|
+
"typescript": "^5.9.2",
|
|
69
|
+
"unplugin-swc": "^1.5.9",
|
|
70
|
+
"vitest": "^4.1.2",
|
|
71
|
+
"zod": "^4.3.6"
|
|
72
|
+
},
|
|
73
|
+
"publishConfig": {
|
|
74
|
+
"access": "public"
|
|
75
|
+
},
|
|
76
|
+
"license": "MIT",
|
|
77
|
+
"author": "Felix Orinda",
|
|
78
|
+
"engines": {
|
|
79
|
+
"node": ">=20.0"
|
|
80
|
+
},
|
|
81
|
+
"homepage": "https://forinda.github.io/kick-js/",
|
|
82
|
+
"repository": {
|
|
83
|
+
"type": "git",
|
|
84
|
+
"url": "https://github.com/forinda/kick-js.git",
|
|
85
|
+
"directory": "packages/ai"
|
|
86
|
+
},
|
|
87
|
+
"bugs": {
|
|
88
|
+
"url": "https://github.com/forinda/kick-js/issues"
|
|
89
|
+
},
|
|
90
|
+
"scripts": {
|
|
91
|
+
"build": "wireit",
|
|
92
|
+
"dev": "tsdown --watch",
|
|
93
|
+
"test": "vitest run --passWithNoTests",
|
|
94
|
+
"test:watch": "vitest",
|
|
95
|
+
"typecheck": "tsc --noEmit",
|
|
96
|
+
"clean": "rm -rf dist .wireit"
|
|
97
|
+
}
|
|
98
|
+
}
|