@firststep-studio/sdk 0.7.0 → 0.8.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/dist/index.d.mts +326 -3
- package/dist/index.d.ts +326 -3
- package/dist/index.js +236 -12
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +233 -12
- package/dist/index.mjs.map +1 -1
- package/dist/server.d.mts +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/{types-DCrYoOfK.d.mts → types-GoTI_c14.d.mts} +64 -1
- package/dist/{types-DCrYoOfK.d.ts → types-GoTI_c14.d.ts} +64 -1
- package/package.json +1 -1
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/ucp/client.ts","../src/renderMarkers.ts","../src/streamMetadata.ts","../src/platform.ts","../src/auth.ts"],"sourcesContent":["/**\n * UCP (Universal Classification Protocol) Client\n *\n * Client for communicating with UCP-compliant classifier endpoints.\n * Supports both full configuration and simple endpoint URL.\n *\n * @example\n * ```typescript\n * // Simple usage with endpoint URL\n * const client = UCPClient.fromEndpoint('https://api.example.com/ucp/v1/classifiers/abc123');\n *\n * // With API key\n * const client = UCPClient.fromEndpoint(\n * 'https://api.example.com/ucp/v1/classifiers/abc123',\n * { apiKey: 'your-api-key' }\n * );\n *\n * // Classify messages\n * const result = await client.classify(messages);\n * ```\n */\n\nimport type {\n UCPMessage,\n UCPClassifyRequest,\n UCPClassifyResponse,\n UCPErrorResponse,\n UCPInfoResponse,\n UCPClientConfig,\n UCPEndpointConfig,\n ClassificationResult,\n} from './types';\nimport type { ChatMessage } from '../types';\n\n// Default timeout for requests\nconst DEFAULT_TIMEOUT = 30000;\n\n/**\n * UCP Client for classifier communication\n */\nexport class UCPClient {\n private baseUrl: string;\n private classifierId: string;\n private apiKey?: string;\n private timeout: number;\n private headers: Record<string, string>;\n\n constructor(config: UCPClientConfig) {\n // Normalize base URL (remove trailing slash)\n this.baseUrl = config.baseUrl.replace(/\\/$/, '');\n this.classifierId = config.classifierId;\n this.apiKey = config.apiKey;\n this.timeout = config.timeout ?? DEFAULT_TIMEOUT;\n this.headers = config.headers ?? {};\n }\n\n /**\n * Create client from a full endpoint URL\n *\n * URL format: {baseUrl}/classifiers/{classifierId}\n * Example: https://api.example.com/ucp/v1/classifiers/abc123\n */\n static fromEndpoint(\n endpoint: string,\n options?: { apiKey?: string; timeout?: number }\n ): UCPClient {\n // Parse endpoint URL to extract baseUrl and classifierId\n const url = new URL(endpoint);\n const pathParts = url.pathname.split('/');\n\n // Find \"classifiers\" in path and get the ID after it\n const classifiersIndex = pathParts.indexOf('classifiers');\n if (classifiersIndex === -1 || classifiersIndex >= pathParts.length - 1) {\n throw new Error(\n `Invalid UCP endpoint URL: ${endpoint}. Expected format: {baseUrl}/classifiers/{classifierId}`\n );\n }\n\n const classifierId = pathParts[classifiersIndex + 1];\n const baseUrlPath = pathParts.slice(0, classifiersIndex).join('/');\n const baseUrl = `${url.protocol}//${url.host}${baseUrlPath}`;\n\n return new UCPClient({\n baseUrl,\n classifierId,\n apiKey: options?.apiKey,\n timeout: options?.timeout,\n });\n }\n\n /**\n * Get the classify endpoint URL\n */\n get classifyUrl(): string {\n return `${this.baseUrl}/classifiers/${this.classifierId}/classify`;\n }\n\n /**\n * Get the info endpoint URL\n */\n get infoUrl(): string {\n return `${this.baseUrl}/classifiers/${this.classifierId}/info`;\n }\n\n /**\n * Classify messages using UCP protocol\n */\n async classify(messages: UCPMessage[]): Promise<ClassificationResult> {\n const request: UCPClassifyRequest = { messages };\n\n const response = await this.fetch<UCPClassifyResponse>(\n this.classifyUrl,\n {\n method: 'POST',\n body: JSON.stringify(request),\n }\n );\n\n // Normalize to SDK format\n return {\n classifierId: this.classifierId,\n category: response.category,\n level: response.level,\n confidence: response.score / 100, // Normalize to 0-1\n reasoning: response.rationale,\n raw: response,\n };\n }\n\n /**\n * Classify ChatMessage array (convenience method)\n * Converts ChatMessage to UCPMessage format\n */\n async classifyChat(messages: ChatMessage[]): Promise<ClassificationResult> {\n const ucpMessages: UCPMessage[] = messages\n .filter(m => m.role === 'user' || m.role === 'assistant')\n .map((m, index) => ({\n id: `msg-${index}`,\n role: m.role as 'user' | 'assistant',\n content: m.content,\n timestamp: m.timestamp ? m.timestamp.getTime() : Date.now(),\n metadata: m.metadata,\n }));\n\n return this.classify(ucpMessages);\n }\n\n /**\n * Get classifier info\n */\n async getInfo(): Promise<UCPInfoResponse> {\n return this.fetch<UCPInfoResponse>(this.infoUrl, {\n method: 'GET',\n });\n }\n\n /**\n * Check if the classifier endpoint is reachable\n */\n async healthCheck(): Promise<boolean> {\n try {\n const healthUrl = `${this.baseUrl}/health`;\n const response = await fetch(healthUrl, {\n method: 'GET',\n signal: AbortSignal.timeout(5000),\n });\n return response.ok;\n } catch {\n return false;\n }\n }\n\n /**\n * Internal fetch helper with error handling\n */\n private async fetch<T>(url: string, options: RequestInit): Promise<T> {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...this.headers,\n };\n\n if (this.apiKey) {\n headers['Authorization'] = `Bearer ${this.apiKey}`;\n }\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.timeout);\n\n try {\n const response = await fetch(url, {\n ...options,\n headers,\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n const ucpError = errorData as UCPErrorResponse;\n\n throw new UCPError(\n ucpError.error?.code || `HTTP_${response.status}`,\n ucpError.error?.message || `HTTP ${response.status}: ${response.statusText}`,\n ucpError.error?.details\n );\n }\n\n return response.json() as Promise<T>;\n } catch (error) {\n clearTimeout(timeoutId);\n\n if (error instanceof UCPError) {\n throw error;\n }\n\n if (error instanceof Error) {\n if (error.name === 'AbortError') {\n throw new UCPError('TIMEOUT', `Request timed out after ${this.timeout}ms`);\n }\n throw new UCPError('NETWORK_ERROR', error.message);\n }\n\n throw new UCPError('UNKNOWN_ERROR', 'An unknown error occurred');\n }\n }\n}\n\n/**\n * UCP-specific error class\n */\nexport class UCPError extends Error {\n constructor(\n public code: string,\n message: string,\n public details?: unknown\n ) {\n super(message);\n this.name = 'UCPError';\n }\n}\n\n// ============================================\n// Convenience Functions\n// ============================================\n\n/**\n * Create a UCP client from an endpoint URL\n */\nexport function createUCPClient(\n endpoint: string,\n options?: { apiKey?: string; timeout?: number }\n): UCPClient {\n return UCPClient.fromEndpoint(endpoint, options);\n}\n\n/**\n * Classify messages using a UCP endpoint\n * One-shot function for simple usage\n */\nexport async function classifyWithUCP(\n endpoint: string,\n messages: ChatMessage[],\n options?: { apiKey?: string; timeout?: number }\n): Promise<ClassificationResult> {\n const client = createUCPClient(endpoint, options);\n return client.classifyChat(messages);\n}\n","/**\n * Render Markers\n *\n * Utilities for building render markers that FirstStep Studio's frontend\n * can parse and display as rich UI components (cards, alerts, forms, etc.).\n *\n * This module is the single source of truth for render marker specs.\n * - Handlers (producers) use the builder functions to emit markers\n * - Studio frontend (consumer) uses the types and MARKER_TYPES constants to parse them\n *\n * @example\n * ```typescript\n * import { renderMarkers } from '@firststep-studio/sdk';\n *\n * // In your handleStream():\n * yield {\n * type: 'text',\n * content: renderMarkers.helplineCard({\n * helplines: [{ name: '988 Lifeline', phoneNumber: '988', categories: ['crisis'], status: 'open', statusLabel: 'Available' }],\n * }),\n * };\n * ```\n */\n\n// ============================================\n// Marker Type Constants\n// ============================================\n\nexport const MARKER_TYPES = {\n HELPLINE_CARD: 'RENDER_HELPLINE_CARD',\n EMERGENCY: 'RENDER_EMERGENCY',\n RESOURCE_CARD: 'RENDER_RESOURCE_CARD',\n PROVIDER_CARD: 'RENDER_PROVIDER_CARD',\n SAFETY_PLAN: 'RENDER_SAFETY_PLAN',\n REPORT_CARD: 'RENDER_REPORT_CARD',\n} as const;\n\nexport type MarkerType = typeof MARKER_TYPES[keyof typeof MARKER_TYPES];\n\n// ============================================\n// Payload Types\n// ============================================\n\n/**\n * Helpline card payload.\n * Renders a carousel of helpline cards with contact buttons (call, text, chat, WhatsApp).\n */\nexport interface HelplineCardPayload {\n helplines: HelplineCardItem[];\n type?: 'throughline' | 'throughline_fallback' | 'stage';\n fallback?: {\n message: string;\n linkText?: string;\n linkUrl?: string;\n icon?: string;\n topic?: string;\n topics?: string[];\n };\n}\n\nexport interface HelplineCardItem {\n name: string;\n description?: string;\n categories: string[];\n status: 'open' | 'closed';\n statusLabel: string;\n statusBadge?: string;\n hoursText?: string;\n supportTypes?: string;\n smsNumber?: string;\n phoneNumber?: string;\n website?: string;\n webchat?: string;\n whatsapp?: string;\n specialties?: string[];\n highlightedTag?: string;\n verified?: boolean;\n}\n\n/**\n * Emergency number payload.\n * Renders a prominent alert card with a large call button.\n */\nexport interface EmergencyPayload {\n number: string;\n countryName?: string;\n countryCode?: string;\n}\n\n/**\n * Resource card payload.\n * Renders a carousel of resource cards with video thumbnails, tags, and visit buttons.\n */\nexport interface ResourceCardPayload {\n resources: ResourceCardItem[];\n}\n\nexport interface ResourceCardItem {\n name: string;\n url?: string;\n description?: string;\n video_url?: string;\n type?: string;\n tags?: string[];\n highlightedTag?: string;\n}\n\n/**\n * Provider card payload.\n * Renders a carousel of provider directory cards with specialty/language tags and contact info.\n */\nexport interface ProviderCardPayload {\n providers: ProviderCardItem[];\n}\n\nexport interface ProviderCardItem {\n id: string;\n name: string;\n type?: string;\n specialty: string[];\n language: string[];\n description?: string;\n location?: string;\n open_hours?: string;\n contact_phone?: string;\n contact_email?: string;\n address?: string;\n}\n\n/**\n * Safety plan payload.\n * Renders a multi-section table with expandable sections and save/export actions.\n */\nexport interface SafetyPlanPayload {\n sections: SafetyPlanSection[];\n helplines?: Array<{ name: string; phone?: string }>;\n actions?: {\n savePng?: boolean;\n saveTxt?: boolean;\n copy?: boolean;\n };\n}\n\nexport interface SafetyPlanSection {\n id: string;\n title: string;\n items: Array<string | { name: string; phone?: string; description?: string }>;\n}\n\n/**\n * Report card payload.\n * Renders a summary card of collected report data with a submit button.\n */\nexport interface ReportCardPayload {\n topic: string;\n location?: string;\n description?: string;\n perpetrator_known?: boolean;\n contact_mode?: string;\n contact_value?: string;\n submitEndpoint?: string;\n}\n\n// ============================================\n// Builder Functions\n// ============================================\n\n/**\n * Wrap a payload in render marker tags.\n * Internal helper used by all builder functions.\n */\nfunction wrapMarker(type: string, payload: unknown): string {\n return `[${type}]${JSON.stringify(payload)}[/${type}]`;\n}\n\n/**\n * Render marker builder functions.\n *\n * Each function takes a typed payload and returns a render marker string\n * that can be yielded as a `text` chunk in handleStream().\n *\n * @example\n * ```typescript\n * // Emit helpline cards\n * yield { type: 'text', content: renderMarkers.helplineCard({ helplines: [...] }) };\n *\n * // Emit emergency number\n * yield { type: 'text', content: renderMarkers.emergency({ number: '911', countryName: 'United States' }) };\n * ```\n */\nexport const renderMarkers = {\n /** Build a helpline card carousel marker */\n helplineCard: (payload: HelplineCardPayload): string =>\n wrapMarker(MARKER_TYPES.HELPLINE_CARD, payload),\n\n /** Build an emergency number alert marker */\n emergency: (payload: EmergencyPayload): string =>\n wrapMarker(MARKER_TYPES.EMERGENCY, payload),\n\n /** Build a resource card carousel marker */\n resourceCard: (payload: ResourceCardPayload): string =>\n wrapMarker(MARKER_TYPES.RESOURCE_CARD, payload),\n\n /** Build a provider card carousel marker */\n providerCard: (payload: ProviderCardPayload): string =>\n wrapMarker(MARKER_TYPES.PROVIDER_CARD, payload),\n\n /** Build a safety plan artifact marker */\n safetyPlan: (payload: SafetyPlanPayload): string =>\n wrapMarker(MARKER_TYPES.SAFETY_PLAN, payload),\n\n /** Build a report draft card marker */\n reportCard: (payload: ReportCardPayload): string =>\n wrapMarker(MARKER_TYPES.REPORT_CARD, payload),\n};\n","/**\n * Stream Metadata Builders\n *\n * Utilities for building typed metadata stream chunks that FirstStep Studio's\n * proxy persists to MongoDB for Dashboard features (Form Insights, Session\n * History, Routing Logs, Agent Transitions).\n *\n * Each function returns a ready-to-yield ProtocolStreamChunk. No need to\n * construct the `{ type, content }` wrapper manually.\n *\n * @example\n * ```typescript\n * import { streamMetadata } from '@firststep-studio/sdk';\n *\n * // In your handleStream():\n *\n * // 1. Welcome path: declare your schema once\n * yield streamMetadata.declareSchema({\n * agents: [{ id: 'intake', title: 'Intake', order: 0 }],\n * questions: [{ id: 'name', agentId: 'intake', title: 'Name', type: 'text' }],\n * });\n *\n * // 2. After each turn: send collected field values\n * yield streamMetadata.formDataUpdate({ name: 'Alice', age: '28' });\n *\n * // 3. On stage change: signal an agent transition\n * yield streamMetadata.agentTransition({ id: 'support', title: 'Support' });\n *\n * // 4. On classification: send routing result\n * yield streamMetadata.routingResult({\n * decision: 'classified',\n * reason: 'User mentioned self-harm',\n * category: 'crisis',\n * level: 'high',\n * score: 92,\n * });\n * ```\n */\n\nimport type {\n ProtocolStreamChunk,\n SchemaDeclarationPayload,\n SchemaAgent,\n SchemaQuestion,\n AgentTransitionPayload,\n RoutingClassificationPayload,\n HandoffRequestPayload,\n HandoffReturnPayload,\n HandoffOfferPayload,\n} from './types';\n\n// ============================================\n// Builder Functions\n// ============================================\n\nexport const streamMetadata = {\n /**\n * Declare the form schema (agents + questions) for Dashboard Form Insights.\n *\n * Yield this once during session initialization (welcome message).\n * The Studio proxy stores it in `chatbot.externalSchema` so that\n * Dashboard Form Insights can display question-level analytics.\n *\n * @param schema - Agents and questions your handler uses\n * @returns A metadata stream chunk ready to yield or queue.push\n */\n declareSchema(schema: SchemaDeclarationPayload): ProtocolStreamChunk {\n return { type: 'metadata', content: { schema } };\n },\n\n /**\n * Send collected form field values for Dashboard persistence.\n *\n * Call this after each turn when new fields are captured.\n * Values are incrementally merged into `ChatSession.formData`.\n *\n * @param fields - Key-value pairs of field IDs to their collected values\n * @returns A metadata stream chunk ready to yield or queue.push\n */\n formDataUpdate(fields: Record<string, string | number>): ProtocolStreamChunk {\n return { type: 'metadata', content: { formData: fields } };\n },\n\n /**\n * Signal an agent/stage transition for routing logs.\n *\n * The Studio proxy records this in `ChatSession.routingLogs` so\n * the Dashboard can show the conversation's agent flow.\n *\n * @param agent - The agent/stage being transitioned to\n * @returns A metadata stream chunk ready to yield or queue.push\n */\n agentTransition(agent: AgentTransitionPayload): ProtocolStreamChunk {\n return { type: 'metadata', content: { currentAgent: agent } };\n },\n\n /**\n * Send a routing/classification result for routing logs.\n *\n * The Studio proxy records this in `ChatSession.routingLogs` as a\n * classification event with category, level, and confidence score.\n *\n * @param result - Classification/routing decision details\n * @returns A metadata stream chunk ready to yield or queue.push\n */\n routingResult(result: RoutingClassificationPayload): ProtocolStreamChunk {\n return { type: 'metadata', content: { routing: result } };\n },\n\n // ============================================\n // Handoff Protocol Builders\n // ============================================\n\n /**\n * Request a handoff to another chatbot.\n *\n * The Studio proxy pushes a handoff stack entry and routes subsequent\n * user messages to the target handler. If `options.consent` is set,\n * Studio stores the request as a pending offer instead.\n *\n * @param payload - Handoff target, reason, context, and options\n * @returns A metadata stream chunk ready to yield or queue.push\n */\n handoffRequest(payload: HandoffRequestPayload): ProtocolStreamChunk {\n return { type: 'metadata', content: { handoffRequest: payload } };\n },\n\n /**\n * Return from a handoff back to the parent handler.\n *\n * The Studio proxy pops the handoff stack and delivers the return\n * result to the parent handler on the next user message.\n *\n * @param payload - Summary and artifacts from the completed handoff\n * @returns A metadata stream chunk ready to yield or queue.push\n */\n handoffReturn(payload: HandoffReturnPayload): ProtocolStreamChunk {\n return { type: 'metadata', content: { handoffReturn: payload } };\n },\n\n /**\n * Offer a handoff with user consent required.\n *\n * Studio stores this as a pending offer, sends an SSE event to the\n * frontend, and waits for the user to accept or decline before\n * executing the handoff.\n *\n * @param payload - Offer display info and the pending handoff request\n * @returns A metadata stream chunk ready to yield or queue.push\n */\n handoffOffer(payload: HandoffOfferPayload): ProtocolStreamChunk {\n return { type: 'metadata', content: { handoffOffer: payload } };\n },\n};\n\n// ============================================\n// Re-export payload types for convenience\n// ============================================\n\nexport type {\n SchemaDeclarationPayload,\n SchemaAgent,\n SchemaQuestion,\n AgentTransitionPayload,\n RoutingClassificationPayload,\n HandoffRequestPayload,\n HandoffReturnPayload,\n HandoffOfferPayload,\n};\n","/**\n * Platform Client\n *\n * SDK client for handlers to call back to FirstStep Studio.\n * Enables handlers to query platform data (chatbot list, etc.)\n * using the same API token used for request signature verification.\n *\n * @example\n * ```typescript\n * import { createPlatformClient } from '@firststep-studio/sdk';\n *\n * const client = createPlatformClient({\n * studioUrl: 'https://studio-api.example.com',\n * token: process.env.FIRSTSTEP_TOKEN,\n * });\n *\n * const chatbots = await client.listChatbots();\n * // [{ id: '...', name: 'Safety Navigator', configSlug: 'safety-nav' }, ...]\n * ```\n */\n\n// ============================================\n// Types\n// ============================================\n\nexport interface PlatformClientConfig {\n /** Studio backend API URL (e.g. https://studio-api.example.com) */\n studioUrl: string;\n /** API token (same as FIRSTSTEP_TOKEN used for signature verification) */\n token: string;\n /** Request timeout in ms (default: 10000) */\n timeout?: number;\n}\n\nexport interface PlatformChatbot {\n /** Studio chatbot MongoDB ID */\n id: string;\n /** Chatbot display name */\n name: string;\n /** Handler URL (if external handler configured) */\n handlerUrl?: string;\n /** Config slug for multi-config routing */\n configSlug?: string;\n /** Handler type */\n handlerType?: 'builtin' | 'external';\n}\n\nexport class PlatformError extends Error {\n status: number;\n constructor(message: string, status: number) {\n super(message);\n this.name = 'PlatformError';\n this.status = status;\n }\n}\n\n// ============================================\n// Client\n// ============================================\n\nexport class PlatformClient {\n private studioUrl: string;\n private token: string;\n private timeout: number;\n\n constructor(config: PlatformClientConfig) {\n this.studioUrl = config.studioUrl.replace(/\\/+$/, '');\n this.token = config.token;\n this.timeout = config.timeout || 10000;\n }\n\n /**\n * List chatbots accessible to this handler's organization.\n * Studio resolves the organization from the API token.\n */\n async listChatbots(): Promise<PlatformChatbot[]> {\n const res = await fetch(`${this.studioUrl}/api/protocol/chatbots`, {\n method: 'GET',\n headers: {\n 'Authorization': `Bearer ${this.token}`,\n 'Content-Type': 'application/json',\n },\n signal: AbortSignal.timeout(this.timeout),\n });\n\n if (!res.ok) {\n const body = await res.text().catch(() => '');\n throw new PlatformError(\n `Failed to list chatbots: ${res.status} ${body}`,\n res.status\n );\n }\n\n const data = await res.json() as { chatbots: PlatformChatbot[] };\n return data.chatbots;\n }\n}\n\n// ============================================\n// Factory\n// ============================================\n\n/**\n * Create a PlatformClient for calling back to FirstStep Studio.\n */\nexport function createPlatformClient(config: PlatformClientConfig): PlatformClient {\n return new PlatformClient(config);\n}\n","import { createHash, createHmac, timingSafeEqual } from 'crypto';\n\nconst TOKEN_PREFIX = 'fst_';\nconst TOKEN_LENGTH = 44; // fst_ (4) + 40 hex chars\n\n/**\n * Hash a token to derive the shared HMAC key.\n * The backend only stores SHA-256(token) and uses that hash as the HMAC key.\n * The SDK must hash the plaintext token the same way to verify signatures.\n */\nfunction deriveSigningKey(token: string): string {\n return createHash('sha256').update(token).digest('hex');\n}\n\n/**\n * Verify an HMAC-SHA256 request signature.\n *\n * The handler server can use this to verify that incoming webhook\n * requests were signed by the FirstStep platform using the shared token.\n *\n * The HMAC key is SHA-256(token), matching the backend which only stores\n * the token hash and uses it directly as the HMAC key.\n *\n * @param token - The API token (FIRSTSTEP_TOKEN)\n * @param payload - The raw request body string\n * @param signature - The signature from the X-FirstStep-Signature header\n * @returns true if the signature is valid\n *\n * @example\n * ```typescript\n * import { verifyRequestSignature } from '@firststep-studio/sdk';\n *\n * app.post('/webhook', (req, res) => {\n * const signature = req.headers['x-firststep-signature'] as string;\n * if (!verifyRequestSignature(process.env.FIRSTSTEP_TOKEN!, req.body, signature)) {\n * return res.status(401).send('Invalid signature');\n * }\n * // Process the request...\n * });\n * ```\n */\nexport function verifyRequestSignature(\n token: string,\n payload: string,\n signature: string\n): boolean {\n try {\n const signingKey = deriveSigningKey(token);\n const expected = createHmac('sha256', signingKey)\n .update(payload)\n .digest('hex');\n const expectedBuffer = Buffer.from(expected, 'hex');\n const signatureBuffer = Buffer.from(signature, 'hex');\n\n if (expectedBuffer.length !== signatureBuffer.length) {\n return false;\n }\n\n return timingSafeEqual(expectedBuffer, signatureBuffer);\n } catch {\n return false;\n }\n}\n\n/**\n * Create an HMAC-SHA256 signature for a payload.\n *\n * Used by the platform to sign outgoing requests to handler servers.\n *\n * @param token - The API token\n * @param payload - The request body string to sign\n * @returns The hex-encoded HMAC signature\n */\nexport function createRequestSignature(\n token: string,\n payload: string\n): string {\n const signingKey = deriveSigningKey(token);\n return createHmac('sha256', signingKey).update(payload).digest('hex');\n}\n\n/**\n * Create an Authorization header value for API requests.\n *\n * @param token - The API token (fst_xxx)\n * @returns The header value, e.g. \"Bearer fst_xxx\"\n *\n * @example\n * ```typescript\n * import { createAuthHeader } from '@firststep-studio/sdk';\n *\n * const response = await fetch('https://api.firststep.ai/api/projects', {\n * headers: {\n * 'Authorization': createAuthHeader(process.env.FIRSTSTEP_TOKEN!),\n * },\n * });\n * ```\n */\nexport function createAuthHeader(token: string): string {\n return `Bearer ${token}`;\n}\n\n/**\n * Validate that a token has the correct format (fst_ prefix + 40 hex chars).\n *\n * @param token - The token string to validate\n * @returns true if the token matches the expected format\n */\nexport function isValidToken(token: string): boolean {\n return (\n typeof token === 'string' &&\n token.startsWith(TOKEN_PREFIX) &&\n token.length === TOKEN_LENGTH\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/ucp/client.ts","../src/renderMarkers.ts","../src/streamMetadata.ts","../src/platform.ts","../src/auth.ts","../src/llm.ts"],"sourcesContent":["/**\n * UCP (Universal Classification Protocol) Client\n *\n * Client for communicating with UCP-compliant classifier endpoints.\n * Supports both full configuration and simple endpoint URL.\n *\n * @example\n * ```typescript\n * // Simple usage with endpoint URL\n * const client = UCPClient.fromEndpoint('https://api.example.com/ucp/v1/classifiers/abc123');\n *\n * // With API key\n * const client = UCPClient.fromEndpoint(\n * 'https://api.example.com/ucp/v1/classifiers/abc123',\n * { apiKey: 'your-api-key' }\n * );\n *\n * // Classify messages\n * const result = await client.classify(messages);\n * ```\n */\n\nimport type {\n UCPMessage,\n UCPClassifyRequest,\n UCPClassifyResponse,\n UCPErrorResponse,\n UCPInfoResponse,\n UCPClientConfig,\n UCPEndpointConfig,\n ClassificationResult,\n} from './types';\nimport type { ChatMessage } from '../types';\n\n// Default timeout for requests\nconst DEFAULT_TIMEOUT = 30000;\n\n/**\n * UCP Client for classifier communication\n */\nexport class UCPClient {\n private baseUrl: string;\n private classifierId: string;\n private apiKey?: string;\n private timeout: number;\n private headers: Record<string, string>;\n\n constructor(config: UCPClientConfig) {\n // Normalize base URL (remove trailing slash)\n this.baseUrl = config.baseUrl.replace(/\\/$/, '');\n this.classifierId = config.classifierId;\n this.apiKey = config.apiKey;\n this.timeout = config.timeout ?? DEFAULT_TIMEOUT;\n this.headers = config.headers ?? {};\n }\n\n /**\n * Create client from a full endpoint URL\n *\n * URL format: {baseUrl}/classifiers/{classifierId}\n * Example: https://api.example.com/ucp/v1/classifiers/abc123\n */\n static fromEndpoint(\n endpoint: string,\n options?: { apiKey?: string; timeout?: number }\n ): UCPClient {\n // Parse endpoint URL to extract baseUrl and classifierId\n const url = new URL(endpoint);\n const pathParts = url.pathname.split('/');\n\n // Find \"classifiers\" in path and get the ID after it\n const classifiersIndex = pathParts.indexOf('classifiers');\n if (classifiersIndex === -1 || classifiersIndex >= pathParts.length - 1) {\n throw new Error(\n `Invalid UCP endpoint URL: ${endpoint}. Expected format: {baseUrl}/classifiers/{classifierId}`\n );\n }\n\n const classifierId = pathParts[classifiersIndex + 1];\n const baseUrlPath = pathParts.slice(0, classifiersIndex).join('/');\n const baseUrl = `${url.protocol}//${url.host}${baseUrlPath}`;\n\n return new UCPClient({\n baseUrl,\n classifierId,\n apiKey: options?.apiKey,\n timeout: options?.timeout,\n });\n }\n\n /**\n * Get the classify endpoint URL\n */\n get classifyUrl(): string {\n return `${this.baseUrl}/classifiers/${this.classifierId}/classify`;\n }\n\n /**\n * Get the info endpoint URL\n */\n get infoUrl(): string {\n return `${this.baseUrl}/classifiers/${this.classifierId}/info`;\n }\n\n /**\n * Classify messages using UCP protocol\n */\n async classify(messages: UCPMessage[]): Promise<ClassificationResult> {\n const request: UCPClassifyRequest = { messages };\n\n const response = await this.fetch<UCPClassifyResponse>(\n this.classifyUrl,\n {\n method: 'POST',\n body: JSON.stringify(request),\n }\n );\n\n // Normalize to SDK format\n return {\n classifierId: this.classifierId,\n category: response.category,\n level: response.level,\n confidence: response.score / 100, // Normalize to 0-1\n reasoning: response.rationale,\n raw: response,\n };\n }\n\n /**\n * Classify ChatMessage array (convenience method)\n * Converts ChatMessage to UCPMessage format\n */\n async classifyChat(messages: ChatMessage[]): Promise<ClassificationResult> {\n const ucpMessages: UCPMessage[] = messages\n .filter(m => m.role === 'user' || m.role === 'assistant')\n .map((m, index) => ({\n id: `msg-${index}`,\n role: m.role as 'user' | 'assistant',\n content: m.content,\n timestamp: m.timestamp ? m.timestamp.getTime() : Date.now(),\n metadata: m.metadata,\n }));\n\n return this.classify(ucpMessages);\n }\n\n /**\n * Get classifier info\n */\n async getInfo(): Promise<UCPInfoResponse> {\n return this.fetch<UCPInfoResponse>(this.infoUrl, {\n method: 'GET',\n });\n }\n\n /**\n * Check if the classifier endpoint is reachable\n */\n async healthCheck(): Promise<boolean> {\n try {\n const healthUrl = `${this.baseUrl}/health`;\n const response = await fetch(healthUrl, {\n method: 'GET',\n signal: AbortSignal.timeout(5000),\n });\n return response.ok;\n } catch {\n return false;\n }\n }\n\n /**\n * Internal fetch helper with error handling\n */\n private async fetch<T>(url: string, options: RequestInit): Promise<T> {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...this.headers,\n };\n\n if (this.apiKey) {\n headers['Authorization'] = `Bearer ${this.apiKey}`;\n }\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.timeout);\n\n try {\n const response = await fetch(url, {\n ...options,\n headers,\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n const ucpError = errorData as UCPErrorResponse;\n\n throw new UCPError(\n ucpError.error?.code || `HTTP_${response.status}`,\n ucpError.error?.message || `HTTP ${response.status}: ${response.statusText}`,\n ucpError.error?.details\n );\n }\n\n return response.json() as Promise<T>;\n } catch (error) {\n clearTimeout(timeoutId);\n\n if (error instanceof UCPError) {\n throw error;\n }\n\n if (error instanceof Error) {\n if (error.name === 'AbortError') {\n throw new UCPError('TIMEOUT', `Request timed out after ${this.timeout}ms`);\n }\n throw new UCPError('NETWORK_ERROR', error.message);\n }\n\n throw new UCPError('UNKNOWN_ERROR', 'An unknown error occurred');\n }\n }\n}\n\n/**\n * UCP-specific error class\n */\nexport class UCPError extends Error {\n constructor(\n public code: string,\n message: string,\n public details?: unknown\n ) {\n super(message);\n this.name = 'UCPError';\n }\n}\n\n// ============================================\n// Convenience Functions\n// ============================================\n\n/**\n * Create a UCP client from an endpoint URL\n */\nexport function createUCPClient(\n endpoint: string,\n options?: { apiKey?: string; timeout?: number }\n): UCPClient {\n return UCPClient.fromEndpoint(endpoint, options);\n}\n\n/**\n * Classify messages using a UCP endpoint\n * One-shot function for simple usage\n */\nexport async function classifyWithUCP(\n endpoint: string,\n messages: ChatMessage[],\n options?: { apiKey?: string; timeout?: number }\n): Promise<ClassificationResult> {\n const client = createUCPClient(endpoint, options);\n return client.classifyChat(messages);\n}\n","/**\n * Render Markers\n *\n * Utilities for building render markers that FirstStep Studio's frontend\n * can parse and display as rich UI components (cards, alerts, forms, etc.).\n *\n * This module is the single source of truth for render marker specs.\n * - Handlers (producers) use the builder functions to emit markers\n * - Studio frontend (consumer) uses the types and MARKER_TYPES constants to parse them\n *\n * @example\n * ```typescript\n * import { renderMarkers } from '@firststep-studio/sdk';\n *\n * // In your handleStream():\n * yield {\n * type: 'text',\n * content: renderMarkers.helplineCard({\n * helplines: [{ name: '988 Lifeline', phoneNumber: '988', categories: ['crisis'], status: 'open', statusLabel: 'Available' }],\n * }),\n * };\n * ```\n */\n\n// ============================================\n// Marker Type Constants\n// ============================================\n\nexport const MARKER_TYPES = {\n HELPLINE_CARD: 'RENDER_HELPLINE_CARD',\n EMERGENCY: 'RENDER_EMERGENCY',\n RESOURCE_CARD: 'RENDER_RESOURCE_CARD',\n PROVIDER_CARD: 'RENDER_PROVIDER_CARD',\n SAFETY_PLAN: 'RENDER_SAFETY_PLAN',\n REPORT_CARD: 'RENDER_REPORT_CARD',\n} as const;\n\nexport type MarkerType = typeof MARKER_TYPES[keyof typeof MARKER_TYPES];\n\n// ============================================\n// Payload Types\n// ============================================\n\n/**\n * Helpline card payload.\n * Renders a carousel of helpline cards with contact buttons (call, text, chat, WhatsApp).\n */\nexport interface HelplineCardPayload {\n helplines: HelplineCardItem[];\n type?: 'throughline' | 'throughline_fallback' | 'stage';\n fallback?: {\n message: string;\n linkText?: string;\n linkUrl?: string;\n icon?: string;\n topic?: string;\n topics?: string[];\n };\n}\n\nexport interface HelplineCardItem {\n name: string;\n description?: string;\n categories: string[];\n status: 'open' | 'closed';\n statusLabel: string;\n statusBadge?: string;\n hoursText?: string;\n supportTypes?: string;\n smsNumber?: string;\n phoneNumber?: string;\n website?: string;\n webchat?: string;\n whatsapp?: string;\n specialties?: string[];\n highlightedTag?: string;\n verified?: boolean;\n}\n\n/**\n * Emergency number payload.\n * Renders a prominent alert card with a large call button.\n */\nexport interface EmergencyPayload {\n number: string;\n countryName?: string;\n countryCode?: string;\n}\n\n/**\n * Resource card payload.\n * Renders a carousel of resource cards with video thumbnails, tags, and visit buttons.\n */\nexport interface ResourceCardPayload {\n resources: ResourceCardItem[];\n}\n\nexport interface ResourceCardItem {\n name: string;\n url?: string;\n description?: string;\n video_url?: string;\n type?: string;\n tags?: string[];\n highlightedTag?: string;\n}\n\n/**\n * Provider card payload.\n * Renders a carousel of provider directory cards with specialty/language tags and contact info.\n */\nexport interface ProviderCardPayload {\n providers: ProviderCardItem[];\n}\n\nexport interface ProviderCardItem {\n id: string;\n name: string;\n type?: string;\n specialty: string[];\n language: string[];\n description?: string;\n location?: string;\n open_hours?: string;\n contact_phone?: string;\n contact_email?: string;\n address?: string;\n}\n\n/**\n * Safety plan payload.\n * Renders a multi-section table with expandable sections and save/export actions.\n */\nexport interface SafetyPlanPayload {\n sections: SafetyPlanSection[];\n helplines?: Array<{ name: string; phone?: string }>;\n actions?: {\n savePng?: boolean;\n saveTxt?: boolean;\n copy?: boolean;\n };\n}\n\nexport interface SafetyPlanSection {\n id: string;\n title: string;\n items: Array<string | { name: string; phone?: string; description?: string }>;\n}\n\n/**\n * Report card payload.\n * Renders a summary card of collected report data with a submit button.\n */\nexport interface ReportCardPayload {\n topic: string;\n location?: string;\n description?: string;\n perpetrator_known?: boolean;\n contact_mode?: string;\n contact_value?: string;\n submitEndpoint?: string;\n}\n\n// ============================================\n// Builder Functions\n// ============================================\n\n/**\n * Wrap a payload in render marker tags.\n * Internal helper used by all builder functions.\n */\nfunction wrapMarker(type: string, payload: unknown): string {\n return `[${type}]${JSON.stringify(payload)}[/${type}]`;\n}\n\n/**\n * Render marker builder functions.\n *\n * Each function takes a typed payload and returns a render marker string\n * that can be yielded as a `text` chunk in handleStream().\n *\n * @example\n * ```typescript\n * // Emit helpline cards\n * yield { type: 'text', content: renderMarkers.helplineCard({ helplines: [...] }) };\n *\n * // Emit emergency number\n * yield { type: 'text', content: renderMarkers.emergency({ number: '911', countryName: 'United States' }) };\n * ```\n */\nexport const renderMarkers = {\n /** Build a helpline card carousel marker */\n helplineCard: (payload: HelplineCardPayload): string =>\n wrapMarker(MARKER_TYPES.HELPLINE_CARD, payload),\n\n /** Build an emergency number alert marker */\n emergency: (payload: EmergencyPayload): string =>\n wrapMarker(MARKER_TYPES.EMERGENCY, payload),\n\n /** Build a resource card carousel marker */\n resourceCard: (payload: ResourceCardPayload): string =>\n wrapMarker(MARKER_TYPES.RESOURCE_CARD, payload),\n\n /** Build a provider card carousel marker */\n providerCard: (payload: ProviderCardPayload): string =>\n wrapMarker(MARKER_TYPES.PROVIDER_CARD, payload),\n\n /** Build a safety plan artifact marker */\n safetyPlan: (payload: SafetyPlanPayload): string =>\n wrapMarker(MARKER_TYPES.SAFETY_PLAN, payload),\n\n /** Build a report draft card marker */\n reportCard: (payload: ReportCardPayload): string =>\n wrapMarker(MARKER_TYPES.REPORT_CARD, payload),\n};\n","/**\n * Stream Metadata Builders\n *\n * Utilities for building typed metadata stream chunks that FirstStep Studio's\n * proxy persists to MongoDB for Dashboard features (Form Insights, Session\n * History, Routing Logs, Agent Transitions).\n *\n * Each function returns a ready-to-yield ProtocolStreamChunk. No need to\n * construct the `{ type, content }` wrapper manually.\n *\n * @example\n * ```typescript\n * import { streamMetadata } from '@firststep-studio/sdk';\n *\n * // In your handleStream():\n *\n * // 1. Welcome path: declare your schema once\n * yield streamMetadata.declareSchema({\n * agents: [{ id: 'intake', title: 'Intake', order: 0 }],\n * questions: [{ id: 'name', agentId: 'intake', title: 'Name', type: 'text' }],\n * });\n *\n * // 2. After each turn: send collected field values\n * yield streamMetadata.formDataUpdate({ name: 'Alice', age: '28' });\n *\n * // 3. On stage change: signal an agent transition\n * yield streamMetadata.agentTransition({ id: 'support', title: 'Support' });\n *\n * // 4. On classification: send routing result\n * yield streamMetadata.routingResult({\n * decision: 'classified',\n * reason: 'User mentioned self-harm',\n * category: 'crisis',\n * level: 'high',\n * score: 92,\n * });\n * ```\n */\n\nimport type {\n ProtocolStreamChunk,\n SchemaDeclarationPayload,\n SchemaAgent,\n SchemaQuestion,\n AgentTransitionPayload,\n RoutingClassificationPayload,\n HandoffRequestPayload,\n HandoffReturnPayload,\n HandoffOfferPayload,\n} from './types';\n\n// ============================================\n// Builder Functions\n// ============================================\n\nexport const streamMetadata = {\n /**\n * Declare the form schema (agents + questions) for Dashboard Form Insights.\n *\n * Yield this once during session initialization (welcome message).\n * The Studio proxy stores it in `chatbot.externalSchema` so that\n * Dashboard Form Insights can display question-level analytics.\n *\n * @param schema - Agents and questions your handler uses\n * @returns A metadata stream chunk ready to yield or queue.push\n */\n declareSchema(schema: SchemaDeclarationPayload): ProtocolStreamChunk {\n return { type: 'metadata', content: { schema } };\n },\n\n /**\n * Send collected form field values for Dashboard persistence.\n *\n * Call this after each turn when new fields are captured.\n * Values are incrementally merged into `ChatSession.formData`.\n *\n * @param fields - Key-value pairs of field IDs to their collected values\n * @returns A metadata stream chunk ready to yield or queue.push\n */\n formDataUpdate(fields: Record<string, string | number>): ProtocolStreamChunk {\n return { type: 'metadata', content: { formData: fields } };\n },\n\n /**\n * Signal an agent/stage transition for routing logs.\n *\n * The Studio proxy records this in `ChatSession.routingLogs` so\n * the Dashboard can show the conversation's agent flow.\n *\n * @param agent - The agent/stage being transitioned to\n * @returns A metadata stream chunk ready to yield or queue.push\n */\n agentTransition(agent: AgentTransitionPayload): ProtocolStreamChunk {\n return { type: 'metadata', content: { currentAgent: agent } };\n },\n\n /**\n * Send a routing/classification result for routing logs.\n *\n * The Studio proxy records this in `ChatSession.routingLogs` as a\n * classification event with category, level, and confidence score.\n *\n * @param result - Classification/routing decision details\n * @returns A metadata stream chunk ready to yield or queue.push\n */\n routingResult(result: RoutingClassificationPayload): ProtocolStreamChunk {\n return { type: 'metadata', content: { routing: result } };\n },\n\n // ============================================\n // Handoff Protocol Builders\n // ============================================\n\n /**\n * Request a handoff to another chatbot.\n *\n * The Studio proxy pushes a handoff stack entry and routes subsequent\n * user messages to the target handler. If `options.consent` is set,\n * Studio stores the request as a pending offer instead.\n *\n * @param payload - Handoff target, reason, context, and options\n * @returns A metadata stream chunk ready to yield or queue.push\n */\n handoffRequest(payload: HandoffRequestPayload): ProtocolStreamChunk {\n return { type: 'metadata', content: { handoffRequest: payload } };\n },\n\n /**\n * Return from a handoff back to the parent handler.\n *\n * The Studio proxy pops the handoff stack and delivers the return\n * result to the parent handler on the next user message.\n *\n * @param payload - Summary and artifacts from the completed handoff\n * @returns A metadata stream chunk ready to yield or queue.push\n */\n handoffReturn(payload: HandoffReturnPayload): ProtocolStreamChunk {\n return { type: 'metadata', content: { handoffReturn: payload } };\n },\n\n /**\n * Offer a handoff with user consent required.\n *\n * Studio stores this as a pending offer, sends an SSE event to the\n * frontend, and waits for the user to accept or decline before\n * executing the handoff.\n *\n * @param payload - Offer display info and the pending handoff request\n * @returns A metadata stream chunk ready to yield or queue.push\n */\n handoffOffer(payload: HandoffOfferPayload): ProtocolStreamChunk {\n return { type: 'metadata', content: { handoffOffer: payload } };\n },\n};\n\n// ============================================\n// Re-export payload types for convenience\n// ============================================\n\nexport type {\n SchemaDeclarationPayload,\n SchemaAgent,\n SchemaQuestion,\n AgentTransitionPayload,\n RoutingClassificationPayload,\n HandoffRequestPayload,\n HandoffReturnPayload,\n HandoffOfferPayload,\n};\n","/**\n * Platform Client\n *\n * SDK client for handlers to call back to FirstStep Studio.\n * Enables handlers to query platform data (chatbot list, etc.)\n * using the same API token used for request signature verification.\n *\n * @example\n * ```typescript\n * import { createPlatformClient } from '@firststep-studio/sdk';\n *\n * const client = createPlatformClient({\n * studioUrl: 'https://studio-api.example.com',\n * token: process.env.FIRSTSTEP_TOKEN,\n * });\n *\n * const chatbots = await client.listChatbots();\n * // [{ id: '...', name: 'Safety Navigator', configSlug: 'safety-nav' }, ...]\n * ```\n */\n\n// ============================================\n// Types\n// ============================================\n\nexport interface PlatformClientConfig {\n /** Studio backend API URL (e.g. https://studio-api.example.com) */\n studioUrl: string;\n /** API token (same as FIRSTSTEP_TOKEN used for signature verification) */\n token: string;\n /** Request timeout in ms (default: 10000) */\n timeout?: number;\n}\n\nexport interface PlatformOrganization {\n /** Studio organization ID */\n id: string;\n /** Display name (null if the organization record has been deleted) */\n name: string | null;\n /** URL-safe slug */\n slug: string | null;\n}\n\nexport interface PlatformTokenInfo {\n /** API token ID */\n id: string;\n /** Human-friendly token label */\n name: string;\n /** Authorized scopes */\n scopes: string[];\n /** Project IDs this token is scoped to (null = full org access) */\n projectIds: string[] | null;\n}\n\nexport interface PlatformIdentity {\n organization: PlatformOrganization;\n token: PlatformTokenInfo;\n}\n\nexport interface PlatformChatbotRegisterInput {\n /** Stable handler-side identifier used to upsert (e.g. handler config ID). */\n externalId: string;\n /** Display name shown in Studio's chatbot list. */\n name: string;\n /** Optional human description. */\n description?: string;\n /** Public URL where Studio should send protocol requests. */\n handlerUrl: string;\n /** Slug for multi-config routing on the handler side. */\n configSlug?: string;\n /** Optional capability flags advertised to Studio. */\n capabilities?: {\n streaming?: boolean;\n formQuestions?: boolean;\n knowledgeActions?: boolean;\n integrations?: boolean;\n [key: string]: unknown;\n };\n}\n\nexport interface PlatformChatbotRegistration {\n /** Studio chatbot MongoDB ID (immutable across re-registrations). */\n id: string;\n /** Echoes back the externalId used as upsert key. */\n externalId: string;\n /** Resolved name as stored in Studio. */\n name: string;\n protocolConfig: Record<string, unknown>;\n}\n\nexport interface PlatformChatbot {\n /** Studio chatbot MongoDB ID */\n id: string;\n /** Chatbot display name */\n name: string;\n /** Handler URL (if external handler configured) */\n handlerUrl?: string;\n /** Config slug for multi-config routing */\n configSlug?: string;\n /** Handler type */\n handlerType?: 'builtin' | 'external';\n}\n\nexport class PlatformError extends Error {\n status: number;\n constructor(message: string, status: number) {\n super(message);\n this.name = 'PlatformError';\n this.status = status;\n }\n}\n\n// ============================================\n// Client\n// ============================================\n\nexport class PlatformClient {\n private studioUrl: string;\n private token: string;\n private timeout: number;\n\n constructor(config: PlatformClientConfig) {\n this.studioUrl = config.studioUrl.replace(/\\/+$/, '');\n this.token = config.token;\n this.timeout = config.timeout || 10000;\n }\n\n private async request<T>(\n path: string,\n errorPrefix: string,\n init: { method?: string; body?: unknown } = {}\n ): Promise<T> {\n const res = await fetch(`${this.studioUrl}${path}`, {\n method: init.method || 'GET',\n headers: {\n 'Authorization': `Bearer ${this.token}`,\n 'Content-Type': 'application/json',\n },\n body: init.body !== undefined ? JSON.stringify(init.body) : undefined,\n signal: AbortSignal.timeout(this.timeout),\n });\n\n if (!res.ok) {\n const body = await res.text().catch(() => '');\n throw new PlatformError(`${errorPrefix}: ${res.status} ${body}`, res.status);\n }\n if (res.status === 204) return undefined as T;\n return res.json() as Promise<T>;\n }\n\n /**\n * Resolve the organization and token metadata for the configured token.\n * Useful for connection state UI (\"Connected to {orgName}\") and audit.\n */\n async getMe(): Promise<PlatformIdentity> {\n return this.request<PlatformIdentity>('/api/protocol/me', 'Failed to fetch identity');\n }\n\n /**\n * List chatbots accessible to this handler's organization.\n * Studio resolves the organization from the API token.\n */\n async listChatbots(): Promise<PlatformChatbot[]> {\n const data = await this.request<{ chatbots: PlatformChatbot[] }>(\n '/api/protocol/chatbots',\n 'Failed to list chatbots'\n );\n return data.chatbots;\n }\n\n /**\n * Register or update an external-handler chatbot under the token's\n * organization. Idempotent: identified by `externalId`, so handlers\n * can call this on every config save without creating duplicates.\n */\n async registerChatbot(input: PlatformChatbotRegisterInput): Promise<PlatformChatbotRegistration> {\n return this.request<PlatformChatbotRegistration>(\n '/api/protocol/chatbots',\n 'Failed to register chatbot',\n { method: 'POST', body: input }\n );\n }\n\n /**\n * Mark the chatbot identified by `externalId` as disconnected.\n * The Studio record is preserved (history, analytics survive).\n */\n async unregisterChatbot(externalId: string): Promise<{ ok: boolean; modified: number }> {\n return this.request<{ ok: boolean; modified: number }>(\n `/api/protocol/chatbots/${encodeURIComponent(externalId)}`,\n 'Failed to unregister chatbot',\n { method: 'DELETE' }\n );\n }\n}\n\n// ============================================\n// Factory\n// ============================================\n\n/**\n * Create a PlatformClient for calling back to FirstStep Studio.\n */\nexport function createPlatformClient(config: PlatformClientConfig): PlatformClient {\n return new PlatformClient(config);\n}\n","import { createHash, createHmac, timingSafeEqual } from 'crypto';\n\nconst TOKEN_PREFIX = 'fst_';\nconst TOKEN_LENGTH = 44; // fst_ (4) + 40 hex chars\n\n/**\n * Hash a token to derive the shared HMAC key.\n * The backend only stores SHA-256(token) and uses that hash as the HMAC key.\n * The SDK must hash the plaintext token the same way to verify signatures.\n */\nfunction deriveSigningKey(token: string): string {\n return createHash('sha256').update(token).digest('hex');\n}\n\n/**\n * Verify an HMAC-SHA256 request signature.\n *\n * The handler server can use this to verify that incoming webhook\n * requests were signed by the FirstStep platform using the shared token.\n *\n * The HMAC key is SHA-256(token), matching the backend which only stores\n * the token hash and uses it directly as the HMAC key.\n *\n * @param token - The API token (FIRSTSTEP_TOKEN)\n * @param payload - The raw request body string\n * @param signature - The signature from the X-FirstStep-Signature header\n * @returns true if the signature is valid\n *\n * @example\n * ```typescript\n * import { verifyRequestSignature } from '@firststep-studio/sdk';\n *\n * app.post('/webhook', (req, res) => {\n * const signature = req.headers['x-firststep-signature'] as string;\n * if (!verifyRequestSignature(process.env.FIRSTSTEP_TOKEN!, req.body, signature)) {\n * return res.status(401).send('Invalid signature');\n * }\n * // Process the request...\n * });\n * ```\n */\nexport function verifyRequestSignature(\n token: string,\n payload: string,\n signature: string\n): boolean {\n try {\n const signingKey = deriveSigningKey(token);\n const expected = createHmac('sha256', signingKey)\n .update(payload)\n .digest('hex');\n const expectedBuffer = Buffer.from(expected, 'hex');\n const signatureBuffer = Buffer.from(signature, 'hex');\n\n if (expectedBuffer.length !== signatureBuffer.length) {\n return false;\n }\n\n return timingSafeEqual(expectedBuffer, signatureBuffer);\n } catch {\n return false;\n }\n}\n\n/**\n * Create an HMAC-SHA256 signature for a payload.\n *\n * Used by the platform to sign outgoing requests to handler servers.\n *\n * @param token - The API token\n * @param payload - The request body string to sign\n * @returns The hex-encoded HMAC signature\n */\nexport function createRequestSignature(\n token: string,\n payload: string\n): string {\n const signingKey = deriveSigningKey(token);\n return createHmac('sha256', signingKey).update(payload).digest('hex');\n}\n\n/**\n * Create an Authorization header value for API requests.\n *\n * @param token - The API token (fst_xxx)\n * @returns The header value, e.g. \"Bearer fst_xxx\"\n *\n * @example\n * ```typescript\n * import { createAuthHeader } from '@firststep-studio/sdk';\n *\n * const response = await fetch('https://api.firststep.ai/api/projects', {\n * headers: {\n * 'Authorization': createAuthHeader(process.env.FIRSTSTEP_TOKEN!),\n * },\n * });\n * ```\n */\nexport function createAuthHeader(token: string): string {\n return `Bearer ${token}`;\n}\n\n/**\n * Validate that a token has the correct format (fst_ prefix + 40 hex chars).\n *\n * @param token - The token string to validate\n * @returns true if the token matches the expected format\n */\nexport function isValidToken(token: string): boolean {\n return (\n typeof token === 'string' &&\n token.startsWith(TOKEN_PREFIX) &&\n token.length === TOKEN_LENGTH\n );\n}\n","/**\n * LLM Client\n *\n * SDK client for calling Studio's `/api/llm/*` proxy routes. Studio forwards\n * the request body 1:1 to Google's Generative Language REST API, so the\n * shapes here mirror Google's REST shape (NOT the @google/genai SDK shape).\n *\n * Reference: https://ai.google.dev/api/generate-content\n *\n * @example\n * ```typescript\n * import { createLLMClient } from '@firststep-studio/sdk';\n *\n * const llm = createLLMClient({\n * studioUrl: 'https://studio-api.example.com',\n * token: process.env.FIRSTSTEP_TOKEN!,\n * });\n *\n * // Single-shot\n * const res = await llm.generate({\n * model: 'gemini-3-flash-preview',\n * contents: [{ role: 'user', parts: [{ text: 'Hello' }] }],\n * generationConfig: { temperature: 0.7, maxOutputTokens: 500 },\n * });\n * console.log(res.candidates[0].content.parts[0].text);\n *\n * // Streaming\n * for await (const chunk of llm.generateStream({\n * model: 'gemini-3-flash-preview',\n * contents: [{ role: 'user', parts: [{ text: 'Count to 5' }] }],\n * })) {\n * process.stdout.write(chunk.candidates?.[0]?.content?.parts?.[0]?.text ?? '');\n * }\n *\n * // Cache\n * const cache = await llm.cache.create({\n * model: 'models/gemini-2.5-flash',\n * systemInstruction: { parts: [{ text: 'long system prompt...' }] },\n * ttl: '3600s',\n * });\n * await llm.cache.get(cache.name);\n * await llm.cache.delete(cache.name);\n * ```\n */\n\n// ============================================\n// Types — Google REST API shape (kept loose for forward-compat)\n// ============================================\n\nexport interface LLMClientConfig {\n /** Studio backend API URL (e.g. https://studio-api.example.com) */\n studioUrl: string;\n /** API token (Bearer fst_<...>) */\n token: string;\n /** Request timeout in ms for non-streaming calls (default: 60000) */\n timeout?: number;\n}\n\nexport interface LLMPart {\n text?: string;\n inlineData?: { mimeType: string; data: string };\n [k: string]: unknown;\n}\n\nexport interface LLMContent {\n role?: 'user' | 'model';\n parts: LLMPart[];\n}\n\nexport interface LLMGenerationConfig {\n temperature?: number;\n maxOutputTokens?: number;\n topP?: number;\n topK?: number;\n responseMimeType?: string;\n responseSchema?: Record<string, unknown>;\n thinkingConfig?: {\n thinkingBudget?: number;\n thinkingLevel?: 'minimal' | 'low' | 'medium' | 'high';\n includeThoughts?: boolean;\n };\n stopSequences?: string[];\n [k: string]: unknown;\n}\n\nexport interface LLMSafetySetting {\n category: string;\n threshold: string;\n}\n\n/**\n * Request body for /api/llm/generate and /api/llm/generate/stream.\n * Mirrors Google's `:generateContent` REST request, plus `model` at the top level\n * so Studio can build the URL path.\n */\nexport interface LLMGenerateRequest {\n model: string;\n contents: LLMContent[];\n systemInstruction?: { parts: LLMPart[] };\n generationConfig?: LLMGenerationConfig;\n safetySettings?: LLMSafetySetting[];\n /** Resource name of an explicit cache, e.g. \"cachedContents/abc123\" */\n cachedContent?: string;\n /** Function-calling tools (forwarded as-is) */\n tools?: unknown[];\n [k: string]: unknown;\n}\n\nexport interface LLMUsageMetadata {\n promptTokenCount?: number;\n candidatesTokenCount?: number;\n cachedContentTokenCount?: number;\n thoughtsTokenCount?: number;\n totalTokenCount?: number;\n [k: string]: unknown;\n}\n\nexport interface LLMCandidate {\n content?: LLMContent;\n finishReason?: string;\n index?: number;\n safetyRatings?: unknown[];\n [k: string]: unknown;\n}\n\nexport interface LLMGenerateResponse {\n candidates?: LLMCandidate[];\n usageMetadata?: LLMUsageMetadata;\n promptFeedback?: { blockReason?: string; [k: string]: unknown };\n modelVersion?: string;\n responseId?: string;\n [k: string]: unknown;\n}\n\nexport interface LLMCacheCreateRequest {\n model: string;\n displayName?: string;\n systemInstruction?: { parts: LLMPart[] };\n contents?: LLMContent[];\n /** Time-to-live, e.g. \"3600s\" */\n ttl?: string;\n [k: string]: unknown;\n}\n\nexport interface LLMCachedContent {\n name: string;\n model: string;\n createTime?: string;\n updateTime?: string;\n expireTime?: string;\n displayName?: string;\n usageMetadata?: { totalTokenCount?: number };\n [k: string]: unknown;\n}\n\nexport class LLMError extends Error {\n status: number;\n body: unknown;\n constructor(message: string, status: number, body: unknown) {\n super(message);\n this.name = 'LLMError';\n this.status = status;\n this.body = body;\n }\n}\n\n// ============================================\n// Client\n// ============================================\n\nexport class LLMClient {\n private studioUrl: string;\n private token: string;\n private timeout: number;\n\n constructor(config: LLMClientConfig) {\n this.studioUrl = config.studioUrl.replace(/\\/+$/, '');\n this.token = config.token;\n this.timeout = config.timeout ?? 60000;\n }\n\n private headers(): Record<string, string> {\n return {\n 'Authorization': `Bearer ${this.token}`,\n 'Content-Type': 'application/json',\n };\n }\n\n /**\n * Single-shot generation. Body is forwarded to Google's :generateContent\n * unchanged. Response is Google's response shape unchanged.\n */\n async generate(req: LLMGenerateRequest): Promise<LLMGenerateResponse> {\n const res = await fetch(`${this.studioUrl}/api/llm/generate`, {\n method: 'POST',\n headers: this.headers(),\n body: JSON.stringify(req),\n signal: AbortSignal.timeout(this.timeout),\n });\n\n const text = await res.text();\n let body: unknown;\n try { body = text ? JSON.parse(text) : undefined; } catch { body = { raw: text }; }\n\n if (!res.ok) {\n throw new LLMError(`LLM generate failed: ${res.status}`, res.status, body);\n }\n return body as LLMGenerateResponse;\n }\n\n /**\n * Streaming generation as an AsyncGenerator of partial responses.\n * Each yielded chunk has the same shape as a non-streaming response (a\n * `candidates[]` slice with the next text fragment, plus usageMetadata\n * on the final chunk).\n *\n * Note: timeout option is not enforced for streams; consumer should\n * abort externally if needed.\n */\n async *generateStream(req: LLMGenerateRequest): AsyncGenerator<LLMGenerateResponse> {\n const res = await fetch(`${this.studioUrl}/api/llm/generate/stream`, {\n method: 'POST',\n headers: this.headers(),\n body: JSON.stringify(req),\n });\n\n if (!res.ok || !res.body) {\n const text = await res.text().catch(() => '');\n let body: unknown;\n try { body = text ? JSON.parse(text) : undefined; } catch { body = { raw: text }; }\n throw new LLMError(`LLM generateStream failed: ${res.status}`, res.status, body);\n }\n\n // Consume SSE: lines like `data: {json}\\n\\n`. We pop full lines off a\n // rolling buffer and yield each parsed `data:` payload.\n const reader = (res.body as ReadableStream<Uint8Array>).getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n\n const lines = buffer.split('\\n');\n buffer = lines.pop() ?? '';\n for (const line of lines) {\n const t = line.trim();\n if (!t.startsWith('data: ')) continue;\n const payload = t.slice(6);\n if (payload === '[DONE]') continue;\n try {\n yield JSON.parse(payload) as LLMGenerateResponse;\n } catch {\n // Skip unparsable chunk; SSE may emit stray comments / heartbeats.\n }\n }\n }\n\n // Flush any final buffered line that didn't end in \\n.\n const t = buffer.trim();\n if (t.startsWith('data: ')) {\n const payload = t.slice(6);\n if (payload && payload !== '[DONE]') {\n try { yield JSON.parse(payload) as LLMGenerateResponse; } catch { /* noop */ }\n }\n }\n } finally {\n try { reader.releaseLock(); } catch { /* noop */ }\n }\n }\n\n /**\n * Explicit prompt cache surface. Maps to Google's `/cachedContents` REST API.\n */\n cache = {\n /**\n * Create an explicit cache. Returns the resource (use `cache.name` as the\n * `cachedContent` field on a future generate() request).\n */\n create: async (req: LLMCacheCreateRequest): Promise<LLMCachedContent> => {\n const res = await fetch(`${this.studioUrl}/api/llm/cache`, {\n method: 'POST',\n headers: this.headers(),\n body: JSON.stringify(req),\n signal: AbortSignal.timeout(this.timeout),\n });\n const text = await res.text();\n let body: unknown;\n try { body = text ? JSON.parse(text) : undefined; } catch { body = { raw: text }; }\n if (!res.ok) {\n throw new LLMError(`LLM cache.create failed: ${res.status}`, res.status, body);\n }\n return body as LLMCachedContent;\n },\n\n /**\n * Fetch a cache by resource name (e.g. \"cachedContents/abc123\").\n * Returns null when the cache is not found (404), which is the common\n * \"expired\" signal — callers can simply recreate.\n */\n get: async (name: string): Promise<LLMCachedContent | null> => {\n const res = await fetch(`${this.studioUrl}/api/llm/cache/${encodeURIComponent(name)}`, {\n method: 'GET',\n headers: this.headers(),\n signal: AbortSignal.timeout(this.timeout),\n });\n if (res.status === 404) return null;\n const text = await res.text();\n let body: unknown;\n try { body = text ? JSON.parse(text) : undefined; } catch { body = { raw: text }; }\n if (!res.ok) {\n throw new LLMError(`LLM cache.get failed: ${res.status}`, res.status, body);\n }\n return body as LLMCachedContent;\n },\n\n /**\n * Delete a cache by resource name. Idempotent: 404 is treated as success.\n */\n delete: async (name: string): Promise<void> => {\n const res = await fetch(`${this.studioUrl}/api/llm/cache/${encodeURIComponent(name)}`, {\n method: 'DELETE',\n headers: this.headers(),\n signal: AbortSignal.timeout(this.timeout),\n });\n if (res.ok || res.status === 404) return;\n const text = await res.text().catch(() => '');\n let body: unknown;\n try { body = text ? JSON.parse(text) : undefined; } catch { body = { raw: text }; }\n throw new LLMError(`LLM cache.delete failed: ${res.status}`, res.status, body);\n },\n };\n}\n\n// ============================================\n// Factory\n// ============================================\n\n/**\n * Create an LLMClient that talks to Studio's /api/llm/* proxy routes.\n */\nexport function createLLMClient(config: LLMClientConfig): LLMClient {\n return new LLMClient(config);\n}\n"],"mappings":"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","names":["t"]}
|
package/dist/server.d.mts
CHANGED
package/dist/server.d.ts
CHANGED
|
@@ -367,12 +367,75 @@ interface KnowledgeContext {
|
|
|
367
367
|
queryDatabase(knowledgeId: string, query: string): Promise<KnowledgeResult>;
|
|
368
368
|
/** Search page knowledge */
|
|
369
369
|
searchPages(knowledgeId: string, query: string): Promise<KnowledgeResult>;
|
|
370
|
+
/**
|
|
371
|
+
* Fetch knowledge metadata (name, type, column shape) without records.
|
|
372
|
+
*
|
|
373
|
+
* Used by handlers when generating system prompts: the LLM needs to know
|
|
374
|
+
* a knowledge base's columns/types to write valid queries against it,
|
|
375
|
+
* but loading the actual records would be wasteful (and big). Platform
|
|
376
|
+
* implementations of this method return a pre-resolved projection — the
|
|
377
|
+
* "main sheet" columns for database knowledge, just `name`+`type` for
|
|
378
|
+
* text knowledge — so handlers don't have to know about sheet selection
|
|
379
|
+
* or storage internals.
|
|
380
|
+
*
|
|
381
|
+
* Returns null when the knowledge id is not found.
|
|
382
|
+
*/
|
|
383
|
+
getMetadata?(knowledgeId: string): Promise<KnowledgeMetadata | null>;
|
|
370
384
|
}
|
|
371
385
|
interface KnowledgeResult {
|
|
372
386
|
success: boolean;
|
|
373
387
|
records?: Record<string, unknown>[];
|
|
374
388
|
error?: string;
|
|
375
389
|
}
|
|
390
|
+
/**
|
|
391
|
+
* Knowledge metadata projection returned by KnowledgeContext.getMetadata.
|
|
392
|
+
*
|
|
393
|
+
* Intentionally minimal: only the fields a handler needs to describe the
|
|
394
|
+
* knowledge to an LLM (or branch on type). Storage shape, sheet selection,
|
|
395
|
+
* record contents, etc. stay platform-internal.
|
|
396
|
+
*/
|
|
397
|
+
interface KnowledgeMetadata {
|
|
398
|
+
/** Knowledge id (echoed back so callers can correlate). */
|
|
399
|
+
id: string;
|
|
400
|
+
/** Display name. */
|
|
401
|
+
name: string;
|
|
402
|
+
/**
|
|
403
|
+
* Knowledge kind. `text` = unstructured pages searched semantically;
|
|
404
|
+
* `database` = tabular rows queried with structured filters. Other
|
|
405
|
+
* platform-specific kinds may be added; handlers should treat unknown
|
|
406
|
+
* values as opaque.
|
|
407
|
+
*/
|
|
408
|
+
type: 'text' | 'database' | string;
|
|
409
|
+
/**
|
|
410
|
+
* For database knowledge: the canonical (main-sheet) column shape that
|
|
411
|
+
* the LLM should be told about. Empty/undefined for non-database kinds.
|
|
412
|
+
*/
|
|
413
|
+
columns?: KnowledgeColumnMetadata[];
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Column metadata as exposed to handlers. Platform implementations
|
|
417
|
+
* project from their internal column model down to this shape — the LLM-
|
|
418
|
+
* facing prompt-builder only needs name, type, and any descriptive hints.
|
|
419
|
+
*/
|
|
420
|
+
interface KnowledgeColumnMetadata {
|
|
421
|
+
id: string;
|
|
422
|
+
name: string;
|
|
423
|
+
/** Column data type (e.g. 'text', 'number', 'url', 'email', 'date'). */
|
|
424
|
+
type: string;
|
|
425
|
+
description?: string;
|
|
426
|
+
/**
|
|
427
|
+
* Bag of analysis hints the platform may have produced (unique value
|
|
428
|
+
* counts, sample values, ranges). Optional and platform-defined; the
|
|
429
|
+
* SDK reserves the keys it documents and tolerates extras.
|
|
430
|
+
*/
|
|
431
|
+
metadata?: {
|
|
432
|
+
description?: string;
|
|
433
|
+
analysis?: {
|
|
434
|
+
uniqueCount?: number;
|
|
435
|
+
uniqueValues?: string[];
|
|
436
|
+
};
|
|
437
|
+
} & Record<string, unknown>;
|
|
438
|
+
}
|
|
376
439
|
interface IntegrationContext {
|
|
377
440
|
/** Execute integration */
|
|
378
441
|
execute(integrationId: string, params: Record<string, unknown>): Promise<IntegrationResult>;
|
|
@@ -651,4 +714,4 @@ interface HandoffInboundContext {
|
|
|
651
714
|
returnResult?: HandoffReturnPayload;
|
|
652
715
|
}
|
|
653
716
|
|
|
654
|
-
export type { AgentTransitionPayload as A,
|
|
717
|
+
export type { HandoffOptions as $, AgentTransitionPayload as A, RoutingDecision as B, ChatMessage as C, DeploymentInfo as D, ChatbotInfo as E, ClassifierConfig as F, FormData as G, HandoffRequestPayload as H, IntegrationContext as I, FormFieldValue as J, KnowledgeContext as K, LoggerContext as L, RoutingLog as M, SessionMetadata as N, AnalyticsContext as O, ProtocolStreamChunk as P, InteractionEvent as Q, RoutingClassificationPayload as R, SchemaDeclarationPayload as S, InteractionEventType as T, FormSchema as U, FormFieldDefinition as V, FormFieldType as W, ProtocolRegistration as X, SchemaAgent as Y, SchemaQuestion as Z, HandoffContext as _, HandoffReturnPayload as a, HandoffOfferPayload as b, ProtocolRequest as c, ProtocolResponse as d, SessionStatus as e, HandoffInboundContext as f, ProtocolForm as g, ProtocolFormField as h, ProtocolFormOption as i, ProtocolFieldValidation as j, ProtocolError as k, ProtocolHandler as l, ProtocolCapabilities as m, HandlerInfo as n, ProtocolContext as o, SessionContext as p, SessionState as q, KnowledgeResult as r, KnowledgeMetadata as s, KnowledgeColumnMetadata as t, IntegrationResult as u, HelplineSearchOptions as v, HelplineResult as w, Helpline as x, StorageContext as y, StorageSetOptions as z };
|
|
@@ -367,12 +367,75 @@ interface KnowledgeContext {
|
|
|
367
367
|
queryDatabase(knowledgeId: string, query: string): Promise<KnowledgeResult>;
|
|
368
368
|
/** Search page knowledge */
|
|
369
369
|
searchPages(knowledgeId: string, query: string): Promise<KnowledgeResult>;
|
|
370
|
+
/**
|
|
371
|
+
* Fetch knowledge metadata (name, type, column shape) without records.
|
|
372
|
+
*
|
|
373
|
+
* Used by handlers when generating system prompts: the LLM needs to know
|
|
374
|
+
* a knowledge base's columns/types to write valid queries against it,
|
|
375
|
+
* but loading the actual records would be wasteful (and big). Platform
|
|
376
|
+
* implementations of this method return a pre-resolved projection — the
|
|
377
|
+
* "main sheet" columns for database knowledge, just `name`+`type` for
|
|
378
|
+
* text knowledge — so handlers don't have to know about sheet selection
|
|
379
|
+
* or storage internals.
|
|
380
|
+
*
|
|
381
|
+
* Returns null when the knowledge id is not found.
|
|
382
|
+
*/
|
|
383
|
+
getMetadata?(knowledgeId: string): Promise<KnowledgeMetadata | null>;
|
|
370
384
|
}
|
|
371
385
|
interface KnowledgeResult {
|
|
372
386
|
success: boolean;
|
|
373
387
|
records?: Record<string, unknown>[];
|
|
374
388
|
error?: string;
|
|
375
389
|
}
|
|
390
|
+
/**
|
|
391
|
+
* Knowledge metadata projection returned by KnowledgeContext.getMetadata.
|
|
392
|
+
*
|
|
393
|
+
* Intentionally minimal: only the fields a handler needs to describe the
|
|
394
|
+
* knowledge to an LLM (or branch on type). Storage shape, sheet selection,
|
|
395
|
+
* record contents, etc. stay platform-internal.
|
|
396
|
+
*/
|
|
397
|
+
interface KnowledgeMetadata {
|
|
398
|
+
/** Knowledge id (echoed back so callers can correlate). */
|
|
399
|
+
id: string;
|
|
400
|
+
/** Display name. */
|
|
401
|
+
name: string;
|
|
402
|
+
/**
|
|
403
|
+
* Knowledge kind. `text` = unstructured pages searched semantically;
|
|
404
|
+
* `database` = tabular rows queried with structured filters. Other
|
|
405
|
+
* platform-specific kinds may be added; handlers should treat unknown
|
|
406
|
+
* values as opaque.
|
|
407
|
+
*/
|
|
408
|
+
type: 'text' | 'database' | string;
|
|
409
|
+
/**
|
|
410
|
+
* For database knowledge: the canonical (main-sheet) column shape that
|
|
411
|
+
* the LLM should be told about. Empty/undefined for non-database kinds.
|
|
412
|
+
*/
|
|
413
|
+
columns?: KnowledgeColumnMetadata[];
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Column metadata as exposed to handlers. Platform implementations
|
|
417
|
+
* project from their internal column model down to this shape — the LLM-
|
|
418
|
+
* facing prompt-builder only needs name, type, and any descriptive hints.
|
|
419
|
+
*/
|
|
420
|
+
interface KnowledgeColumnMetadata {
|
|
421
|
+
id: string;
|
|
422
|
+
name: string;
|
|
423
|
+
/** Column data type (e.g. 'text', 'number', 'url', 'email', 'date'). */
|
|
424
|
+
type: string;
|
|
425
|
+
description?: string;
|
|
426
|
+
/**
|
|
427
|
+
* Bag of analysis hints the platform may have produced (unique value
|
|
428
|
+
* counts, sample values, ranges). Optional and platform-defined; the
|
|
429
|
+
* SDK reserves the keys it documents and tolerates extras.
|
|
430
|
+
*/
|
|
431
|
+
metadata?: {
|
|
432
|
+
description?: string;
|
|
433
|
+
analysis?: {
|
|
434
|
+
uniqueCount?: number;
|
|
435
|
+
uniqueValues?: string[];
|
|
436
|
+
};
|
|
437
|
+
} & Record<string, unknown>;
|
|
438
|
+
}
|
|
376
439
|
interface IntegrationContext {
|
|
377
440
|
/** Execute integration */
|
|
378
441
|
execute(integrationId: string, params: Record<string, unknown>): Promise<IntegrationResult>;
|
|
@@ -651,4 +714,4 @@ interface HandoffInboundContext {
|
|
|
651
714
|
returnResult?: HandoffReturnPayload;
|
|
652
715
|
}
|
|
653
716
|
|
|
654
|
-
export type { AgentTransitionPayload as A,
|
|
717
|
+
export type { HandoffOptions as $, AgentTransitionPayload as A, RoutingDecision as B, ChatMessage as C, DeploymentInfo as D, ChatbotInfo as E, ClassifierConfig as F, FormData as G, HandoffRequestPayload as H, IntegrationContext as I, FormFieldValue as J, KnowledgeContext as K, LoggerContext as L, RoutingLog as M, SessionMetadata as N, AnalyticsContext as O, ProtocolStreamChunk as P, InteractionEvent as Q, RoutingClassificationPayload as R, SchemaDeclarationPayload as S, InteractionEventType as T, FormSchema as U, FormFieldDefinition as V, FormFieldType as W, ProtocolRegistration as X, SchemaAgent as Y, SchemaQuestion as Z, HandoffContext as _, HandoffReturnPayload as a, HandoffOfferPayload as b, ProtocolRequest as c, ProtocolResponse as d, SessionStatus as e, HandoffInboundContext as f, ProtocolForm as g, ProtocolFormField as h, ProtocolFormOption as i, ProtocolFieldValidation as j, ProtocolError as k, ProtocolHandler as l, ProtocolCapabilities as m, HandlerInfo as n, ProtocolContext as o, SessionContext as p, SessionState as q, KnowledgeResult as r, KnowledgeMetadata as s, KnowledgeColumnMetadata as t, IntegrationResult as u, HelplineSearchOptions as v, HelplineResult as w, Helpline as x, StorageContext as y, StorageSetOptions as z };
|