diff --git a/.eslintrc.js b/.eslintrc.js index ee9bf34..9c84b42 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -11,9 +11,12 @@ module.exports = { sourceType: 'module', }, rules: { - '@typescript-eslint/no-unused-vars': 'warn', + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': ['warn', { 'varsIgnorePattern': '^_', 'argsIgnorePattern': '^_' }], '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/no-explicit-any': 'warn', + 'no-redeclare': 'off', + '@typescript-eslint/no-redeclare': 'off', }, }; diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3c6cc32..775b8b2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,6 +30,5 @@ jobs: - name: Run tests run: npm run test -- --run - # TODO: enable this once all checks are set - # - name: Lint - # run: npm run lint + - name: Lint + run: npm run lint diff --git a/docs/agents_sdk_integration.md b/docs/agents_sdk_integration.md index 57fdb9c..442aae5 100644 --- a/docs/agents_sdk_integration.md +++ b/docs/agents_sdk_integration.md @@ -23,8 +23,8 @@ import { GuardrailAgent } from '@openai/guardrails'; import { Runner } from '@openai/agents'; // Create agent with guardrails automatically configured -const agent = new GuardrailAgent({ - config: { +const agent = await GuardrailAgent.create( + { version: 1, input: { version: 1, @@ -39,9 +39,9 @@ const agent = new GuardrailAgent({ ] } }, - name: "Customer support agent", - instructions: "You are a customer support agent. You help customers with their questions." -}); + "Customer support agent", + "You are a customer support agent. You help customers with their questions." +); async function main() { while (true) { @@ -79,22 +79,30 @@ GuardrailAgent supports the same configuration formats as our other clients: ```typescript // Object configuration (recommended) -const agent = new GuardrailAgent({ - config: { +const agent = await GuardrailAgent.create( + { version: 1, input: { version: 1, guardrails: [...] }, output: { version: 1, guardrails: [...] } }, - // ... other agent options -}); - -// Dynamic configuration -const configDict = { - version: 1, - input: { version: 1, guardrails: [...] }, - output: { version: 1, guardrails: [...] } -}; -const agent = new GuardrailAgent({ config: configDict, ... }); + "Agent name", + "Agent instructions" +); + +// File path configuration +const agent = await GuardrailAgent.create( + './guardrails_config.json', + "Agent name", + "Agent instructions" +); + +// With additional agent options +const agent = await GuardrailAgent.create( + configDict, + "Agent name", + "Agent instructions", + { /* additional agent options */ } +); ``` ## Next Steps diff --git a/docs/index.md b/docs/index.md index 0947036..f395be3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -14,7 +14,7 @@ ## How It Works 1. **Configure**: Use the Wizard or define pipeline configurations -2. **Replace**: Use `GuardrailsAsyncOpenAI` instead of `AsyncOpenAI` +2. **Replace**: Use `GuardrailsOpenAI` instead of `OpenAI` 3. **Validate**: Guardrails run automatically on every API call 4. **Handle**: Access results via `response.guardrail_results` diff --git a/docs/ref/checks/competitors.md b/docs/ref/checks/competitors.md index 919d21e..24ef424 100644 --- a/docs/ref/checks/competitors.md +++ b/docs/ref/checks/competitors.md @@ -6,7 +6,7 @@ Flags mentions of competitors from a configurable list. Scans text for mentions ```json { - "name": "Competitor Detection", + "name": "Competitors", "config": { "competitors": ["competitor1", "rival-company.com", "alternative-provider"] } diff --git a/docs/ref/checks/llm_base.md b/docs/ref/checks/llm_base.md index 07f255f..8d37433 100644 --- a/docs/ref/checks/llm_base.md +++ b/docs/ref/checks/llm_base.md @@ -5,8 +5,10 @@ Base configuration for LLM-based guardrails. Provides common configuration optio ## Configuration ```json +// This is a base configuration class, not a standalone guardrail +// Use one of the LLM-based guardrails instead: { - "name": "LLM Base", + "name": "NSFW Text", // or "Jailbreak", "Hallucination Detection", etc. "config": { "model": "gpt-5", "confidence_threshold": 0.7 diff --git a/docs/ref/types-typescript.md b/docs/ref/types-typescript.md index b320c33..8a5225a 100644 --- a/docs/ref/types-typescript.md +++ b/docs/ref/types-typescript.md @@ -31,7 +31,11 @@ export interface GuardrailResult { originalException?: Error; info: { checked_text: string; - [key: string]: any; + media_type?: string; + detected_content_type?: string; + stage_name?: string; + guardrail_name?: string; + [key: string]: unknown; }; } ``` @@ -41,7 +45,7 @@ Standard result returned by every guardrail check. The `executionFailed` field i ## CheckFn ```typescript -export type CheckFn = +export type CheckFn = (ctx: TContext, input: TIn, config: TCfg) => GuardrailResult | Promise; ``` @@ -52,7 +56,7 @@ Callable signature implemented by all guardrails. May be sync or async. ```typescript export type MaybeAwaitableResult = GuardrailResult | Promise; export type TContext = object; -export type TIn = unknown; +export type TIn = TextInput; export type TCfg = object; ``` diff --git a/package-lock.json b/package-lock.json index cba5e68..30e8cd2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,8 +18,8 @@ }, "devDependencies": { "@types/node": "^20.0.0", - "@typescript-eslint/eslint-plugin": "^6.0.0", - "@typescript-eslint/parser": "^6.0.0", + "@typescript-eslint/eslint-plugin": "^8.46.1", + "@typescript-eslint/parser": "^8.46.1", "@vitest/coverage-v8": "^1.6.0", "eslint": "^8.0.0", "prettier": "^3.0.0", @@ -1303,13 +1303,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/node": { "version": "20.19.10", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.10.tgz", @@ -1329,13 +1322,6 @@ "form-data": "^4.0.4" } }, - "node_modules/@types/semver": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", - "integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", @@ -1346,124 +1332,160 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", - "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.1.tgz", + "integrity": "sha512-rUsLh8PXmBjdiPY+Emjz9NX2yHvhS11v0SR6xNJkm5GM1MO9ea/1GoDKlHHZGrOJclL/cZ2i/vRUYVtjRhrHVQ==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/type-utils": "6.21.0", - "@typescript-eslint/utils": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4", + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.46.1", + "@typescript-eslint/type-utils": "8.46.1", + "@typescript-eslint/utils": "8.46.1", + "@typescript-eslint/visitor-keys": "8.46.1", "graphemer": "^1.4.0", - "ignore": "^5.2.4", + "ignore": "^7.0.0", "natural-compare": "^1.4.0", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" + "ts-api-utils": "^2.1.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "@typescript-eslint/parser": "^8.46.1", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" } }, "node_modules/@typescript-eslint/parser": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", - "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.1.tgz", + "integrity": "sha512-6JSSaBZmsKvEkbRUkf7Zj7dru/8ZCrJxAqArcLaVMee5907JdtEbKGsZ7zNiIm/UAkpGUkaSMZEXShnN2D1HZA==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", + "@typescript-eslint/scope-manager": "8.46.1", + "@typescript-eslint/types": "8.46.1", + "@typescript-eslint/typescript-estree": "8.46.1", + "@typescript-eslint/visitor-keys": "8.46.1", "debug": "^4.3.4" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.1.tgz", + "integrity": "sha512-FOIaFVMHzRskXr5J4Jp8lFVV0gz5ngv3RHmn+E4HYxSJ3DgDzU7fVI1/M7Ijh1zf6S7HIoaIOtln1H5y8V+9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.46.1", + "@typescript-eslint/types": "^8.46.1", + "debug": "^4.3.4" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", - "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.1.tgz", + "integrity": "sha512-weL9Gg3/5F0pVQKiF8eOXFZp8emqWzZsOJuWRUNtHT+UNV2xSJegmpCNQHy37aEQIbToTq7RHKhWvOsmbM680A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0" + "@typescript-eslint/types": "8.46.1", + "@typescript-eslint/visitor-keys": "8.46.1" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.1.tgz", + "integrity": "sha512-X88+J/CwFvlJB+mK09VFqx5FE4H5cXD+H/Bdza2aEWkSb8hnWIQorNcscRl4IEo1Cz9VI/+/r/jnGWkbWPx54g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, "node_modules/@typescript-eslint/type-utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", - "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.1.tgz", + "integrity": "sha512-+BlmiHIiqufBxkVnOtFwjah/vrkF4MtKKvpXrKSPLCkCtAp8H01/VV43sfqA98Od7nJpDcFnkwgyfQbOG0AMvw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/types": "8.46.1", + "@typescript-eslint/typescript-estree": "8.46.1", + "@typescript-eslint/utils": "8.46.1", "debug": "^4.3.4", - "ts-api-utils": "^1.0.1" + "ts-api-utils": "^2.1.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", - "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.1.tgz", + "integrity": "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ==", "dev": true, "license": "MIT", "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -1471,78 +1493,89 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", - "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.1.tgz", + "integrity": "sha512-uIifjT4s8cQKFQ8ZBXXyoUODtRoAd7F7+G8MKmtzj17+1UbdzFl52AzRyZRyKqPHhgzvXunnSckVu36flGy8cg==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", + "@typescript-eslint/project-service": "8.46.1", + "@typescript-eslint/tsconfig-utils": "8.46.1", + "@typescript-eslint/types": "8.46.1", + "@typescript-eslint/visitor-keys": "8.46.1", "debug": "^4.3.4", - "globby": "^11.1.0", + "fast-glob": "^3.3.2", "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", - "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.1.tgz", + "integrity": "sha512-vkYUy6LdZS7q1v/Gxb2Zs7zziuXN0wxqsetJdeZdRe/f5dwJFglmuvZBfTUivCtjH725C1jWCDfpadadD95EDQ==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "semver": "^7.5.4" + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.46.1", + "@typescript-eslint/types": "8.46.1", + "@typescript-eslint/typescript-estree": "8.46.1" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", - "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.1.tgz", + "integrity": "sha512-ptkmIf2iDkNUjdeu2bQqhFPV1m6qTnFFjg7PPDjxKWaMaP0Z6I9l30Jr3g5QqbZGdw8YdYvLp+XnqnWWZOg/NA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "6.21.0", - "eslint-visitor-keys": "^3.4.1" + "@typescript-eslint/types": "8.46.1", + "eslint-visitor-keys": "^4.2.1" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", @@ -1828,16 +1861,6 @@ "dev": true, "license": "Python-2.0" }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/assertion-error": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", @@ -2208,19 +2231,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -3091,27 +3101,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -3670,9 +3659,9 @@ } }, "node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "license": "ISC", "dependencies": { @@ -4033,16 +4022,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/pathe": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", @@ -4361,22 +4340,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/rimraf/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/rollup": { "version": "4.46.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.2.tgz", @@ -4674,16 +4637,6 @@ "dev": true, "license": "ISC" }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -4925,16 +4878,16 @@ "license": "MIT" }, "node_modules/ts-api-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", - "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=16" + "node": ">=18.12" }, "peerDependencies": { - "typescript": ">=4.2.0" + "typescript": ">=4.8.4" } }, "node_modules/type-check": { diff --git a/package.json b/package.json index 50f8392..821aae9 100644 --- a/package.json +++ b/package.json @@ -50,12 +50,12 @@ }, "devDependencies": { "@types/node": "^20.0.0", - "@typescript-eslint/eslint-plugin": "^6.0.0", - "@typescript-eslint/parser": "^6.0.0", + "@typescript-eslint/eslint-plugin": "^8.46.1", + "@typescript-eslint/parser": "^8.46.1", + "@vitest/coverage-v8": "^1.6.0", "eslint": "^8.0.0", "prettier": "^3.0.0", "rimraf": "^5.0.0", - "@vitest/coverage-v8": "^1.6.0", "typescript": "^5.0.0", "vitest": "^1.0.0" }, diff --git a/src/__tests__/integration/integration.test.ts b/src/__tests__/integration/integration.test.ts index 7c794e3..f37105c 100644 --- a/src/__tests__/integration/integration.test.ts +++ b/src/__tests__/integration/integration.test.ts @@ -10,12 +10,11 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { GuardrailRegistry } from '../../registry'; -import { GuardrailSpec } from '../../spec'; -import { CheckFn, GuardrailResult } from '../../types'; +import { CheckFn } from '../../types'; import { loadConfigBundle } from '../../runtime'; // Mock check function for testing -const mockCheck: CheckFn = (ctx, data, config) => ({ +const mockCheck: CheckFn = (ctx, data) => ({ tripwireTriggered: data === 'trigger', info: { checked_text: data, diff --git a/src/__tests__/integration/test_suite.ts b/src/__tests__/integration/test_suite.ts index 533a72d..9631abf 100644 --- a/src/__tests__/integration/test_suite.ts +++ b/src/__tests__/integration/test_suite.ts @@ -5,18 +5,14 @@ * guardrail configurations using the new GuardrailsClient design. */ -import { GuardrailsOpenAI } from '../../index.js'; - -interface Context { - guardrail_llm: any; // OpenAI client -} +import { GuardrailsOpenAI, GuardrailsResponse } from '../../index.js'; class GuardrailTest { /** Represents a complete test case for a guardrail. */ constructor( public name: string, - public config: Record, + public config: Record, public passing_cases: string[], public failing_cases: string[] ) {} @@ -319,15 +315,15 @@ interface TestResult { case: string; status: 'PASS' | 'FAIL' | 'ERROR'; expected: 'pass'; - details: any; + details: unknown; }>; failing_cases: Array<{ case: string; status: 'PASS' | 'FAIL' | 'ERROR'; expected: 'fail'; - details: any; + details: unknown; }>; - errors: any[]; + errors: unknown[]; } interface TestSuiteResults { @@ -346,8 +342,7 @@ interface TestSuiteResults { async function runTest( test: GuardrailTest, - guardrailsClient: GuardrailsOpenAI, - mediaType: string = 'text/plain' + guardrailsClient: GuardrailsOpenAI ): Promise { /** Run a single guardrail test and collect its results. */ const results: TestResult = { @@ -366,7 +361,7 @@ async function runTest( model: 'gpt-4o-mini', messages: [{ role: 'user', content: case_ }], suppressTripwire: true, - }); + } as Parameters[0]) as GuardrailsResponse; // Check if any guardrails were triggered const tripwireTriggered = response.guardrail_results.tripwiresTriggered; @@ -396,14 +391,14 @@ async function runTest( console.log(` Info: ${JSON.stringify(info)}`); } } - } catch (e: any) { + } catch (e: unknown) { results.passing_cases.push({ case: case_, status: 'ERROR', expected: 'pass', details: String(e), }); - console.log(`⚠️ ${test.name} - Passing case ${idx + 1} error: ${e}`); + console.log(`⚠️ ${test.name} - Passing case ${idx + 1} error: ${e instanceof Error ? e.message : String(e)}`); } } @@ -416,7 +411,7 @@ async function runTest( model: 'gpt-4o-mini', messages: [{ role: 'user', content: case_ }], suppressTripwire: true, - }); + } as Parameters[0]) as GuardrailsResponse; // Check if any guardrails were triggered const tripwireTriggered = response.guardrail_results.tripwiresTriggered; @@ -446,14 +441,14 @@ async function runTest( }); console.log(`❌ ${test.name} - Failing case ${idx + 1} not triggered`); } - } catch (e: any) { + } catch (e: unknown) { results.failing_cases.push({ case: case_, status: 'ERROR', expected: 'fail', details: String(e), }); - console.log(`⚠️ ${test.name} - Failing case ${idx + 1} error: ${e}`); + console.log(`⚠️ ${test.name} - Failing case ${idx + 1} error: ${e instanceof Error ? e.message : String(e)}`); } } @@ -461,8 +456,7 @@ async function runTest( } async function runTestSuite( - testFilter?: string, - mediaType: string = 'text/plain' + testFilter?: string ): Promise { /** Run all or a subset of guardrail tests and summarize results. */ const results: TestSuiteResults = { @@ -504,7 +498,7 @@ async function runTestSuite( // Initialize GuardrailsOpenAI for this test const guardrailsClient = await GuardrailsOpenAI.create(pipelineConfig); - const outcome = await runTest(test, guardrailsClient, mediaType); + const outcome = await runTest(test, guardrailsClient); results.tests.push(outcome); // Calculate test status @@ -592,7 +586,7 @@ async function main(): Promise { console.log(`Test filter: ${args.test || 'all'}`); console.log(`Media type: ${args.mediaType}`); - const results = await runTestSuite(args.test, args.mediaType); + const results = await runTestSuite(args.test); printSummary(results); diff --git a/src/__tests__/unit/agents.test.ts b/src/__tests__/unit/agents.test.ts index a4f3032..ff7cabb 100644 --- a/src/__tests__/unit/agents.test.ts +++ b/src/__tests__/unit/agents.test.ts @@ -4,6 +4,19 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { GuardrailAgent } from '../../agents'; +import { TextInput } from '../../types'; +import { z } from 'zod'; + +// Define the expected agent interface for testing +interface MockAgent { + name: string; + instructions: string; + inputGuardrails: Array<{ execute: (input: TextInput) => Promise<{ outputInfo: Record; tripwireTriggered: boolean }> }>; + outputGuardrails: Array<{ execute: (input: TextInput) => Promise<{ outputInfo: Record; tripwireTriggered: boolean }> }>; + model?: string; + temperature?: number; + max_tokens?: number; +} // Mock the @openai/agents module vi.mock('@openai/agents', () => ({ @@ -22,12 +35,20 @@ vi.mock('../../runtime', () => ({ instantiateGuardrails: vi.fn(() => Promise.resolve([ { - definition: { name: 'Keywords' }, + definition: { + name: 'Keywords', + description: 'Test guardrail', + mediaType: 'text/plain', + configSchema: z.object({}), + checkFn: vi.fn(), + contextSchema: z.object({}), + metadata: {} + }, config: {}, - run: vi.fn().mockResolvedValue({ - tripwireTriggered: false, - info: { checked_text: 'test input' }, - }), + run: vi.fn().mockResolvedValue({ + tripwireTriggered: false, + info: { checked_text: 'test input' }, + }), }, ]) ), @@ -62,7 +83,7 @@ describe('GuardrailAgent', () => { }, }; - const agent = await GuardrailAgent.create(config, 'Test Agent', 'Test instructions'); + const agent = await GuardrailAgent.create(config, 'Test Agent', 'Test instructions') as MockAgent; expect(agent.name).toBe('Test Agent'); expect(agent.instructions).toBe('Test instructions'); @@ -79,7 +100,7 @@ describe('GuardrailAgent', () => { }, }; - const agent = await GuardrailAgent.create(config, 'Test Agent', 'Test instructions'); + const agent = await GuardrailAgent.create(config, 'Test Agent', 'Test instructions') as MockAgent; expect(agent.name).toBe('Test Agent'); expect(agent.instructions).toBe('Test instructions'); @@ -104,7 +125,7 @@ describe('GuardrailAgent', () => { }, }; - const agent = await GuardrailAgent.create(config, 'Test Agent', 'Test instructions'); + const agent = await GuardrailAgent.create(config, 'Test Agent', 'Test instructions') as MockAgent; expect(agent.name).toBe('Test Agent'); expect(agent.instructions).toBe('Test instructions'); @@ -132,7 +153,7 @@ describe('GuardrailAgent', () => { 'Test Agent', 'Test instructions', agentKwargs - ); + ) as MockAgent; expect(agent.model).toBe('gpt-4'); expect(agent.temperature).toBe(0.7); @@ -142,7 +163,7 @@ describe('GuardrailAgent', () => { it('should handle empty configuration gracefully', async () => { const config = { version: 1 }; - const agent = await GuardrailAgent.create(config, 'Test Agent', 'Test instructions'); + const agent = await GuardrailAgent.create(config, 'Test Agent', 'Test instructions') as MockAgent; expect(agent.name).toBe('Test Agent'); expect(agent.instructions).toBe('Test instructions'); @@ -165,7 +186,7 @@ describe('GuardrailAgent', () => { 'Test instructions', {}, true // raiseGuardrailErrors = true - ); + ) as MockAgent; expect(agent.name).toBe('Test Agent'); expect(agent.instructions).toBe('Test instructions'); @@ -181,7 +202,7 @@ describe('GuardrailAgent', () => { }, }; - const agent = await GuardrailAgent.create(config, 'Test Agent', 'Test instructions'); + const agent = await GuardrailAgent.create(config, 'Test Agent', 'Test instructions') as MockAgent; expect(agent.name).toBe('Test Agent'); expect(agent.instructions).toBe('Test instructions'); @@ -205,13 +226,13 @@ describe('GuardrailAgent', () => { }, }; - const agent = await GuardrailAgent.create(config, 'Test Agent', 'Test instructions'); + const agent = await GuardrailAgent.create(config, 'Test Agent', 'Test instructions') as MockAgent; expect(agent.inputGuardrails).toHaveLength(1); // Test the guardrail function const guardrailFunction = agent.inputGuardrails[0]; - const result = await guardrailFunction.execute({ input: 'test input' }); + const result = await guardrailFunction.execute('test input'); expect(result).toHaveProperty('outputInfo'); expect(result).toHaveProperty('tripwireTriggered'); @@ -233,10 +254,20 @@ describe('GuardrailAgent', () => { vi.mocked(instantiateGuardrails).mockImplementationOnce(() => Promise.resolve([ { - definition: { name: 'Keywords' }, + definition: { + name: 'Keywords', + description: 'Test guardrail', + mediaType: 'text/plain', + configSchema: z.object({}), + checkFn: vi.fn(), + metadata: {}, + ctxRequirements: z.object({}), + schema: () => ({}), + instantiate: vi.fn() + }, config: {}, run: vi.fn().mockRejectedValue(new Error('Guardrail execution failed')), - } as any, + } as unknown as Parameters[0] extends Promise ? T extends readonly (infer U)[] ? U : never : never, ]) ); @@ -247,10 +278,10 @@ describe('GuardrailAgent', () => { 'Test instructions', {}, false - ); + ) as MockAgent; const guardrailFunctionDefault = agentDefault.inputGuardrails[0]; - const resultDefault = await guardrailFunctionDefault.execute({ input: 'test' }); + const resultDefault = await guardrailFunctionDefault.execute('test'); // When raiseGuardrailErrors=false, execution errors should NOT trigger tripwires // This allows execution to continue in fail-safe mode @@ -262,10 +293,20 @@ describe('GuardrailAgent', () => { vi.mocked(instantiateGuardrails).mockImplementationOnce(() => Promise.resolve([ { - definition: { name: 'Keywords' }, + definition: { + name: 'Keywords', + description: 'Test guardrail', + mediaType: 'text/plain', + configSchema: z.object({}), + checkFn: vi.fn(), + metadata: {}, + ctxRequirements: z.object({}), + schema: () => ({}), + instantiate: vi.fn() + }, config: {}, run: vi.fn().mockRejectedValue(new Error('Guardrail execution failed')), - } as any, + } as unknown as Parameters[0] extends Promise ? T extends readonly (infer U)[] ? U : never : never, ]) ); @@ -276,12 +317,12 @@ describe('GuardrailAgent', () => { 'Test instructions', {}, true - ); + ) as MockAgent; const guardrailFunctionStrict = agentStrict.inputGuardrails[0]; // When raiseGuardrailErrors=true, execution errors should be thrown - await expect(guardrailFunctionStrict.execute({ input: 'test' })).rejects.toThrow( + await expect(guardrailFunctionStrict.execute('test')).rejects.toThrow( 'Guardrail execution failed' ); }); diff --git a/src/__tests__/unit/base-client.test.ts b/src/__tests__/unit/base-client.test.ts index d157fe5..f4b1574 100644 --- a/src/__tests__/unit/base-client.test.ts +++ b/src/__tests__/unit/base-client.test.ts @@ -9,31 +9,37 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { GuardrailsBaseClient, GuardrailResultsImpl, StageGuardrails } from '../../base-client'; import { GuardrailTripwireTriggered } from '../../exceptions'; -import { GuardrailLLMContext, GuardrailResult } from '../../types'; +import { GuardrailLLMContext, GuardrailResult, TextInput, Message, TextOnlyMessageArray } from '../../types'; -interface MockGuardrail { - definition: { - name: string; - metadata?: Record; +// Removed unused interface + +interface MockPipeline { + stages: string[]; + config: Record; +} + +// Interface for response with guardrail results +interface ResponseWithGuardrailResults { + guardrail_results: { + output: GuardrailResult[]; }; - run: ReturnType; } class TestGuardrailsClient extends GuardrailsBaseClient { public setContext(ctx: GuardrailLLMContext): void { - (this as any).context = ctx; + (this as unknown as { context: GuardrailLLMContext }).context = ctx; } public setGuardrails(guardrails: StageGuardrails): void { - (this as any).guardrails = guardrails; + (this as unknown as { guardrails: StageGuardrails }).guardrails = guardrails; } - public setPipeline(pipeline: any): void { - (this as any).pipeline = pipeline; + public setPipeline(pipeline: MockPipeline): void { + (this as unknown as { pipeline: MockPipeline }).pipeline = pipeline; } protected createDefaultContext(): GuardrailLLMContext { - return { guardrailLlm: {} as any }; + return { guardrailLlm: {} as unknown as import('openai').OpenAI }; } protected overrideResources(): void { @@ -43,10 +49,17 @@ class TestGuardrailsClient extends GuardrailsBaseClient { const createGuardrail = ( name: string, - implementation: (ctx: any, text: string) => GuardrailResult | Promise -): MockGuardrail => ({ - definition: { name }, + implementation: (ctx: GuardrailLLMContext, text: TextInput) => GuardrailResult | Promise, + metadata?: Record +): unknown => ({ + definition: { + name, + mediaType: 'text/plain', // Ensure test guardrails have proper media type + metadata: metadata || {} + }, + config: {}, run: vi.fn(implementation), + ensureAsync: vi.fn(), }); describe('GuardrailsBaseClient helpers', () => { @@ -54,7 +67,7 @@ describe('GuardrailsBaseClient helpers', () => { beforeEach(() => { client = new TestGuardrailsClient(); - client.setContext({ guardrailLlm: {} } as GuardrailLLMContext); + client.setContext({ guardrailLlm: {} as unknown as import('openai').OpenAI }); client.setGuardrails({ pre_flight: [], input: [], @@ -62,43 +75,44 @@ describe('GuardrailsBaseClient helpers', () => { }); }); - describe('extractLatestUserMessage', () => { + describe('extractLatestUserTextMessage', () => { it('returns the latest user message and index for string content', () => { - const messages = [ + const messages: Message[] = [ { role: 'system', content: 'hi' }, { role: 'user', content: ' first ' }, { role: 'assistant', content: 'ok' }, { role: 'user', content: ' second ' }, ]; - const [text, index] = client.extractLatestUserMessage(messages); + const [text, index] = client.extractLatestUserTextMessage(messages); expect(text).toBe('second'); expect(index).toBe(3); }); it('handles responses API content parts', () => { - const messages = [ - { role: 'user', content: [{ type: 'text', text: 'hello' }] }, + const messages: Message[] = [ + { role: 'user', content: [{ type: 'text' as const, text: 'hello' }] }, { role: 'user', content: [ - { type: 'text', text: 'part1' }, - { type: 'text', text: 'part2' }, + { type: 'text' as const, text: 'part1' }, + { type: 'text' as const, text: 'part2' }, ], }, ]; - const [text, index] = client.extractLatestUserMessage(messages); + const [text, index] = client.extractLatestUserTextMessage(messages); expect(text).toBe('part1 part2'); expect(index).toBe(1); }); it('returns empty string when no user messages exist', () => { - const [text, index] = client.extractLatestUserMessage([ + const messages: Message[] = [ { role: 'assistant', content: 'hi' }, - ]); + ]; + const [text, index] = client.extractLatestUserTextMessage(messages); expect(text).toBe(''); expect(index).toBe(-1); }); @@ -110,6 +124,7 @@ describe('GuardrailsBaseClient helpers', () => { { tripwireTriggered: false, info: { + checked_text: 'Reach me at alice@example.com', detected_entities: { EMAIL: ['alice@example.com'], }, @@ -126,13 +141,13 @@ describe('GuardrailsBaseClient helpers', () => { }); it('masks detected PII in the latest user message with structured content', () => { - const messages = [ + const messages: Message[] = [ { role: 'assistant', content: 'hello' }, { role: 'user', content: [ - { type: 'text', text: 'Call me at 123-456-7890' }, - { type: 'text', text: 'or email alice@example.com' }, + { type: 'text' as const, text: 'Call me at 123-456-7890' }, + { type: 'text' as const, text: 'or email alice@example.com' }, ], }, ]; @@ -141,6 +156,7 @@ describe('GuardrailsBaseClient helpers', () => { { tripwireTriggered: false, info: { + checked_text: 'Call me at 123-456-7890 or email alice@example.com', detected_entities: { PHONE: ['123-456-7890'], EMAIL: ['alice@example.com'], @@ -149,11 +165,11 @@ describe('GuardrailsBaseClient helpers', () => { }, ]; - const masked = client.applyPreflightModifications(messages, results) as any[]; + const masked = client.applyPreflightModifications(messages, results) as Message[]; const [, latestMessage] = masked; - expect(latestMessage.content[0].text).toBe('Call me at '); - expect(latestMessage.content[1].text).toBe('or email '); + expect((latestMessage.content as { type: string; text: string }[])[0].text).toBe('Call me at '); + expect((latestMessage.content as { type: string; text: string }[])[1].text).toBe('or email '); // Ensure assistant message unchanged expect(masked[0]).toEqual(messages[0]); }); @@ -173,7 +189,7 @@ describe('GuardrailsBaseClient helpers', () => { beforeEach(() => { client.setGuardrails({ - pre_flight: [createGuardrail('Test Guard', async () => ({ ...baseResult }))], + pre_flight: [createGuardrail('Test Guard', async () => ({ ...baseResult, info: { ...baseResult.info, checked_text: 'payload' } })) as unknown as Parameters[0]['pre_flight'][0]], input: [], output: [], }); @@ -194,8 +210,8 @@ describe('GuardrailsBaseClient helpers', () => { pre_flight: [ createGuardrail('Tripwire', async () => ({ tripwireTriggered: true, - info: { reason: 'bad' }, - })), + info: { checked_text: 'payload', reason: 'bad' }, + })) as unknown as Parameters[0]['pre_flight'][0], ], input: [], output: [], @@ -211,8 +227,8 @@ describe('GuardrailsBaseClient helpers', () => { pre_flight: [ createGuardrail('Tripwire', async () => ({ tripwireTriggered: true, - info: { reason: 'bad' }, - })), + info: { checked_text: 'payload', reason: 'bad' }, + })) as unknown as Parameters[0]['pre_flight'][0], ], input: [], output: [], @@ -228,7 +244,7 @@ describe('GuardrailsBaseClient helpers', () => { pre_flight: [ createGuardrail('Faulty', async () => { throw new Error('boom'); - }), + }) as unknown as Parameters[0]['pre_flight'][0], ], input: [], output: [], @@ -242,14 +258,14 @@ describe('GuardrailsBaseClient helpers', () => { it('creates a conversation-aware context for prompt injection detection guardrails', async () => { const guardrail = createGuardrail('Prompt Injection Detection', async () => ({ tripwireTriggered: false, - info: {}, - })); + info: { checked_text: 'payload' }, + }), { requiresConversationHistory: true }); client.setGuardrails({ - pre_flight: [guardrail], + pre_flight: [guardrail as unknown as Parameters[0]['pre_flight'][0]], input: [], output: [], }); - const spy = vi.spyOn(client as any, 'createContextWithConversation'); + const spy = vi.spyOn(client as unknown as { createContextWithConversation: () => GuardrailLLMContext }, 'createContextWithConversation'); await client.runStageGuardrails( 'pre_flight', @@ -265,18 +281,27 @@ describe('GuardrailsBaseClient helpers', () => { describe('handleLlmResponse', () => { it('appends LLM response to conversation history and returns guardrail results', async () => { - const conversation = [{ role: 'user', content: 'hi' }]; - const outputResult = { tripwireTriggered: false, info: {} }; + const conversation: TextOnlyMessageArray = [{ role: 'user', content: 'hi' }]; + const outputResult: GuardrailResult = { tripwireTriggered: false, info: { checked_text: 'All good' } }; + interface MockLLMResponse { + choices: Array<{ + message: { + role: string; + content: string; + }; + }>; + } + const runSpy = vi - .spyOn(client as any, 'runStageGuardrails') + .spyOn(client as unknown as { runStageGuardrails: () => Promise }, 'runStageGuardrails') .mockResolvedValue([outputResult]); - const llmResponse: any = { + const llmResponse: MockLLMResponse = { choices: [{ message: { role: 'assistant', content: 'All good' } }], }; - const response = await (client as any).handleLlmResponse( - llmResponse, + const response = await (client as unknown as { handleLlmResponse: (llmResponse: unknown, inputResults: GuardrailResult[], outputResults: GuardrailResult[], conversation: TextOnlyMessageArray) => Promise }).handleLlmResponse( + llmResponse as unknown, [], [], conversation @@ -291,8 +316,8 @@ describe('GuardrailsBaseClient helpers', () => { ]), false ); - expect(response.guardrail_results).toBeInstanceOf(GuardrailResultsImpl); - expect(response.guardrail_results.output).toEqual([outputResult]); + expect((response as unknown as ResponseWithGuardrailResults).guardrail_results).toBeInstanceOf(GuardrailResultsImpl); + expect((response as unknown as ResponseWithGuardrailResults).guardrail_results.output).toEqual([outputResult]); }); }); }); diff --git a/src/__tests__/unit/chat-resources.test.ts b/src/__tests__/unit/chat-resources.test.ts index f866baf..f8f8b15 100644 --- a/src/__tests__/unit/chat-resources.test.ts +++ b/src/__tests__/unit/chat-resources.test.ts @@ -9,29 +9,21 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; const streamSyncMock = vi.fn(); -vi.mock( - '../../streaming', - () => ({ - StreamingMixin: { - streamWithGuardrailsSync: streamSyncMock, - }, - }), - { virtual: true } -); - -vi.mock( - '../../streaming.js', - () => ({ - StreamingMixin: { - streamWithGuardrailsSync: streamSyncMock, - }, - }), - { virtual: true } -); +vi.mock('../../streaming', () => ({ + StreamingMixin: { + streamWithGuardrailsSync: streamSyncMock, + }, +})); + +vi.mock('../../streaming.js', () => ({ + StreamingMixin: { + streamWithGuardrailsSync: streamSyncMock, + }, +})); const baseClientMock = () => { return { - extractLatestUserMessage: vi.fn().mockReturnValue(['latest user', 1]), + extractLatestUserTextMessage: vi.fn().mockReturnValue(['latest user', 1]), runStageGuardrails: vi.fn(), applyPreflightModifications: vi.fn((payload) => payload), handleLlmResponse: vi.fn().mockResolvedValue({ result: 'handled' }), @@ -65,7 +57,7 @@ describe('Chat resource', () => { it('runs guardrail stages and delegates non-streaming responses to handler', async () => { const { Chat } = await import('../../resources/chat/chat'); - const chat = new Chat(client as any); + const chat = new Chat(client as unknown as ConstructorParameters[0]); const messages = [{ role: 'user', content: 'hello' }]; const result = await chat.completions.create({ @@ -73,7 +65,7 @@ describe('Chat resource', () => { model: 'gpt-4', }); - expect(client.extractLatestUserMessage).toHaveBeenCalledWith(messages); + expect(client.extractLatestUserTextMessage).toHaveBeenCalledWith(messages); expect(client.runStageGuardrails).toHaveBeenNthCalledWith( 1, 'pre_flight', @@ -125,14 +117,14 @@ describe('Responses resource', () => { .mockResolvedValueOnce([{ stage: 'preflight' }]) .mockResolvedValueOnce([{ stage: 'input' }]); - const responses = new Responses(client as any); + const responses = new Responses(client as unknown as ConstructorParameters[0]); const payload = await responses.create({ input: 'Tell me something', model: 'gpt-4o', }); - expect(client.extractLatestUserMessage).not.toHaveBeenCalled(); // string input path + expect(client.extractLatestUserTextMessage).not.toHaveBeenCalled(); // string input path expect(client.runStageGuardrails).toHaveBeenNthCalledWith( 1, 'pre_flight', diff --git a/src/__tests__/unit/checks/jailbreak.test.ts b/src/__tests__/unit/checks/jailbreak.test.ts index 00d0534..cf3c0f0 100644 --- a/src/__tests__/unit/checks/jailbreak.test.ts +++ b/src/__tests__/unit/checks/jailbreak.test.ts @@ -2,7 +2,7 @@ * Ensures jailbreak guardrail delegates to createLLMCheckFn with correct metadata. */ -import { describe, it, expect, vi } from 'vitest'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; const createLLMCheckFnMock = vi.fn(() => 'mocked-guardrail'); const registerMock = vi.fn(); diff --git a/src/__tests__/unit/checks/keywords-urls.test.ts b/src/__tests__/unit/checks/keywords-urls.test.ts index 7b4545b..8f5cfa5 100644 --- a/src/__tests__/unit/checks/keywords-urls.test.ts +++ b/src/__tests__/unit/checks/keywords-urls.test.ts @@ -6,6 +6,7 @@ import { describe, it, expect } from 'vitest'; import { keywordsCheck, KeywordsConfig } from '../../../checks/keywords'; import { urls } from '../../../checks/urls'; import { competitorsCheck } from '../../../checks/competitors'; +import { GuardrailResult } from '../../../types'; describe('keywords guardrail', () => { it('detects keywords with trailing punctuation removed', () => { @@ -13,7 +14,7 @@ describe('keywords guardrail', () => { {}, 'Please keep this secret!', KeywordsConfig.parse({ keywords: ['secret!!!'] }) - ); + ) as GuardrailResult; expect(result.tripwireTriggered).toBe(true); expect(result.info?.matchedKeywords).toEqual(['secret']); @@ -26,7 +27,7 @@ describe('keywords guardrail', () => { {}, 'All clear content', KeywordsConfig.parse({ keywords: ['secret'] }) - ); + ) as GuardrailResult; expect(result.tripwireTriggered).toBe(false); expect(result.info?.matchedKeywords).toEqual([]); @@ -71,8 +72,8 @@ describe('urls guardrail', () => { 'https://user:pass@secure.example.com', 'javascript:alert(1)', ]); - expect(result.info?.blocked_reasons?.some((reason: string) => reason.includes('Blocked scheme: http'))).toBe(true); - expect(result.info?.blocked_reasons?.some((reason: string) => reason.includes('Contains userinfo'))).toBe(true); + expect((result.info?.blocked_reasons as string[])?.some((reason: string) => reason.includes('Blocked scheme: http'))).toBe(true); + expect((result.info?.blocked_reasons as string[])?.some((reason: string) => reason.includes('Contains userinfo'))).toBe(true); }); it('honours subdomain allowance settings', async () => { @@ -94,12 +95,12 @@ describe('urls guardrail', () => { }); describe('competitors guardrail', () => { - it('reuses keywords check and annotates guardrail name', async () => { - const result = await competitorsCheck( + it('reuses keywords check and annotates guardrail name', () => { + const result = competitorsCheck( {}, 'We prefer Acme Corp over others.', { keywords: ['acme corp'] } - ); + ) as GuardrailResult; expect(result.tripwireTriggered).toBe(true); expect(result.info?.guardrail_name).toBe('Competitors'); diff --git a/src/__tests__/unit/checks/pii.test.ts b/src/__tests__/unit/checks/pii.test.ts index b9fe687..198d662 100644 --- a/src/__tests__/unit/checks/pii.test.ts +++ b/src/__tests__/unit/checks/pii.test.ts @@ -16,8 +16,8 @@ describe('pii guardrail', () => { const result = await pii({}, text, config); expect(result.tripwireTriggered).toBe(false); - expect(result.info?.detected_entities?.EMAIL_ADDRESS).toEqual(['john@example.com']); - expect(result.info?.detected_entities?.US_SSN).toEqual(['111-22-3333']); + expect((result.info?.detected_entities as Record)?.EMAIL_ADDRESS).toEqual(['john@example.com']); + expect((result.info?.detected_entities as Record)?.US_SSN).toEqual(['111-22-3333']); expect(result.info?.checked_text).toBe('Contact SSN: '); }); @@ -30,7 +30,7 @@ describe('pii guardrail', () => { const result = await pii({}, 'Call me at (415) 123-4567', config); expect(result.tripwireTriggered).toBe(true); - expect(result.info?.detected_entities?.PHONE_NUMBER?.[0]).toContain('415'); + expect((result.info?.detected_entities as Record)?.PHONE_NUMBER?.[0]).toContain('415'); expect(result.info?.checked_text).toContain(''); }); diff --git a/src/__tests__/unit/checks/topical-alignment.test.ts b/src/__tests__/unit/checks/topical-alignment.test.ts index 30a5ff1..1324dca 100644 --- a/src/__tests__/unit/checks/topical-alignment.test.ts +++ b/src/__tests__/unit/checks/topical-alignment.test.ts @@ -22,13 +22,27 @@ describe('topicalAlignmentCheck', () => { buildFullPromptMock.mockClear(); }); - const config = { + interface TopicalAlignmentConfig { + model: string; + confidence_threshold: number; + system_prompt_details: string; + } + + const config: TopicalAlignmentConfig = { model: 'gpt-topic', confidence_threshold: 0.6, system_prompt_details: 'Stay on topic about finance.', }; - const makeCtx = (response: any) => { + interface MockLLMResponse { + choices: Array<{ + message: { + content: string; + }; + }>; + } + + const makeCtx = (response: MockLLMResponse) => { const create = vi.fn().mockResolvedValue(response); return { ctx: { @@ -56,7 +70,7 @@ describe('topicalAlignmentCheck', () => { ], }); - const result = await topicalAlignmentCheck(ctx, 'Discussing sports', config as any); + const result = await topicalAlignmentCheck(ctx, 'Discussing sports', config); expect(buildFullPromptMock).toHaveBeenCalled(); expect(create).toHaveBeenCalledWith({ @@ -76,11 +90,11 @@ describe('topicalAlignmentCheck', () => { it('returns failure info when no content is returned', async () => { const { topicalAlignmentCheck } = await import('../../../checks/topical-alignment'); const { ctx } = makeCtx({ - choices: [{ message: {} }], + choices: [{ message: { content: '' } }], }); const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - const result = await topicalAlignmentCheck(ctx, 'Hi', config as any); + const result = await topicalAlignmentCheck(ctx, 'Hi', config); consoleSpy.mockRestore(); @@ -101,7 +115,17 @@ describe('topicalAlignmentCheck', () => { }, }; - const result = await topicalAlignmentCheck(ctx as any, 'Test', config as any); + interface MockContext { + guardrailLlm: { + chat: { + completions: { + create: ReturnType; + }; + }; + }; + } + + const result = await topicalAlignmentCheck(ctx as MockContext, 'Test', config); expect(result.tripwireTriggered).toBe(false); expect(result.info?.error).toContain('timeout'); diff --git a/src/__tests__/unit/checks/user-defined-llm.test.ts b/src/__tests__/unit/checks/user-defined-llm.test.ts index 649930c..fd9291f 100644 --- a/src/__tests__/unit/checks/user-defined-llm.test.ts +++ b/src/__tests__/unit/checks/user-defined-llm.test.ts @@ -61,7 +61,15 @@ describe('userDefinedLLMCheck', () => { it('falls back to text parsing when response_format is unsupported', async () => { const { ctx, create } = makeCtx(); - const errorObj = new Error('format not supported') as any; + interface OpenAIError extends Error { + error: { + param: string; + code?: string; + message?: string; + }; + } + + const errorObj = new Error('format not supported') as OpenAIError; errorObj.error = { param: 'response_format' }; create.mockRejectedValueOnce(errorObj); create.mockResolvedValueOnce({ diff --git a/src/__tests__/unit/cli.test.ts b/src/__tests__/unit/cli.test.ts index 1337020..739e2e0 100644 --- a/src/__tests__/unit/cli.test.ts +++ b/src/__tests__/unit/cli.test.ts @@ -44,12 +44,12 @@ describe('CLI main', () => { runEvaluationCLI.mockReset(); exitCalls = []; - exitSpy = vi.spyOn(process, 'exit').mockImplementation(((code?: number) => { - exitCalls.push(code ?? 0); + exitSpy = vi.spyOn(process, 'exit').mockImplementation((code?: string | number | null | undefined) => { + exitCalls.push(typeof code === 'number' ? code : 0); return undefined as never; - }) as any); - logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); - errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + }) as unknown as ReturnType; + logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) as unknown as ReturnType; + errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) as unknown as ReturnType; await importMain(); }); diff --git a/src/__tests__/unit/client.test.ts b/src/__tests__/unit/client.test.ts index c62cdff..06ffbf8 100644 --- a/src/__tests__/unit/client.test.ts +++ b/src/__tests__/unit/client.test.ts @@ -4,13 +4,45 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; -const openAiInstances: any[] = []; +// Mock interfaces for better type safety +interface MockOpenAIOptions { + apiKey?: string; + baseURL?: string; + organization?: string; + timeout?: number; + maxRetries?: number; +} + +interface MockGuardrailsClient { + raiseGuardrailErrors: boolean; + _resourceClient: MockOpenAI; + context: { + guardrailLlm: MockOpenAI; + }; +} + +// Interface for accessing private properties in tests +interface TestGuardrailsOpenAI { + guardrailsClient: MockGuardrailsClient; + overrideResources(): void; + guardrails: { + chat: { client: MockGuardrailsClient }; + responses: { client: MockGuardrailsClient }; + }; +} + +// Interface for accessing private chat client property +interface TestChat { + client: MockGuardrailsClient; +} + +const openAiInstances: MockOpenAI[] = []; class MockOpenAI { public chat = { completions: { create: vi.fn() } }; public responses = { create: vi.fn() }; - constructor(public options: any = {}) { + constructor(public options: MockOpenAIOptions = {}) { this.apiKey = options.apiKey; this.baseURL = options.baseURL; this.organization = options.organization; @@ -39,10 +71,10 @@ vi.mock('../../runtime', () => ({ })); class MockChat { - constructor(public client: any) {} + constructor(public client: MockGuardrailsClient) {} } class MockResponses { - constructor(public client: any) {} + constructor(public client: MockGuardrailsClient) {} } describe('Guardrails clients', () => { @@ -55,7 +87,7 @@ describe('Guardrails clients', () => { const { GuardrailsOpenAI } = await import('../../client'); const overrideSpy = vi - .spyOn(GuardrailsOpenAI.prototype as any, 'overrideResources') + .spyOn(GuardrailsOpenAI.prototype as unknown as TestGuardrailsOpenAI, 'overrideResources') .mockImplementation(function () { const client = this.guardrailsClient; Object.defineProperty(this, 'chat', { @@ -75,9 +107,9 @@ describe('Guardrails clients', () => { }); }); - const instance = await GuardrailsOpenAI.create({ input: {} }, { apiKey: 'key-123', timeout: 5000 }, true); + const instance = await GuardrailsOpenAI.create({ input: { guardrails: [] } }, { apiKey: 'key-123', timeout: 5000 }, true); - const guardrailsClient = (instance as any).guardrailsClient ?? instance.guardrails.chat.client; + const guardrailsClient = (instance as unknown as TestGuardrailsOpenAI).guardrailsClient ?? (instance.guardrails.chat as unknown as TestChat).client as MockGuardrailsClient; expect(guardrailsClient.raiseGuardrailErrors).toBe(true); const resourceClient = guardrailsClient._resourceClient; @@ -92,7 +124,7 @@ describe('Guardrails clients', () => { expect(instance.guardrails.chat).toBeInstanceOf(MockChat); expect(instance.guardrails.responses).toBeInstanceOf(MockResponses); - expect(instance.guardrails.chat.client).toBe(guardrailsClient); + expect((instance.guardrails.chat as unknown as TestChat).client).toBe(guardrailsClient); expect(instance.chat).toBeInstanceOf(MockChat); overrideSpy.mockRestore(); @@ -102,7 +134,7 @@ describe('Guardrails clients', () => { const { GuardrailsAzureOpenAI } = await import('../../client'); const overrideSpy = vi - .spyOn(GuardrailsAzureOpenAI.prototype as any, 'overrideResources') + .spyOn(GuardrailsAzureOpenAI.prototype as unknown as TestGuardrailsOpenAI, 'overrideResources') .mockImplementation(function () { const client = this.guardrailsClient; Object.defineProperty(this, 'chat', { @@ -122,9 +154,9 @@ describe('Guardrails clients', () => { }); }); - const instance = await GuardrailsAzureOpenAI.create({ output: {} }, { apiKey: 'azure-key' }, false); + const instance = await GuardrailsAzureOpenAI.create({ output: { guardrails: [] } }, { apiKey: 'azure-key' }, false); - const guardrailsClient = (instance as any).guardrailsClient ?? instance.guardrails.chat.client; + const guardrailsClient = (instance as unknown as TestGuardrailsOpenAI).guardrailsClient ?? (instance.guardrails.chat as unknown as TestChat).client as MockGuardrailsClient; expect(guardrailsClient.raiseGuardrailErrors).toBe(false); const resourceClient = guardrailsClient._resourceClient; diff --git a/src/__tests__/unit/evals.test.ts b/src/__tests__/unit/evals.test.ts index 0c7fd8e..62f16c7 100644 --- a/src/__tests__/unit/evals.test.ts +++ b/src/__tests__/unit/evals.test.ts @@ -10,7 +10,10 @@ import { describe, it, expect, vi } from 'vitest'; import { GuardrailMetricsCalculator, validateDataset, JsonResultsReporter } from '../../evals'; -import { Sample, SampleResult, GuardrailMetrics } from '../../evals/core/types'; +import { SampleResult, GuardrailMetrics } from '../../evals/core/types'; +import { Stats } from 'fs'; + +// Using type assertion for fs.Stats mock due to complex union type requirements // Mock file system operations vi.mock('fs/promises', () => ({ @@ -101,7 +104,11 @@ describe('Evaluation Framework', () => { describe('validateDataset', () => { it('should validate valid dataset', async () => { const mockFs = await import('fs/promises'); - vi.mocked(mockFs.stat).mockResolvedValue({} as any); + vi.mocked(mockFs.stat).mockResolvedValue({ + isFile: () => true, + isDirectory: () => false, + size: 1024 + } as unknown as Stats); vi.mocked(mockFs.readFile).mockResolvedValue( '{"id":"1","data":"Sample 1","expectedTriggers":{"test":true}}\n{"id":"2","data":"Sample 2","expectedTriggers":{"test":false}}' ); @@ -114,7 +121,11 @@ describe('Evaluation Framework', () => { it('should validate dataset with snake_case field names', async () => { const mockFs = await import('fs/promises'); - vi.mocked(mockFs.stat).mockResolvedValue({} as any); + vi.mocked(mockFs.stat).mockResolvedValue({ + isFile: () => true, + isDirectory: () => false, + size: 1024 + } as unknown as Stats); vi.mocked(mockFs.readFile).mockResolvedValue( '{"id":"1","data":"Sample 1","expected_triggers":{"test":true}}\n{"id":"2","data":"Sample 2","expected_triggers":{"test":false}}' ); @@ -127,7 +138,11 @@ describe('Evaluation Framework', () => { it('should validate dataset with mixed field naming conventions', async () => { const mockFs = await import('fs/promises'); - vi.mocked(mockFs.stat).mockResolvedValue({} as any); + vi.mocked(mockFs.stat).mockResolvedValue({ + isFile: () => true, + isDirectory: () => false, + size: 1024 + } as unknown as Stats); vi.mocked(mockFs.readFile).mockResolvedValue( '{"id":"1","data":"Sample 1","expectedTriggers":{"test":true}}\n{"id":"2","data":"Sample 2","expected_triggers":{"test":false}}' ); @@ -140,7 +155,11 @@ describe('Evaluation Framework', () => { it('should detect invalid dataset structure', async () => { const mockFs = await import('fs/promises'); - vi.mocked(mockFs.stat).mockResolvedValue({} as any); + vi.mocked(mockFs.stat).mockResolvedValue({ + isFile: () => true, + isDirectory: () => false, + size: 1024 + } as unknown as Stats); vi.mocked(mockFs.readFile).mockResolvedValue( '{"id":"1","data":"Sample 1"}\n{"id":"2","expectedTriggers":{"test":false}}' ); @@ -152,7 +171,11 @@ describe('Evaluation Framework', () => { it('should handle malformed JSON', async () => { const mockFs = await import('fs/promises'); - vi.mocked(mockFs.stat).mockResolvedValue({} as any); + vi.mocked(mockFs.stat).mockResolvedValue({ + isFile: () => true, + isDirectory: () => false, + size: 1024 + } as unknown as Stats); vi.mocked(mockFs.readFile).mockResolvedValue( 'invalid json\n{"id":"1","data":"Sample 1","expectedTriggers":{"test":true}}' ); diff --git a/src/__tests__/unit/llm-base.test.ts b/src/__tests__/unit/llm-base.test.ts index 56a931c..be54d36 100644 --- a/src/__tests__/unit/llm-base.test.ts +++ b/src/__tests__/unit/llm-base.test.ts @@ -1,6 +1,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { LLMConfig, LLMOutput, createLLMCheckFn } from '../../checks/llm-base'; import { defaultSpecRegistry } from '../../registry'; +import { GuardrailLLMContext } from '../../types'; // Mock the registry vi.mock('../../registry', () => ({ @@ -123,7 +124,7 @@ describe('LLM Base', () => { }, }; - const result = await guardrail(mockContext as any, 'test text', { + const result = await guardrail(mockContext as unknown as GuardrailLLMContext, 'test text', { model: 'gpt-4', confidence_threshold: 0.7, }); @@ -160,7 +161,7 @@ describe('LLM Base', () => { }, }; - const result = await guardrail(mockContext as any, 'test text', { + const result = await guardrail(mockContext as unknown as GuardrailLLMContext, 'test text', { model: 'gpt-4', confidence_threshold: 0.7, }); @@ -196,7 +197,7 @@ describe('LLM Base', () => { }, }; - const result = await guardrail(mockContext as any, 'test text', { + const result = await guardrail(mockContext as unknown as GuardrailLLMContext, 'test text', { model: 'gpt-4', confidence_threshold: 0.7, }); diff --git a/src/__tests__/unit/prompt_injection_detection.test.ts b/src/__tests__/unit/prompt_injection_detection.test.ts index ecfae67..904e3b6 100644 --- a/src/__tests__/unit/prompt_injection_detection.test.ts +++ b/src/__tests__/unit/prompt_injection_detection.test.ts @@ -3,6 +3,7 @@ */ import { describe, it, expect, beforeEach } from 'vitest'; +import { OpenAI } from 'openai'; import { promptInjectionDetectionCheck, PromptInjectionDetectionConfig, @@ -41,7 +42,7 @@ describe('Prompt Injection Detection Check', () => { }; mockContext = { - guardrailLlm: mockOpenAI as any, + guardrailLlm: mockOpenAI as unknown as OpenAI, getConversationHistory: () => [ { role: 'user', content: 'What is the weather in Tokyo?' }, { role: 'assistant', content: 'I will check the weather for you.' }, diff --git a/src/__tests__/unit/registry.test.ts b/src/__tests__/unit/registry.test.ts index 0e28075..50ce10f 100644 --- a/src/__tests__/unit/registry.test.ts +++ b/src/__tests__/unit/registry.test.ts @@ -10,12 +10,35 @@ */ import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { z } from 'zod'; import { GuardrailRegistry } from '../../registry'; import { GuardrailSpec, GuardrailSpecMetadata } from '../../spec'; -import { CheckFn, GuardrailResult } from '../../types'; +import { CheckFn } from '../../types'; + +// Mock interfaces for better type safety +interface MockTestContext { + testProperty: string; +} + +interface MockTestConfig { + threshold: number; + enabled: boolean; +} + +// Zod schemas for testing +const TestConfigSchema = z.object({ + threshold: z.number(), + enabled: z.boolean(), +}); + +const TestContextSchema = z.object({ + testProperty: z.string(), +}); + +// Removed unused schemas // Mock check function for testing -const mockCheck: CheckFn = vi.fn().mockImplementation((ctx, data, config) => ({ +const mockCheck: CheckFn = vi.fn().mockImplementation(() => ({ tripwireTriggered: false, })); @@ -49,8 +72,8 @@ describe('Registry Module', () => { const specs = registry.all(); expect(specs).toHaveLength(2); - expect(specs.map((s: any) => s.name)).toContain('guard1'); - expect(specs.map((s: any) => s.name)).toContain('guard2'); + expect(specs.map((s: GuardrailSpec) => s.name)).toContain('guard1'); + expect(specs.map((s: GuardrailSpec) => s.name)).toContain('guard2'); }); it('should handle guardrail with metadata', () => { @@ -75,32 +98,20 @@ describe('Registry Module', () => { }); it('should handle guardrail with config schema', () => { - const configSchema = { - type: 'object', - properties: { - threshold: { type: 'number' }, - }, - }; registry.register( 'schema_guard', mockCheck, 'Guard with schema', 'text/plain', - configSchema as any + TestConfigSchema ); const spec = registry.get('schema_guard'); - expect(spec?.configSchema).toEqual(configSchema); + expect(spec?.configSchema).toBe(TestConfigSchema); }); it('should handle guardrail with context requirements', () => { - const contextRequirements = { - type: 'object', - properties: { - user: { type: 'string' }, - }, - }; registry.register( 'context_guard', @@ -108,11 +119,11 @@ describe('Registry Module', () => { 'Guard with context', 'text/plain', undefined, - contextRequirements as any + TestContextSchema ); const spec = registry.get('context_guard'); - expect(spec?.ctxRequirements).toEqual(contextRequirements); + expect(spec?.ctxRequirements).toBe(TestContextSchema); }); it('should allow overwriting existing guardrails', () => { @@ -148,9 +159,9 @@ describe('Registry Module', () => { 'full_spec', 'Full specification', 'text/plain', - { type: 'object' } as any, + TestConfigSchema, mockCheck, - { type: 'object' } as any, + TestContextSchema, metadata ); @@ -166,14 +177,14 @@ describe('Registry Module', () => { 'test_spec', 'Test specification', 'text/plain', - { type: 'object' } as any, + TestConfigSchema, mockCheck, - { type: 'object' } as any + TestContextSchema ); - const guardrail = spec.instantiate({ threshold: 5 }); + const guardrail = spec.instantiate({ threshold: 5, enabled: true }); expect(guardrail.definition).toBe(spec); - expect(guardrail.config).toEqual({ threshold: 5 }); + expect(guardrail.config).toEqual({ threshold: 5, enabled: true }); }); it('should run instantiated guardrail', async () => { @@ -181,16 +192,16 @@ describe('Registry Module', () => { 'test_spec', 'Test specification', 'text/plain', - { type: 'object' } as any, + TestConfigSchema, mockCheck, - { type: 'object' } as any + TestContextSchema ); - const guardrail = spec.instantiate({ threshold: 5 }); - const result = await guardrail.run({}, 'Hello world'); + const guardrail = spec.instantiate({ threshold: 5, enabled: true }); + const result = await guardrail.run({ testProperty: 'test' }, 'Hello world'); expect(result.tripwireTriggered).toBe(false); - expect(mockCheck).toHaveBeenCalledWith({}, 'Hello world', { threshold: 5 }); + expect(mockCheck).toHaveBeenCalledWith({ testProperty: 'test' }, 'Hello world', { threshold: 5, enabled: true }); }); }); }); diff --git a/src/__tests__/unit/runtime.test.ts b/src/__tests__/unit/runtime.test.ts index 501d75b..7eacebb 100644 --- a/src/__tests__/unit/runtime.test.ts +++ b/src/__tests__/unit/runtime.test.ts @@ -113,7 +113,7 @@ describe('Runtime Module', () => { shouldTrip: z.boolean().optional(), }); - let guardrailCheck: CheckFn; + let guardrailCheck: CheckFn; beforeEach(() => { guardrailCheck = vi.fn().mockImplementation((_ctx, data, cfg) => ({ @@ -130,7 +130,7 @@ describe('Runtime Module', () => { 'Runtime test guard', 'text/plain', configSchema, - z.any(), + z.object({}), { name: 'Runtime Test Guard' } ); }); @@ -182,7 +182,7 @@ describe('Runtime Module', () => { 'Runtime test guard', 'text/plain', configSchema, - z.any(), + z.object({}), { name: 'Runtime Test Guard' } ); @@ -207,7 +207,7 @@ describe('Runtime Module', () => { 'Runtime test guard', 'text/plain', configSchema, - z.any(), + z.object({}), { name: 'Runtime Test Guard' } ); @@ -229,7 +229,7 @@ describe('Runtime Module', () => { 'Runtime test guard', 'text/plain', configSchema, - z.any(), + z.object({}), { name: 'Runtime Test Guard' } ); @@ -241,10 +241,11 @@ describe('Runtime Module', () => { try { await checkPlainText('payload', bundle, context); - } catch (error: any) { - expect(Array.isArray(error.guardrailResults)).toBe(true); - expect(error.guardrailResults).toHaveLength(1); - expect(error.guardrailResults[0].info?.reason).toBe('bad'); + } catch (error: unknown) { + const err = error as { guardrailResults: unknown[] }; + expect(Array.isArray(err.guardrailResults)).toBe(true); + expect(err.guardrailResults).toHaveLength(1); + expect((err.guardrailResults[0] as { info?: { reason: string } }).info?.reason).toBe('bad'); } }); diff --git a/src/__tests__/unit/schema-utils.test.ts b/src/__tests__/unit/schema-utils.test.ts index 7a9bd21..74e24ce 100644 --- a/src/__tests__/unit/schema-utils.test.ts +++ b/src/__tests__/unit/schema-utils.test.ts @@ -67,10 +67,10 @@ describe('schema utilities', () => { const result = ensureStrictJsonSchema(schema); expect(result.additionalProperties).toBe(false); - const profile = (result.properties as any).profile; + const profile = (result.properties as Record).profile as Record; expect(profile.additionalProperties).toBe(false); - const tagItems = (result.properties as any).tags.items; - expect(tagItems.additionalProperties).toBe(false); + const tagItems = (result.properties as Record).tags as Record; + expect((tagItems as Record).items as Record).toHaveProperty('additionalProperties', false); }); }); @@ -94,7 +94,7 @@ describe('schema utilities', () => { }; it('resolves local $ref pointers', () => { - const resolved = resolveRef(rootSchema.properties.address as any, rootSchema); + const resolved = resolveRef(rootSchema.properties.address as Record, rootSchema); expect(resolved).toMatchObject({ type: 'object', properties: { @@ -123,13 +123,13 @@ describe('schema utilities', () => { ...schema, }); - const homeSchema = (resolved.properties as any).addresses.items.properties.home; - expect(homeSchema.properties.street.type).toBe('string'); + const homeSchema = ((resolved.properties as Record).addresses as Record).items as Record; + expect(((homeSchema.properties as Record).home as Record).properties as Record).toHaveProperty('street', { type: 'string' }); }); it('throws when resolving an invalid ref path', () => { expect(() => - resolveRef({ $ref: '#/definitions/Missing' } as any, rootSchema) + resolveRef({ $ref: '#/definitions/Missing' } as Record, rootSchema) ).toThrow('Invalid $ref path'); }); }); diff --git a/src/__tests__/unit/spec.test.ts b/src/__tests__/unit/spec.test.ts index 8c9c486..c9a0d69 100644 --- a/src/__tests__/unit/spec.test.ts +++ b/src/__tests__/unit/spec.test.ts @@ -8,13 +8,13 @@ * - Validation */ -import { describe, it, expect, beforeEach } from 'vitest'; +import { describe, it, expect } from 'vitest'; import { GuardrailSpec, GuardrailSpecMetadata } from '../../spec'; -import { CheckFn } from '../../types'; +import { CheckFn, TextInput } from '../../types'; import { z } from 'zod'; // Mock check function for testing -const mockCheck: CheckFn = (ctx, data, config) => ({ +const mockCheck: CheckFn = (ctx, data) => ({ tripwireTriggered: false, info: { checked_text: data, @@ -166,8 +166,8 @@ describe('Spec Module', () => { }; expect(metadata.engine).toBe('regex'); - expect((metadata as any).custom).toBe(123); - expect((metadata as any).version).toBe('1.0.0'); + expect((metadata as Record).custom).toBe(123); + expect((metadata as Record).version).toBe('1.0.0'); }); it('should handle empty metadata', () => { @@ -216,7 +216,7 @@ describe('Spec Module', () => { const complexContext = z.object({ user: z.string(), permissions: z.array(z.string()), - settings: z.record(z.any()), + settings: z.record(z.unknown()), }); const spec = new GuardrailSpec( @@ -291,7 +291,7 @@ describe('Spec Module', () => { 'Test description', 'text/plain', z.object({}), - undefined as any, + undefined as unknown as CheckFn, z.object({}) ) ).not.toThrow(); diff --git a/src/__tests__/unit/streaming.test.ts b/src/__tests__/unit/streaming.test.ts index 0c76eb6..2bc8754 100644 --- a/src/__tests__/unit/streaming.test.ts +++ b/src/__tests__/unit/streaming.test.ts @@ -8,7 +8,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { StreamingMixin } from '../../streaming'; import { GuardrailTripwireTriggered } from '../../exceptions'; -import { GuardrailsBaseClient, GuardrailResultsImpl } from '../../base-client'; +import { GuardrailsBaseClient, GuardrailResultsImpl, GuardrailsResponse } from '../../base-client'; import { GuardrailResult } from '../../types'; type MockClient = GuardrailsBaseClient & { @@ -21,8 +21,6 @@ const makeChunk = (text: string) => ({ choices: [{ delta: { content: text } }], }); -const guardrailResults = (output: GuardrailResult[] = []) => - new GuardrailResultsImpl([], [], output); async function collectAsyncIterator(iterator: AsyncIterableIterator): Promise { const results: T[] = []; @@ -77,7 +75,7 @@ describe('StreamingMixin', () => { expect(periodicCall[0]).toBe('output'); expect(periodicCall[1]).toBe('hi there'); - const finalResponse = responses[responses.length - 1]; + const finalResponse = responses[responses.length - 1] as GuardrailsResponse; expect(finalResponse.guardrail_results.output).toHaveLength(0); }); @@ -101,14 +99,14 @@ describe('StreamingMixin', () => { const responses = await collectAsyncIterator(iterator); expect(responses).toHaveLength(2); - const finalResponse = responses[1]; + const finalResponse = responses[1] as GuardrailsResponse; expect(finalResponse.guardrail_results.output).toHaveLength(1); }); it('propagates tripwire errors during periodic checks but yields final response', async () => { const tripwire = new GuardrailTripwireTriggered({ tripwireTriggered: true, - info: { guardrail_name: 'Test' }, + info: { guardrail_name: 'Test', checked_text: 'test input' }, }); client.runStageGuardrails.mockImplementationOnce(async () => { @@ -129,7 +127,7 @@ describe('StreamingMixin', () => { false ); - const results: any[] = []; + const results: GuardrailsResponse[] = []; await expect(async () => { for await (const value of iterator) { results.push(value); diff --git a/src/__tests__/unit/types.test.ts b/src/__tests__/unit/types.test.ts index 87bee10..5ce9d30 100644 --- a/src/__tests__/unit/types.test.ts +++ b/src/__tests__/unit/types.test.ts @@ -8,8 +8,9 @@ * - Type compatibility */ -import { describe, it, expect, beforeEach } from 'vitest'; -import { GuardrailResult, CheckFn, GuardrailLLMContext } from '../../types'; +import { describe, it, expect } from 'vitest'; +import { GuardrailResult, GuardrailLLMContext } from '../../types'; +import { OpenAI } from 'openai'; describe('Types Module', () => { describe('GuardrailResult', () => { @@ -53,26 +54,26 @@ describe('Types Module', () => { describe('CheckFn', () => { it('should work with sync function', () => { - const syncCheck = (ctx: any, data: any, config: any): GuardrailResult => ({ + const syncCheck = (ctx: Record, data: string): GuardrailResult => ({ tripwireTriggered: data === 'trigger', info: { checked_text: data, }, }); - const result = syncCheck({}, 'trigger', {}); + const result = syncCheck({}, 'trigger'); expect(result.tripwireTriggered).toBe(true); }); it('should work with async function', async () => { - const asyncCheck = async (ctx: any, data: any, config: any): Promise => ({ + const asyncCheck = async (ctx: Record, data: string): Promise => ({ tripwireTriggered: data === 'trigger', info: { checked_text: data, }, }); - const result = await asyncCheck({}, 'trigger', {}); + const result = await asyncCheck({}, 'trigger'); expect(result.tripwireTriggered).toBe(true); }); }); @@ -80,7 +81,7 @@ describe('Types Module', () => { describe('GuardrailLLMContext', () => { it('should require guardrailLlm property', () => { const context: GuardrailLLMContext = { - guardrailLlm: {} as any, + guardrailLlm: {} as unknown as OpenAI, }; expect(context.guardrailLlm).toBeDefined(); @@ -91,11 +92,11 @@ describe('Types Module', () => { const mockLLM = { someMethod: () => 'test' }; const context: GuardrailLLMContext = { - guardrailLlm: mockLLM as any, + guardrailLlm: mockLLM as unknown as OpenAI, }; expect(context.guardrailLlm).toBeDefined(); - expect((context.guardrailLlm as any).someMethod()).toBe('test'); + expect((context.guardrailLlm as unknown as { someMethod: () => string }).someMethod()).toBe('test'); }); }); @@ -117,10 +118,10 @@ describe('Types Module', () => { }); it('should allow flexible input types', () => { - const check = (ctx: any, data: any, config: any): GuardrailResult => ({ + const check = (ctx: unknown, data: unknown, _config: unknown): GuardrailResult => ({ tripwireTriggered: false, info: { - checked_text: data, + checked_text: String(data), }, }); @@ -129,10 +130,10 @@ describe('Types Module', () => { }); it('should allow flexible config types', () => { - const check = (ctx: any, data: any, config: any): GuardrailResult => ({ + const check = (ctx: unknown, data: unknown, _config: unknown): GuardrailResult => ({ tripwireTriggered: false, info: { - checked_text: data, + checked_text: String(data), }, }); diff --git a/src/__tests__/unit/utils/context.test.ts b/src/__tests__/unit/utils/context.test.ts index 4a072ea..bf55707 100644 --- a/src/__tests__/unit/utils/context.test.ts +++ b/src/__tests__/unit/utils/context.test.ts @@ -32,7 +32,7 @@ describe('validateGuardrailContext', () => { }); it('throws informative error when context is not introspectable', () => { - expect(() => validateGuardrailContext(guardrail, null as unknown as any)).toThrow( + expect(() => validateGuardrailContext(guardrail, null as unknown as object)).toThrow( /Context must support property access/ ); }); diff --git a/src/__tests__/unit/utils/openai-vector-store.test.ts b/src/__tests__/unit/utils/openai-vector-store.test.ts index dd84fcc..f659bfd 100644 --- a/src/__tests__/unit/utils/openai-vector-store.test.ts +++ b/src/__tests__/unit/utils/openai-vector-store.test.ts @@ -47,7 +47,7 @@ vi.mock('openai', () => ({ })); describe('createOpenAIVectorStoreFromPath', () => { - let createOpenAIVectorStoreFromPath: any; + let createOpenAIVectorStoreFromPath: (path: string, config: { apiKey: string }) => Promise; beforeEach(async () => { openAiState.failUploads = false; @@ -59,8 +59,8 @@ describe('createOpenAIVectorStoreFromPath', () => { global.File = global.File || (class PolyfillFile { - constructor(public blobs: any[], public name: string, public options: any) {} - } as any); + constructor(public blobs: unknown[], public name: string, public options: Record) {} + } as unknown as typeof File); vi.resetModules(); ({ createOpenAIVectorStoreFromPath } = await import('../../../utils/openai-vector-store')); diff --git a/src/__tests__/unit/utils/parsing.test.ts b/src/__tests__/unit/utils/parsing.test.ts index 09ed018..34fa236 100644 --- a/src/__tests__/unit/utils/parsing.test.ts +++ b/src/__tests__/unit/utils/parsing.test.ts @@ -2,7 +2,7 @@ * Tests for response parsing utilities. */ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { describe, it, expect, vi, afterEach, afterAll } from 'vitest'; import { parseResponseItems, parseResponseItemsAsJson, @@ -56,7 +56,8 @@ describe('parsing utilities', () => { const entries = parseResponseItems(response); expect(entries).toEqual([{ role: 'assistant', content: 'Segment 1 + segment 2' }]); - expect(warnSpy).toHaveBeenCalled(); // unknown part logs warning + // Unknown parts are now silently skipped in text-only mode + expect(warnSpy).not.toHaveBeenCalled(); }); it('supports filtering entries by predicate', () => { diff --git a/src/agents.ts b/src/agents.ts index 6c6ce93..60ec95a 100644 --- a/src/agents.ts +++ b/src/agents.ts @@ -6,8 +6,24 @@ * configuration file. */ -import { GuardrailLLMContext } from './types'; -import { loadPipelineBundles, instantiateGuardrails, PipelineConfig } from './runtime'; +import { GuardrailLLMContext, GuardrailResult, TextOnlyContent, ContentPart } from './types'; +import { ContentUtils } from './utils/content'; +import { loadPipelineBundles, instantiateGuardrails, PipelineConfig, GuardrailBundle, ConfiguredGuardrail } from './runtime'; + +// Import Agents SDK types for better type safety +import type { + InputGuardrail, + OutputGuardrail, + InputGuardrailFunctionArgs, + OutputGuardrailFunctionArgs +} from '@openai/agents-core'; + +// Type for agent output that might have different structures +interface AgentOutput { + response?: string; + finalOutput?: string | TextOnlyContent; + [key: string]: string | TextOnlyContent | undefined; +} /** * Drop-in replacement for Agents SDK Agent with automatic guardrails integration. @@ -55,9 +71,9 @@ export class GuardrailAgent { config: string | PipelineConfig, name: string, instructions: string, - agentKwargs: Record = {}, + agentKwargs: Record = {}, raiseGuardrailErrors: boolean = false - ): Promise { + ): Promise { // Returns agents.Agent try { // Dynamic import to avoid bundling issues @@ -68,20 +84,20 @@ export class GuardrailAgent { const pipeline = await loadPipelineBundles(config); // Create input guardrails from pre_flight and input stages - const inputGuardrails = []; - if ((pipeline as any).pre_flight) { + const inputGuardrails: InputGuardrail[] = []; + if ((pipeline as Record).pre_flight) { const preFlightGuardrails = await createInputGuardrailsFromStage( 'pre_flight', - (pipeline as any).pre_flight, + (pipeline as Record).pre_flight as GuardrailBundle, undefined, raiseGuardrailErrors ); inputGuardrails.push(...preFlightGuardrails); } - if ((pipeline as any).input) { + if ((pipeline as Record).input) { const inputStageGuardrails = await createInputGuardrailsFromStage( 'input', - (pipeline as any).input, + (pipeline as Record).input as GuardrailBundle, undefined, raiseGuardrailErrors ); @@ -89,11 +105,11 @@ export class GuardrailAgent { } // Create output guardrails from output stage - const outputGuardrails = []; - if ((pipeline as any).output) { + const outputGuardrails: OutputGuardrail[] = []; + if ((pipeline as Record).output) { const outputStageGuardrails = await createOutputGuardrailsFromStage( 'output', - (pipeline as any).output, + (pipeline as Record).output as GuardrailBundle, undefined, raiseGuardrailErrors ); @@ -121,130 +137,153 @@ export class GuardrailAgent { async function createInputGuardrailsFromStage( stageName: string, - stageConfig: any, + stageConfig: GuardrailBundle, context?: GuardrailLLMContext, raiseGuardrailErrors: boolean = false -): Promise { +): Promise { // Instantiate guardrails for this stage - const guardrails = await instantiateGuardrails(stageConfig); - - return guardrails.map((guardrail: any) => ({ - name: `${stageName}: ${guardrail.name || guardrail.definition?.name || 'Unknown Guardrail'}`, - execute: async ({ input, context: agentContext }: { input: string; context?: any }) => { - try { - // Create a proper context with OpenAI client if needed - let guardContext = context || agentContext || {}; - if (!guardContext.guardrailLlm) { - const { OpenAI } = require('openai'); - guardContext = { - ...guardContext, - guardrailLlm: new OpenAI(), - }; + const guardrails: ConfiguredGuardrail[] = await instantiateGuardrails(stageConfig); + + return guardrails.map((guardrail: ConfiguredGuardrail) => { + return { + name: `${stageName}: ${guardrail.definition.name || 'Unknown Guardrail'}`, + execute: async (args: InputGuardrailFunctionArgs) => { + const { input, context: agentContext } = args; + // Extract text from input - handle both string and message object formats + let inputText = ''; + if (typeof input === 'string') { + inputText = input; + } else if (input && typeof input === 'object' && 'content' in input) { + // Use ContentUtils to extract text from message content + inputText = ContentUtils.extractTextFromMessage({ + role: 'user', + content: input.content as string | ContentPart[] + }); } + + try { - const result = await guardrail.run(guardContext, input); + // Create a proper context with OpenAI client if needed + let guardContext: GuardrailLLMContext = (context as unknown as GuardrailLLMContext) || (agentContext as unknown as GuardrailLLMContext) || {} as GuardrailLLMContext; + if (!guardContext.guardrailLlm) { + const { OpenAI } = require('openai'); + guardContext = { + ...guardContext, + guardrailLlm: new OpenAI(), + }; + } - // Check for execution failures when raiseGuardrailErrors=true - if (raiseGuardrailErrors && result.executionFailed) { - throw result.originalException; - } + const result: GuardrailResult = await guardrail.run(guardContext, inputText); + + // Check for execution failures when raiseGuardrailErrors=true + if (raiseGuardrailErrors && result.executionFailed) { + throw result.originalException; + } - return { - outputInfo: result.info || null, - tripwireTriggered: result.tripwireTriggered || false, - }; - } catch (error) { - if (raiseGuardrailErrors) { - // Re-raise the exception to stop execution - throw error; - } else { - // When raiseGuardrailErrors=false, treat errors as safe and continue execution - // Return tripwireTriggered=false to allow execution to continue return { outputInfo: { - error: error instanceof Error ? error.message : String(error), - guardrail_name: guardrail.name || 'unknown', + ...(result.info || {}), + input: inputText, }, - tripwireTriggered: false, + tripwireTriggered: result.tripwireTriggered || false, }; + } catch (error) { + if (raiseGuardrailErrors) { + // Re-raise the exception to stop execution + throw error; + } else { + // When raiseGuardrailErrors=false, treat errors as safe and continue execution + // Return tripwireTriggered=false to allow execution to continue + return { + outputInfo: { + error: error instanceof Error ? error.message : String(error), + guardrail_name: guardrail.definition.name || 'unknown', + input: inputText, + }, + tripwireTriggered: false, + }; + } } } - }, - })); + }; + }); } async function createOutputGuardrailsFromStage( stageName: string, - stageConfig: any, + stageConfig: GuardrailBundle, context?: GuardrailLLMContext, raiseGuardrailErrors: boolean = false -): Promise { +): Promise { // Instantiate guardrails for this stage - const guardrails = await instantiateGuardrails(stageConfig); - - return guardrails.map((guardrail: any) => ({ - name: `${stageName}: ${guardrail.name || guardrail.definition?.name || 'Unknown Guardrail'}`, - execute: async ({ - agentOutput, - context: agentContext, - }: { - agentOutput: any; - context?: any; - }) => { - try { + const guardrails: ConfiguredGuardrail[] = await instantiateGuardrails(stageConfig); + + return guardrails.map((guardrail: ConfiguredGuardrail) => { + return { + name: `${stageName}: ${guardrail.definition.name || 'Unknown Guardrail'}`, + execute: async (args: OutputGuardrailFunctionArgs) => { + const { agentOutput, context: agentContext } = args; // Extract the output text - could be in different formats let outputText = ''; if (typeof agentOutput === 'string') { outputText = agentOutput; - } else if (agentOutput?.response) { - outputText = agentOutput.response; - } else if (agentOutput?.finalOutput) { + } else if (agentOutput && typeof agentOutput === 'object' && 'response' in agentOutput) { + outputText = (agentOutput as AgentOutput).response || ''; + } else if (agentOutput && typeof agentOutput === 'object' && 'finalOutput' in agentOutput) { + const finalOutput = (agentOutput as AgentOutput).finalOutput; outputText = - typeof agentOutput.finalOutput === 'string' - ? agentOutput.finalOutput - : JSON.stringify(agentOutput.finalOutput); + typeof finalOutput === 'string' + ? finalOutput + : JSON.stringify(finalOutput); } else { // Try to extract any string content outputText = JSON.stringify(agentOutput); } + + try { - // Create a proper context with OpenAI client if needed - let guardContext = context || agentContext || {}; - if (!guardContext.guardrailLlm) { - const { OpenAI } = require('openai'); - guardContext = { - ...guardContext, - guardrailLlm: new OpenAI(), - }; - } + // Create a proper context with OpenAI client if needed + let guardContext: GuardrailLLMContext = (context as unknown as GuardrailLLMContext) || (agentContext as unknown as GuardrailLLMContext) || {} as GuardrailLLMContext; + if (!guardContext.guardrailLlm) { + const { OpenAI } = require('openai'); + guardContext = { + ...guardContext, + guardrailLlm: new OpenAI(), + }; + } - const result = await guardrail.run(guardContext, outputText); + const result: GuardrailResult = await guardrail.run(guardContext, outputText); - // Check for execution failures when raiseGuardrailErrors=true - if (raiseGuardrailErrors && result.executionFailed) { - throw result.originalException; - } + // Check for execution failures when raiseGuardrailErrors=true + if (raiseGuardrailErrors && result.executionFailed) { + throw result.originalException; + } - return { - outputInfo: result.info || null, - tripwireTriggered: result.tripwireTriggered || false, - }; - } catch (error) { - if (raiseGuardrailErrors) { - // Re-raise the exception to stop execution - throw error; - } else { - // When raiseGuardrailErrors=false, treat errors as safe and continue execution - // Return tripwireTriggered=false to allow execution to continue return { outputInfo: { - error: error instanceof Error ? error.message : String(error), - guardrail_name: guardrail.name || 'unknown', + ...(result.info || {}), + input: outputText, }, - tripwireTriggered: false, + tripwireTriggered: result.tripwireTriggered || false, }; + } catch (error) { + if (raiseGuardrailErrors) { + // Re-raise the exception to stop execution + throw error; + } else { + // When raiseGuardrailErrors=false, treat errors as safe and continue execution + // Return tripwireTriggered=false to allow execution to continue + return { + outputInfo: { + error: error instanceof Error ? error.message : String(error), + guardrail_name: guardrail.definition.name || 'unknown', + input: outputText, + }, + tripwireTriggered: false, + }; + } } } - }, - })); + }; + }); } diff --git a/src/base-client.ts b/src/base-client.ts index c03e4d5..2624205 100644 --- a/src/base-client.ts +++ b/src/base-client.ts @@ -5,16 +5,14 @@ * async and sync guardrails clients. */ -import { OpenAI } from 'openai'; -import { GuardrailResult, GuardrailLLMContext } from './types'; +import { OpenAI, AzureOpenAI } from 'openai'; +import { GuardrailResult, GuardrailLLMContext, TextOnlyMessageArray, TextOnlyContent, Message, ContentPart, TextContentPart } from './types'; +import { ContentUtils } from './utils/content'; import { - loadConfigBundle, - runGuardrails, - instantiateGuardrails, GuardrailBundle, ConfiguredGuardrail, + instantiateGuardrails, } from './runtime'; -import { defaultSpecRegistry } from './registry'; // Type alias for OpenAI response types export type OpenAIResponseType = @@ -112,57 +110,27 @@ export abstract class GuardrailsBaseClient { public raiseGuardrailErrors: boolean = false; /** - * Extract the latest user message text and its index from a list of message-like items. + * Extract the latest user text message from a conversation for text guardrails. * - * Supports both dict-based messages (OpenAI) and object models with - * role/content attributes. Handles Responses API content-part format. + * This method specifically extracts text content from messages. For other content types, + * create parallel methods like extractLatestUserImage() or extractLatestUserVideo(). * - * @param messages List of messages + * @param messages List of messages (can include non-text content) * @returns Tuple of [message_text, message_index]. Index is -1 if no user message found. */ - public extractLatestUserMessage(messages: any[]): [string, number] { - const getAttr = (obj: any, key: string): any => { - if (typeof obj === 'object' && obj !== null) { - return obj[key]; - } - return undefined; - }; - - const contentToText = (content: any): string => { - // String content - if (typeof content === 'string') { - return content.trim(); - } - // List of content parts (Responses API) - if (Array.isArray(content)) { - const parts: string[] = []; - for (const part of content) { - if (typeof part === 'object' && part !== null) { - const partType = part.type; - const textVal = part.text || ''; - if ( - ['input_text', 'text', 'output_text', 'summary_text'].includes(partType) && - typeof textVal === 'string' - ) { - parts.push(textVal); - } - } + public extractLatestUserTextMessage(messages: Message[]): [string, number] { + const textOnlyMessages = ContentUtils.filterToTextOnly(messages); + + for (let i = textOnlyMessages.length - 1; i >= 0; i--) { + const message = textOnlyMessages[i]; + if (message.role === 'user') { + const text = ContentUtils.extractTextFromMessage(message); + if (text) { + return [text, i]; } - return parts.join(' ').trim(); - } - return ''; - }; - - for (let i = messages.length - 1; i >= 0; i--) { - const message = messages[i]; - const role = getAttr(message, 'role'); - if (role === 'user') { - const content = getAttr(message, 'content'); - const messageText = contentToText(content); - return [messageText, i]; } } - + return ['', -1]; } @@ -207,9 +175,9 @@ export abstract class GuardrailsBaseClient { * @returns Modified data with pre-flight changes applied */ public applyPreflightModifications( - data: any[] | string, + data: Message[] | string, preflightResults: GuardrailResult[] - ): any[] | string { + ): Message[] | string { if (preflightResults.length === 0) { return data; } @@ -259,45 +227,29 @@ export abstract class GuardrailsBaseClient { return maskText(data); } else { // Handle message list input (primarily for chat API and structured Responses API) - const [, latestUserIdx] = this.extractLatestUserMessage(data); + const [, latestUserIdx] = this.extractLatestUserTextMessage(data); if (latestUserIdx === -1) { return data; } // Use shallow copy for efficiency - we only modify the content field of one message const modifiedMessages = [...data]; - - // Extract current content safely - const currentContent = data[latestUserIdx]?.content; + const currentContent = data[latestUserIdx].content; // Apply modifications based on content type - let modifiedContent: any; + let modifiedContent: string | ContentPart[]; if (typeof currentContent === 'string') { // Plain string content - mask individually modifiedContent = maskText(currentContent); } else if (Array.isArray(currentContent)) { // Structured content - mask each text part individually - modifiedContent = []; - for (const part of currentContent) { - if (typeof part === 'object' && part !== null) { - const partType = part.type; - if ( - ['input_text', 'text', 'output_text', 'summary_text'].includes(partType) && - 'text' in part - ) { - // Mask this specific text part individually - const originalText = part.text; - const maskedText = maskText(originalText); - modifiedContent.push({ ...part, text: maskedText }); - } else { - // Keep non-text parts unchanged - modifiedContent.push(part); - } - } else { - // Keep unknown parts unchanged - modifiedContent.push(part); + modifiedContent = currentContent.map(part => { + if (ContentUtils.isText(part)) { + const textPart = part as TextContentPart; + return { ...textPart, text: maskText(textPart.text) }; } - } + return part; // Keep non-text parts unchanged + }); } else { // Unknown content type - skip modifications return data; @@ -351,23 +303,30 @@ export abstract class GuardrailsBaseClient { /** * Extract text content from various response types. */ - protected extractResponseText(response: any): string { - const choice0 = response.choices?.[0]; - const candidates = [ - choice0?.delta?.content, - choice0?.message?.content, - response.output_text, - response.delta, - ]; - - for (const value of candidates) { - if (typeof value === 'string') { - return value || ''; - } + protected extractResponseText(response: OpenAIResponseType): string { + // Handle Response type (no choices property) + if ('output' in response) { + return response.output_text || ''; } - - if (response.type === 'response.output_text.delta') { - return response.delta || ''; + + // Handle other response types with choices + if ('choices' in response && response.choices) { + const choice0 = response.choices[0]; + + // Handle ChatCompletion + if ('message' in choice0 && choice0.message) { + return choice0.message.content || ''; + } + + // Handle Completion + if ('text' in choice0 && choice0.text) { + return choice0.text; + } + + // Handle streaming responses (ChatCompletionChunk) + if ('delta' in choice0 && choice0.delta) { + return choice0.delta.content || ''; + } } return ''; @@ -399,7 +358,7 @@ export abstract class GuardrailsBaseClient { public async initializeClient( config: string | PipelineConfig, openaiArgs: ConstructorParameters[0], - clientClass: typeof OpenAI | any + clientClass: typeof OpenAI | typeof AzureOpenAI ): Promise { // Create a separate OpenAI client instance for resource access // This avoids circular reference issues when overriding OpenAI's resource properties @@ -418,13 +377,29 @@ export abstract class GuardrailsBaseClient { */ protected abstract overrideResources(): void; + + /** + * Determine if a guardrail should run based on content type compatibility. + * + * Currently only supports text/plain content type matching. + * + * @future To extend for multi-modal support: + * - Add image/*, audio/*, video/* pattern matching + * - Implement content type hierarchy (image/* matches image/jpeg, etc.) + * - Add wildcard support for broader compatibility + */ + private shouldRunGuardrail(guardrail: ConfiguredGuardrail, detectedContentType: string): boolean { + return guardrail.definition.mediaType === detectedContentType; + } + + /** * Run guardrails for a specific pipeline stage. */ public async runStageGuardrails( stageName: 'pre_flight' | 'input' | 'output', text: string, - conversationHistory?: any[], + conversationHistory?: Message[], suppressTripwire: boolean = false, raiseGuardrailErrors: boolean = false ): Promise { @@ -433,20 +408,48 @@ export abstract class GuardrailsBaseClient { } try { - // Check if prompt injection detection guardrail is present and we have conversation history - const hasInjectionDetection = this.guardrails[stageName].some( - (guardrail) => guardrail.definition.name.toLowerCase() === 'prompt injection detection' + // Content type detection - currently text-only + // @future: Add content analysis for multi-modal support (images, audio, video) + const detectedContentType = 'text/plain'; + + // Filter guardrails based on content type compatibility + const compatibleGuardrails = this.guardrails[stageName].filter(guardrail => + this.shouldRunGuardrail(guardrail, detectedContentType) + ); + + const skippedGuardrails = this.guardrails[stageName].filter(guardrail => + !this.shouldRunGuardrail(guardrail, detectedContentType) + ); + + // Log warnings for skipped guardrails + if (skippedGuardrails.length > 0) { + console.warn( + `⚠️ Guardrails Warning: ${skippedGuardrails.length} guardrails skipped due to content type mismatch ` + + `(detected: ${detectedContentType}). Skipped: ${skippedGuardrails.map(g => g.definition.name).join(', ')}` + ); + } + + if (compatibleGuardrails.length === 0) { + console.warn(`No guardrails compatible with content type '${detectedContentType}' for stage '${stageName}'`); + return []; + } + + // Check if any guardrail requires conversation history and we have it available + const needsConversationHistory = compatibleGuardrails.some( + (guardrail) => guardrail.definition.metadata?.requiresConversationHistory ); let ctx = this.context; - if (hasInjectionDetection && conversationHistory) { - ctx = this.createContextWithConversation(conversationHistory); + if (needsConversationHistory && conversationHistory) { + // Filter to text-only for conversation history processing + const textOnlyHistory = ContentUtils.filterToTextOnly(conversationHistory); + ctx = this.createContextWithConversation(textOnlyHistory); } const results: GuardrailResult[] = []; - // Run guardrails in parallel using Promise.allSettled to capture all results - const guardrailPromises = this.guardrails[stageName].map(async (guardrail) => { + // Run compatible guardrails in parallel using Promise.allSettled to capture all results + const guardrailPromises = compatibleGuardrails.map(async (guardrail) => { try { const result = await guardrail.run(ctx, text); // Add stage and guardrail metadata @@ -454,6 +457,8 @@ export abstract class GuardrailsBaseClient { ...result.info, stage_name: stageName, guardrail_name: guardrail.definition.name, + media_type: guardrail.definition.mediaType, + detected_content_type: detectedContentType, }; return result; } catch (error) { @@ -467,6 +472,8 @@ export abstract class GuardrailsBaseClient { checked_text: text, // Return original text on error stage_name: stageName, guardrail_name: guardrail.definition.name, + media_type: guardrail.definition.mediaType, + detected_content_type: detectedContentType, error: error instanceof Error ? error.message : String(error), }, }; @@ -518,16 +525,21 @@ export abstract class GuardrailsBaseClient { } /** - * Create a context with conversation history for prompt injection detection guardrail. + * Create a context with conversation history for guardrails that require it. + * + * @future To extend for multi-modal support: + * - Add support for image/audio content in conversation history + * - Implement content type filtering based on guardrail requirements + * - Add metadata about content types in the context */ - protected createContextWithConversation(conversationHistory: any[]): GuardrailLLMContext { - // Create a new context that includes conversation history and prompt injection detection tracking + protected createContextWithConversation(conversationHistory: TextOnlyMessageArray): GuardrailLLMContext { + // Create a new context that includes conversation history and tracking metadata return { guardrailLlm: this.context.guardrailLlm, // Add conversation history methods getConversationHistory: () => conversationHistory, } as GuardrailLLMContext & { - getConversationHistory(): any[]; + getConversationHistory(): TextOnlyMessageArray; }; } @@ -535,9 +547,9 @@ export abstract class GuardrailsBaseClient { * Append LLM response to conversation history. */ protected appendLlmResponseToConversation( - conversationHistory: any[] | string | null, - llmResponse: any - ): any[] { + conversationHistory: TextOnlyMessageArray | string | null, + llmResponse: OpenAIResponseType + ): TextOnlyMessageArray { if (!conversationHistory) { conversationHistory = []; } @@ -551,16 +563,36 @@ export abstract class GuardrailsBaseClient { const updatedHistory = [...conversationHistory]; // For responses API: append the output directly - if (llmResponse.output && Array.isArray(llmResponse.output)) { - updatedHistory.push(...llmResponse.output); + if ('output' in llmResponse && llmResponse.output && Array.isArray(llmResponse.output)) { + // Convert ResponseOutputItem to TextOnlyMessage format + const convertedOutput = llmResponse.output + .filter(item => 'role' in item && 'content' in item) + .map(item => { + const itemWithRole = item as { role: string; content: unknown }; + return { + role: itemWithRole.role, + content: itemWithRole.content as TextOnlyContent + }; + }); + updatedHistory.push(...convertedOutput); } // For chat completions: append the choice message directly (prompt injection detection check will parse) else if ( + 'choices' in llmResponse && llmResponse.choices && Array.isArray(llmResponse.choices) && - llmResponse.choices.length > 0 + llmResponse.choices.length > 0 && + 'message' in llmResponse.choices[0] && + llmResponse.choices[0].message && + llmResponse.choices[0].message.content ) { - updatedHistory.push(llmResponse.choices[0].message); + const message = llmResponse.choices[0].message; + if (message.content) { + updatedHistory.push({ + role: message.role, + content: message.content + }); + } } return updatedHistory; @@ -573,7 +605,7 @@ export abstract class GuardrailsBaseClient { llmResponse: T, preflightResults: GuardrailResult[], inputResults: GuardrailResult[], - conversationHistory?: any[], + conversationHistory?: TextOnlyMessageArray, suppressTripwire: boolean = false ): Promise> { // Create complete conversation history including the LLM response diff --git a/src/checks/competitors.ts b/src/checks/competitors.ts index 22f53ea..8e07722 100644 --- a/src/checks/competitors.ts +++ b/src/checks/competitors.ts @@ -42,17 +42,17 @@ export type CompetitorContext = z.infer; * @param config Configuration specifying competitor keywords. * @returns GuardrailResult indicating whether any competitor keyword was detected. */ -export const competitorsCheck: CheckFn = async ( +export const competitorsCheck: CheckFn = ( ctx, data, config -): Promise => { +): GuardrailResult => { // Convert to KeywordsConfig format and reuse the keywords check const keywordsConfig: KeywordsConfig = { keywords: config.keywords, }; - const result = await keywordsCheck(ctx, data, keywordsConfig); + const result = keywordsCheck(ctx, data, keywordsConfig) as GuardrailResult; // Update the guardrail name in the result return { diff --git a/src/checks/jailbreak.ts b/src/checks/jailbreak.ts index c38dddc..f3d7a83 100644 --- a/src/checks/jailbreak.ts +++ b/src/checks/jailbreak.ts @@ -7,7 +7,6 @@ * engineering. */ -import { z } from 'zod'; import { CheckFn, GuardrailLLMContext } from '../types'; import { LLMConfig, LLMOutput, createLLMCheckFn } from './llm-base'; diff --git a/src/checks/keywords.ts b/src/checks/keywords.ts index 37f5328..96cf2d7 100644 --- a/src/checks/keywords.ts +++ b/src/checks/keywords.ts @@ -8,7 +8,6 @@ import { z } from 'zod'; import { CheckFn, GuardrailResult } from '../types'; import { defaultSpecRegistry } from '../registry'; -import { GuardrailSpecMetadata } from '../spec'; /** * Configuration schema for the keywords guardrail. @@ -47,8 +46,8 @@ export const keywordsCheck: CheckFn = ( config ): GuardrailResult => { // Handle the case where config might be wrapped in another object - const actualConfig = (config as any).config || config; - const { keywords } = actualConfig; + const actualConfig = (config as Record).config || config; + const { keywords } = actualConfig as KeywordsConfig; // Sanitize keywords by stripping trailing punctuation const sanitizedKeywords = keywords.map((k: string) => k.replace(/[.,!?;:]+$/, '')); diff --git a/src/checks/llm-base.ts b/src/checks/llm-base.ts index 63cfb75..2d4bf09 100644 --- a/src/checks/llm-base.ts +++ b/src/checks/llm-base.ts @@ -8,6 +8,7 @@ */ import { z } from 'zod'; +import { OpenAI } from 'openai'; import { CheckFn, GuardrailResult, GuardrailLLMContext } from '../types'; import { defaultSpecRegistry } from '../registry'; @@ -28,6 +29,8 @@ export const LLMConfig = z.object({ .describe( 'Minimum confidence threshold to trigger the guardrail (0.0 to 1.0). Defaults to 0.7.' ), + /** Optional system prompt details for user-defined LLM guardrails */ + system_prompt_details: z.string().optional().describe('Additional system prompt details'), }); export type LLMConfig = z.infer; @@ -147,7 +150,7 @@ function stripJsonCodeFence(text: string): string { export async function runLLM( text: string, systemPrompt: string, - client: { chat: { completions: { create: (params: any) => Promise } } }, + client: OpenAI, model: string, outputModel: typeof LLMOutput ): Promise { @@ -198,7 +201,7 @@ export async function runLLM( } // Fail-closed on JSON parsing errors (malformed or non-JSON responses) - if (error instanceof SyntaxError || (error as any)?.constructor?.name === 'SyntaxError') { + if (error instanceof SyntaxError || (error as Error)?.constructor?.name === 'SyntaxError') { console.warn( 'LLM returned non-JSON or malformed JSON. Failing closed (flagged=true).', error @@ -249,12 +252,12 @@ export function createLLMCheckFn( description: string, systemPrompt: string, outputModel: typeof LLMOutput = LLMOutput, - configModel: any = LLMConfig -): CheckFn { + configModel: typeof LLMConfig = LLMConfig +): CheckFn> { async function guardrailFunc( ctx: GuardrailLLMContext, data: string, - config: any + config: z.infer ): Promise { let renderedSystemPrompt = systemPrompt; @@ -269,20 +272,20 @@ export function createLLMCheckFn( const analysis = await runLLM( data, renderedSystemPrompt, - ctx.guardrailLlm, + ctx.guardrailLlm as OpenAI, // Type assertion to handle OpenAI client compatibility config.model, outputModel ); // Check if this is an error result (LLMErrorOutput with error_message) if ('info' in analysis && analysis.info) { - const errorInfo = analysis.info as any; + const errorInfo = analysis.info as Record; if (errorInfo.error_message) { // This is an execution failure (LLMErrorOutput) return { tripwireTriggered: false, // Don't trigger tripwire on execution errors executionFailed: true, - originalException: new Error(errorInfo.error_message || 'LLM execution failed'), + originalException: new Error(String(errorInfo.error_message || 'LLM execution failed')), info: { checked_text: data, guardrail_name: name, @@ -311,7 +314,7 @@ export function createLLMCheckFn( guardrailFunc, description, 'text/plain', - configModel, + configModel as z.ZodType>, LLMContext, { engine: 'LLM' } ); diff --git a/src/checks/moderation.ts b/src/checks/moderation.ts index e8c697c..0357b80 100644 --- a/src/checks/moderation.ts +++ b/src/checks/moderation.ts @@ -73,7 +73,7 @@ export const ModerationConfigRequired = z */ export const ModerationContext = z.object({ /** Optional OpenAI client to reuse instead of creating a new one */ - guardrailLlm: z.any().optional(), + guardrailLlm: z.unknown().optional(), }); export type ModerationContext = z.infer; @@ -96,22 +96,25 @@ export const moderationCheck: CheckFn => { // Handle the case where config might be wrapped in another object - const actualConfig = (config as any).config || config; + const actualConfig = (config as Record).config || config; // Ensure categories is an array - const categories = actualConfig.categories || Object.values(Category); + const configObj = actualConfig as Record; + const categories = (configObj.categories as string[]) || Object.values(Category); // Reuse provided client only if it targets the official OpenAI API. - const reuseClientIfOpenAI = (context: any): OpenAI | null => { + const reuseClientIfOpenAI = (context: unknown): OpenAI | null => { try { - const candidate = context?.guardrailLlm; + const contextObj = context as Record; + const candidate = contextObj?.guardrailLlm; if (!candidate || typeof candidate !== 'object') return null; - if (!(candidate instanceof (OpenAI as any))) return null; + if (!(candidate instanceof OpenAI)) return null; + const candidateObj = candidate as unknown as Record; const baseURL: string | undefined = - (candidate as any).baseURL ?? - (candidate as any)._client?.baseURL ?? - (candidate as any)._baseURL; + (candidateObj.baseURL as string) ?? + ((candidateObj._client as Record)?.baseURL as string) ?? + (candidateObj._baseURL as string); if ( baseURL === undefined || @@ -153,7 +156,7 @@ export const moderationCheck: CheckFn)[catValue] || false; if (isFlagged) { flaggedCategories.push(catValue); } diff --git a/src/checks/pii.ts b/src/checks/pii.ts index 41dd42e..da9e9d0 100644 --- a/src/checks/pii.ts +++ b/src/checks/pii.ts @@ -179,7 +179,7 @@ interface PiiAnalyzerResult { const DEFAULT_PII_PATTERNS: Record = { [PIIEntity.CREDIT_CARD]: /\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/g, [PIIEntity.CRYPTO]: /\b[13][a-km-zA-HJ-NP-Z1-9]{25,34}\b/g, - [PIIEntity.DATE_TIME]: /\b(0[1-9]|1[0-2])[\/\-](0[1-9]|[12]\d|3[01])[\/\-](19|20)\d{2}\b/g, + [PIIEntity.DATE_TIME]: /\b(0[1-9]|1[0-2])[/-](0[1-9]|[12]\d|3[01])[/-](19|20)\d{2}\b/g, [PIIEntity.EMAIL_ADDRESS]: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, [PIIEntity.IBAN_CODE]: /\b[A-Z]{2}[0-9]{2}[A-Z0-9]{4}[0-9]{7}([A-Z0-9]?){0,16}\b/g, [PIIEntity.IP_ADDRESS]: /\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b/g, @@ -190,7 +190,7 @@ const DEFAULT_PII_PATTERNS: Record = { [PIIEntity.PHONE_NUMBER]: /\b(\+\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g, [PIIEntity.MEDICAL_LICENSE]: /\b[A-Z]{2}\d{6}\b/g, [PIIEntity.URL]: - /\bhttps?:\/\/(?:[-\w.])+(?:\:[0-9]+)?(?:\/(?:[\w\/_.])*(?:\?(?:[\w&=%.])*)?(?:\#(?:[\w.])*)?)?/g, + /\bhttps?:\/\/(?:[-\w.])+(?::[0-9]+)?(?:\/(?:[\w/_.])*(?:\?(?:[\w&=%.])*)?(?:#(?:[\w.])*)?)?/g, // USA [PIIEntity.US_BANK_NUMBER]: /\b\d{8,17}\b/g, @@ -303,7 +303,7 @@ function _detectPii(text: string, config: PIIConfig): PiiDetectionResult { * @returns Text with PII replaced by entity type markers * @throws Error if text is empty or null */ -function _scrubPii(text: string, detection: PiiDetectionResult, config: PIIConfig): string { +function _scrubPii(text: string, detection: PiiDetectionResult, _config: PIIConfig): string { if (!text) { throw new Error('Text cannot be empty or null'); } @@ -373,12 +373,11 @@ function _asResult( * @returns Indicates if any PII was found, and the findings * @throws Error if input text is empty or null */ -export const pii: CheckFn = async ( - ctx, +export const pii: CheckFn, string, PIIConfig> = async ( + _ctx, data, config ): Promise => { - const _ = ctx; const result = _detectPii(data, config); return _asResult(result, config, 'Contains PII', data); }; diff --git a/src/checks/prompt_injection_detection.ts b/src/checks/prompt_injection_detection.ts index 31c6768..e1290e9 100644 --- a/src/checks/prompt_injection_detection.ts +++ b/src/checks/prompt_injection_detection.ts @@ -25,9 +25,9 @@ */ import { z } from 'zod'; -import { CheckFn, GuardrailResult, GuardrailLLMContext, GuardrailLLMContextWithHistory } from '../types'; +import { CheckFn, GuardrailResult, GuardrailLLMContext, GuardrailLLMContextWithHistory, ContentPart, ConversationMessage } from '../types'; import { defaultSpecRegistry } from '../registry'; -import { LLMConfig, LLMOutput, runLLM } from './llm-base'; +import { LLMOutput, runLLM } from './llm-base'; import { parseConversationInput, POSSIBLE_CONVERSATION_KEYS } from '../utils/conversation'; /** @@ -44,6 +44,15 @@ export const PromptInjectionDetectionConfig = z.object({ export type PromptInjectionDetectionConfig = z.infer; +/** + * Extended content part for conversation handling. + */ +export interface ConversationContentPart extends ContentPart { + text?: string; + content?: string | ConversationContentPart[]; + [key: string]: unknown; +} + // Schema for registry registration (ensures all fields are provided) export const PromptInjectionDetectionConfigRequired = z.object({ model: z.string(), @@ -157,7 +166,7 @@ export const promptInjectionDetectionCheck: CheckFn< > = async (ctx, data, config): Promise => { try { const conversationHistory = safeGetConversationHistory(ctx); - const parsedDataMessages = parseConversationInput(data); + const parsedDataMessages = parseConversationInput(data) as ConversationMessage[]; if (conversationHistory.length === 0 && parsedDataMessages.length === 0) { return createSkipResult( 'No conversation history available', @@ -225,7 +234,7 @@ export const promptInjectionDetectionCheck: CheckFn< } }; -function safeGetConversationHistory(ctx: PromptInjectionDetectionContext): any[] { +function safeGetConversationHistory(ctx: PromptInjectionDetectionContext): ConversationMessage[] { try { const history = ctx.getConversationHistory(); if (Array.isArray(history)) { @@ -238,9 +247,9 @@ function safeGetConversationHistory(ctx: PromptInjectionDetectionContext): any[] } function prepareConversationSlice( - conversationHistory: any[], - parsedDataMessages: any[] -): { recentMessages: any[]; actionableMessages: any[]; userIntent: UserIntentDict } { + conversationHistory: ConversationMessage[], + parsedDataMessages: ConversationMessage[] +): { recentMessages: ConversationMessage[]; actionableMessages: ConversationMessage[]; userIntent: UserIntentDict } { const historyMessages = Array.isArray(conversationHistory) ? conversationHistory : []; const datasetMessages = Array.isArray(parsedDataMessages) ? parsedDataMessages : []; @@ -261,7 +270,7 @@ function prepareConversationSlice( return { recentMessages, actionableMessages, userIntent }; } -function sliceMessagesAfterLatestUser(messages: any[]): any[] { +function sliceMessagesAfterLatestUser(messages: ConversationMessage[]): ConversationMessage[] { if (!Array.isArray(messages) || messages.length === 0) { return []; } @@ -274,7 +283,7 @@ function sliceMessagesAfterLatestUser(messages: any[]): any[] { return messages.slice(); } -function findLastUserIndex(messages: any[]): number { +function findLastUserIndex(messages: ConversationMessage[]): number { for (let i = messages.length - 1; i >= 0; i -= 1) { if (isUserMessageEntry(messages[i])) { return i; @@ -283,7 +292,7 @@ function findLastUserIndex(messages: any[]): number { return -1; } -function isUserMessageEntry(entry: any, seen: Set = new Set()): boolean { +function isUserMessageEntry(entry: ConversationMessage, seen: Set = new Set()): boolean { if (!entry || typeof entry !== 'object') { return false; } @@ -315,9 +324,9 @@ function isUserMessageEntry(entry: any, seen: Set = new Set()): boolean { return false; } -function extractUserIntentFromMessages(messages: any[]): UserIntentDict { +function extractUserIntentFromMessages(messages: ConversationMessage[]): UserIntentDict { const userMessages: string[] = []; - const visited = new Set(); + const visited = new Set(); for (const message of messages) { collectUserMessages(message, userMessages, visited); @@ -333,7 +342,7 @@ function extractUserIntentFromMessages(messages: any[]): UserIntentDict { }; } -function collectUserMessages(value: any, collected: string[], visited: Set): void { +function collectUserMessages(value: ConversationMessage, collected: string[], visited: Set): void { if (!value || typeof value !== 'object') { return; } @@ -360,7 +369,7 @@ function collectUserMessages(value: any, collected: string[], visited: Set) } } -function extractUserMessageText(message: any): string { +function extractUserMessageText(message: ConversationMessage): string { if (typeof message === 'string') { return message; } @@ -391,7 +400,7 @@ function extractUserMessageText(message: any): string { return ''; } -function collectTextFromContent(content: any[]): string { +function collectTextFromContent(content: ConversationContentPart[]): string { const parts: string[] = []; for (const item of content) { @@ -400,8 +409,9 @@ function collectTextFromContent(content: any[]): string { } if (typeof item === 'string') { - if (item.trim().length > 0) { - parts.push(item.trim()); + const trimmed = (item as string).trim(); + if (trimmed.length > 0) { + parts.push(trimmed); } continue; } @@ -410,20 +420,29 @@ function collectTextFromContent(content: any[]): string { continue; } - if (typeof (item as { text?: string }).text === 'string') { - parts.push((item as { text: string }).text); + if (typeof (item as ConversationContentPart).text === 'string') { + const text = (item as ConversationContentPart).text; + if (text) { + parts.push(text); + } continue; } - if (typeof (item as { content?: string }).content === 'string') { - parts.push((item as { content: string }).content); + if (typeof (item as ConversationContentPart).content === 'string') { + const content = (item as ConversationContentPart).content as string; + if (content) { + parts.push(content); + } continue; } - if (Array.isArray((item as { content?: any[] }).content)) { - const nested = collectTextFromContent((item as { content: any[] }).content); - if (nested) { - parts.push(nested); + if (Array.isArray((item as ConversationContentPart).content)) { + const contentArray = (item as ConversationContentPart).content as ConversationContentPart[]; + if (contentArray) { + const nested = collectTextFromContent(contentArray); + if (nested) { + parts.push(nested); + } } continue; } @@ -432,14 +451,14 @@ function collectTextFromContent(content: any[]): string { return parts.join(' ').trim(); } -function extractActionableMessages(messages: any[]): any[] { +function extractActionableMessages(messages: ConversationMessage[]): ConversationMessage[] { if (!Array.isArray(messages)) { return []; } return messages.filter((message) => isActionableMessage(message)); } -function isActionableMessage(message: any, seen: Set = new Set()): boolean { +function isActionableMessage(message: ConversationMessage, seen: Set = new Set()): boolean { if (!message || typeof message !== 'object') { return false; } @@ -496,8 +515,8 @@ function createSkipResult( threshold: number, checkedText: string, userGoal: string = 'N/A', - action: any[] = [], - recentMessages: any[] = [] + action: ConversationMessage[] = [], + recentMessages: ConversationMessage[] = [] ): GuardrailResult { return { tripwireTriggered: false, @@ -533,8 +552,8 @@ ${contextText}`; function buildAnalysisPrompt( userGoalText: string, - recentMessages: any[], - actionableMessages: any[] + recentMessages: ConversationMessage[], + actionableMessages: ConversationMessage[] ): string { const recentMessagesText = recentMessages.length > 0 ? JSON.stringify(recentMessages, null, 2) : '[]'; @@ -579,7 +598,7 @@ async function callPromptInjectionDetectionLLM( // Validate the result matches PromptInjectionDetectionOutput schema return PromptInjectionDetectionOutput.parse(result); - } catch (error) { + } catch { // If runLLM fails validation, return a safe fallback PromptInjectionDetectionOutput console.warn('Prompt injection detection LLM call failed, using fallback'); return { @@ -598,5 +617,5 @@ defaultSpecRegistry.register( 'text/plain', PromptInjectionDetectionConfigRequired, undefined, // Context schema will be validated at runtime - { engine: 'LLM' } + { engine: 'LLM', requiresConversationHistory: true } ); diff --git a/src/checks/secret-keys.ts b/src/checks/secret-keys.ts index 5422691..4bb43f1 100644 --- a/src/checks/secret-keys.ts +++ b/src/checks/secret-keys.ts @@ -172,7 +172,7 @@ function charDiversity(s: string): number { */ function containsAllowedPattern(text: string): boolean { // Check if it's a URL pattern - const urlPattern = /^https?:\/\/[a-zA-Z0-9.-]+\/?[a-zA-Z0-9.\/_-]*$/i; + const urlPattern = /^https?:\/\/[a-zA-Z0-9.-]+\/?[a-zA-Z0-9./_-]*$/i; if (urlPattern.test(text)) { // If it's a URL, check if it contains any secret patterns // If it contains secrets, don't allow it diff --git a/src/checks/topical-alignment.ts b/src/checks/topical-alignment.ts index 71aad11..12b0bff 100644 --- a/src/checks/topical-alignment.ts +++ b/src/checks/topical-alignment.ts @@ -7,7 +7,7 @@ */ import { z } from 'zod'; -import { CheckFn, GuardrailResult, GuardrailLLMContext } from '../types'; +import { CheckFn, GuardrailResult } from '../types'; import { defaultSpecRegistry } from '../registry'; import { buildFullPrompt } from './llm-base'; diff --git a/src/checks/urls.ts b/src/checks/urls.ts index e9c6ea1..ab1974a 100644 --- a/src/checks/urls.ts +++ b/src/checks/urls.ts @@ -6,7 +6,7 @@ */ import { z } from 'zod'; -import { CheckFn, GuardrailResult } from '../types'; +import { CheckFn } from '../types'; import { defaultSpecRegistry } from '../registry'; /** @@ -56,11 +56,11 @@ function detectUrls(text: string): string[] { // Pattern 1: URLs with schemes (highest priority) const schemePatterns = [ - /https?:\/\/[^\s<>"{}|\\^`\[\]]+/gi, - /ftp:\/\/[^\s<>"{}|\\^`\[\]]+/gi, - /data:[^\s<>"{}|\\^`\[\]]+/gi, - /javascript:[^\s<>"{}|\\^`\[\]]+/gi, - /vbscript:[^\s<>"{}|\\^`\[\]]+/gi, + /https?:\/\/[^\s<>"{}|\\^`[\]]+/gi, + /ftp:\/\/[^\s<>"{}|\\^`[\]]+/gi, + /data:[^\s<>"{}|\\^`[\]]+/gi, + /javascript:[^\s<>"{}|\\^`[\]]+/gi, + /vbscript:[^\s<>"{}|\\^`[\]]+/gi, ]; const schemeUrls = new Set(); @@ -128,7 +128,7 @@ function detectUrls(text: string): string[] { const bareDomain = parsed.hostname.toLowerCase().replace(/^www\./, ''); schemeUrlDomains.add(bareDomain); } - } catch (error) { + } catch { // Skip URLs with parsing errors (malformed URLs, encoding issues) // This is expected for edge cases and doesn't require logging } diff --git a/src/checks/user-defined-llm.ts b/src/checks/user-defined-llm.ts index 7c63fd9..c1f96b2 100644 --- a/src/checks/user-defined-llm.ts +++ b/src/checks/user-defined-llm.ts @@ -7,7 +7,7 @@ */ import { z } from 'zod'; -import { CheckFn, GuardrailResult, GuardrailLLMContext } from '../types'; +import { CheckFn, GuardrailResult } from '../types'; import { defaultSpecRegistry } from '../registry'; /** @@ -100,9 +100,10 @@ export const userDefinedLLMCheck: CheckFn[0], - clientClass: typeof AzureOpenAI | any + clientClass: typeof AzureOpenAI | typeof OpenAI ): Promise { // Store azure arguments this.azureArgs = openaiArgs; diff --git a/src/evals/core/async-engine.ts b/src/evals/core/async-engine.ts index a170c36..9e0bb75 100644 --- a/src/evals/core/async-engine.ts +++ b/src/evals/core/async-engine.ts @@ -7,7 +7,7 @@ import { Context, RunEngine, Sample, SampleResult } from './types'; import { ConfiguredGuardrail } from '../../runtime'; -import { GuardrailLLMContextWithHistory, GuardrailResult } from '../../types'; +import { GuardrailLLMContextWithHistory, GuardrailResult, GuardrailLLMContext, ConversationMessage } from '../../types'; import { parseConversationInput } from '../../utils/conversation'; /** @@ -70,7 +70,7 @@ export class AsyncRunEngine implements RunEngine { */ private async evaluateSample(context: Context, sample: Sample): Promise { const triggered: Record = {}; - const details: Record = {}; + const details: Record = {}; for (const name of this.guardrailNames) { triggered[name] = false; @@ -96,6 +96,7 @@ export class AsyncRunEngine implements RunEngine { console.error(`Error running guardrail ${name} on sample ${sample.id}:`, guardrailError); triggered[name] = false; details[name] = { + checked_text: sample.data, error: guardrailError instanceof Error ? guardrailError.message : String(guardrailError), }; } @@ -130,7 +131,7 @@ export class AsyncRunEngine implements RunEngine { return await this.runPromptInjectionIncremental(context, guardrail, sampleData); } - return await guardrail.run(context as any, sampleData); + return await guardrail.run(context as GuardrailLLMContext, sampleData); } private isPromptInjectionGuardrail(guardrail: ConfiguredGuardrail): boolean { @@ -146,7 +147,7 @@ export class AsyncRunEngine implements RunEngine { guardrail: ConfiguredGuardrail, sampleData: string ): Promise { - const conversation = parseConversationInput(sampleData); + const conversation = parseConversationInput(sampleData) as ConversationMessage[]; if (conversation.length === 0) { const guardrailContext = this.createPromptInjectionContext(context, []); @@ -193,7 +194,7 @@ export class AsyncRunEngine implements RunEngine { private createPromptInjectionContext( context: Context, - conversationHistory: any[] + conversationHistory: ConversationMessage[] ): GuardrailLLMContextWithHistory { return { guardrailLlm: context.guardrailLlm, diff --git a/src/evals/core/jsonl-loader.ts b/src/evals/core/jsonl-loader.ts index 1c7990b..3e6e8c3 100644 --- a/src/evals/core/jsonl-loader.ts +++ b/src/evals/core/jsonl-loader.ts @@ -43,7 +43,6 @@ export class JsonlDatasetLoader implements DatasetLoader { */ async load(path: string): Promise { const fs = await import('fs/promises'); - const pathModule = await import('path'); if (!(await fs.stat(path).catch(() => false))) { throw new Error(`Dataset file not found: ${path}`); diff --git a/src/evals/core/types.ts b/src/evals/core/types.ts index b231d87..c95dbaa 100644 --- a/src/evals/core/types.ts +++ b/src/evals/core/types.ts @@ -45,7 +45,7 @@ export interface SampleResult { /** Mapping of guardrail names to actual trigger status. */ triggered: Record; /** Additional details for each guardrail (e.g., info, errors). */ - details: Record; + details: Record; } /** diff --git a/src/evals/guardrail-evals.ts b/src/evals/guardrail-evals.ts index ba4c723..8c3636a 100644 --- a/src/evals/guardrail-evals.ts +++ b/src/evals/guardrail-evals.ts @@ -5,7 +5,7 @@ * It loads guardrail configurations, runs evaluations asynchronously, calculates metrics, and saves results. */ -import { Context, Sample } from './core/types'; +import { Context } from './core/types'; import { JsonlDatasetLoader } from './core/jsonl-loader'; import { AsyncRunEngine } from './core/async-engine'; import { GuardrailMetricsCalculator } from './core/calculator'; diff --git a/src/registry.ts b/src/registry.ts index b3c962d..364fee7 100644 --- a/src/registry.ts +++ b/src/registry.ts @@ -8,7 +8,7 @@ */ import { z } from 'zod'; -import { CheckFn, TContext, TIn, TCfg } from './types'; +import { CheckFn, TextInput } from './types'; import { GuardrailSpec, GuardrailSpecMetadata } from './spec'; /** @@ -66,11 +66,11 @@ export class GuardrailRegistry { * @param ctxRequirements Optional Zod schema for context validation. * @param metadata Optional structured metadata. */ - register( + register( name: string, checkFn: CheckFn, description: string, - mediaType: string, + mediaType: string = 'text/plain', configSchema?: z.ZodType, ctxRequirements?: z.ZodType, metadata?: GuardrailSpecMetadata @@ -88,7 +88,7 @@ export class GuardrailRegistry { metadata ); - this.specs.set(name, spec); + this.specs.set(name, spec as unknown as GuardrailSpec); } /** diff --git a/src/resources/chat/chat.ts b/src/resources/chat/chat.ts index 7be01b6..eb718f9 100644 --- a/src/resources/chat/chat.ts +++ b/src/resources/chat/chat.ts @@ -1,10 +1,14 @@ -/* eslint-disable no-dupe-class-members */ /** * Chat completions with guardrails. */ +/* eslint-disable no-dupe-class-members */ import { OpenAI } from 'openai'; import { GuardrailsBaseClient, GuardrailsResponse } from '../../base-client'; +import { Message } from '../../types'; + +// Note: We need to filter out non-text content since guardrails only work with text +// The existing extractLatestUserTextMessage method expects TextOnlyMessageArray /** * Chat completions with guardrails. @@ -31,7 +35,7 @@ export class ChatCompletions { // Overload: streaming create( params: { - messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[]; + messages: Message[]; model: string; stream: true; suppressTripwire?: boolean; @@ -41,7 +45,7 @@ export class ChatCompletions { // Overload: non-streaming (default) create( params: { - messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[]; + messages: Message[]; model: string; stream?: false; suppressTripwire?: boolean; @@ -50,7 +54,7 @@ export class ChatCompletions { async create( params: { - messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[]; + messages: Message[]; model: string; stream?: boolean; suppressTripwire?: boolean; @@ -58,7 +62,8 @@ export class ChatCompletions { ): Promise | AsyncIterableIterator> { const { messages, model, stream = false, suppressTripwire = false, ...kwargs } = params; - const [latestMessage] = this.client.extractLatestUserMessage(messages); + // Extract latest user message text for guardrails (guardrails only work with text content) + const [latestMessage] = this.client.extractLatestUserTextMessage(messages); // Preflight first const preflightResults = await this.client.runStageGuardrails( @@ -84,6 +89,8 @@ export class ChatCompletions { suppressTripwire, this.client.raiseGuardrailErrors ), + // Access protected _resourceClient - necessary for external resource classes + // eslint-disable-next-line @typescript-eslint/no-explicit-any (this.client as any)._resourceClient.chat.completions.create({ messages: modifiedMessages, model, @@ -106,6 +113,8 @@ export class ChatCompletions { suppressTripwire ); } else { + // Access protected handleLlmResponse - necessary for external resource classes + // eslint-disable-next-line @typescript-eslint/no-explicit-any return (this.client as any).handleLlmResponse( llmResponse, preflightResults, diff --git a/src/resources/responses/responses.ts b/src/resources/responses/responses.ts index 468e4e5..6b9db62 100644 --- a/src/resources/responses/responses.ts +++ b/src/resources/responses/responses.ts @@ -1,9 +1,10 @@ /** * Responses API with guardrails. */ - +/* eslint-disable no-dupe-class-members */ import { OpenAI } from 'openai'; import { GuardrailsBaseClient, GuardrailsResponse } from '../../base-client'; +import { Message } from '../../types'; /** * Responses API with guardrails. @@ -19,7 +20,7 @@ export class Responses { // Overload: streaming create( params: { - input: string | unknown[]; + input: string | Message[]; model: string; stream: true; tools?: unknown[]; @@ -28,10 +29,9 @@ export class Responses { ): Promise>; // Overload: non-streaming (default) - /* eslint-disable no-dupe-class-members */ create( params: { - input: string | unknown[]; + input: string | Message[]; model: string; stream?: false; tools?: unknown[]; @@ -41,7 +41,7 @@ export class Responses { async create( params: { - input: string | unknown[]; + input: string | Message[]; model: string; stream?: boolean; tools?: unknown[]; @@ -53,32 +53,39 @@ export class Responses { // Determine latest user message text when a list of messages is provided let latestMessage: string; if (Array.isArray(input)) { - [latestMessage] = (this.client).extractLatestUserMessage(input); + [latestMessage] = this.client.extractLatestUserTextMessage(input); } else { latestMessage = input; } + // Extract conversation history for guardrail processing + const conversationHistory = Array.isArray(input) ? input : undefined; + // Preflight first (run checks on the latest user message text, with full conversation) const preflightResults = await this.client.runStageGuardrails( 'pre_flight', latestMessage, - Array.isArray(input) ? input : undefined, + conversationHistory, suppressTripwire, this.client.raiseGuardrailErrors ); // Apply pre-flight modifications (PII masking, etc.) - const modifiedInput = this.client.applyPreflightModifications(input, preflightResults); + const modifiedInput = this.client.applyPreflightModifications( + input, + preflightResults + ); // Input guardrails and LLM call concurrently const [inputResults, llmResponse] = await Promise.all([ this.client.runStageGuardrails( 'input', latestMessage, - Array.isArray(input) ? input : undefined, + conversationHistory, suppressTripwire, this.client.raiseGuardrailErrors ), + // eslint-disable-next-line @typescript-eslint/no-explicit-any (this.client as any)._resourceClient.responses.create({ input: modifiedInput, model, @@ -102,6 +109,7 @@ export class Responses { suppressTripwire ); } else { + // eslint-disable-next-line @typescript-eslint/no-explicit-any return (this.client as any).handleLlmResponse( llmResponse, preflightResults, diff --git a/src/runtime.ts b/src/runtime.ts index f558935..e48aa09 100644 --- a/src/runtime.ts +++ b/src/runtime.ts @@ -6,7 +6,7 @@ */ import { GuardrailSpec } from './spec'; -import { GuardrailResult, TContext, TIn, TCfg } from './types'; +import { GuardrailResult, TContext, TIn, TextInput } from './types'; import { defaultSpecRegistry } from './registry'; /** @@ -16,7 +16,7 @@ export interface GuardrailConfig { /** The registry name used to look up the guardrail spec. */ name: string; /** Configuration object for this guardrail instance. */ - config: Record; + config: Record; } /** @@ -48,7 +48,7 @@ export interface PipelineConfig { * object. The resulting instance is used to run guardrail logic in production * pipelines. It supports both sync and async check functions. */ -export class ConfiguredGuardrail { +export class ConfiguredGuardrail { constructor( public readonly definition: GuardrailSpec, public readonly config: TCfg @@ -64,9 +64,9 @@ export class ConfiguredGuardrail { * @param args Arguments for the check function. * @returns Promise resolving to the result of the check function. */ - private async ensureAsync( - fn: (...args: any[]) => GuardrailResult | Promise, - ...args: any[] + private async ensureAsync( + fn: (...args: T) => GuardrailResult | Promise, + ...args: T ): Promise { const result = fn(...args); if (result instanceof Promise) { @@ -200,9 +200,9 @@ export async function instantiateGuardrails( try { // Validate configuration against schema if available - let validatedConfig = guardrailConfig.config; + let validatedConfig: Record = guardrailConfig.config; if (spec.configSchema) { - validatedConfig = spec.configSchema.parse(guardrailConfig.config); + validatedConfig = spec.configSchema.parse(guardrailConfig.config) as Record; } const guardrail = spec.instantiate(validatedConfig); @@ -228,7 +228,7 @@ export function loadConfigBundle(jsonString: string): GuardrailBundle { const parsed = JSON.parse(jsonString); // Handle nested structure (input.guardrails) or direct structure (guardrails) - let guardrailsArray: any[] | undefined; + let guardrailsArray: unknown[] | undefined; if (parsed.guardrails && Array.isArray(parsed.guardrails)) { // Direct structure @@ -244,10 +244,11 @@ export function loadConfigBundle(jsonString: string): GuardrailBundle { // Validate each guardrail config for (const guardrail of guardrailsArray!) { - if (!guardrail.name || typeof guardrail.name !== 'string') { + const guardrailObj = guardrail as Record; + if (!guardrailObj.name || typeof guardrailObj.name !== 'string') { throw new Error('Invalid guardrail config: missing or invalid name'); } - if (!guardrail.config || typeof guardrail.config !== 'object') { + if (!guardrailObj.config || typeof guardrailObj.config !== 'object') { throw new Error('Invalid guardrail config: missing or invalid config object'); } } diff --git a/src/spec.ts b/src/spec.ts index 49afd8c..c16236d 100644 --- a/src/spec.ts +++ b/src/spec.ts @@ -8,7 +8,7 @@ */ import { z } from 'zod'; -import { CheckFn, TContext, TIn, TCfg } from './types'; +import { CheckFn, TextInput } from './types'; import { ConfiguredGuardrail } from './runtime'; /** @@ -20,8 +20,10 @@ import { ConfiguredGuardrail } from './runtime'; export interface GuardrailSpecMetadata { /** How the guardrail is implemented (regex/LLM/etc.) */ engine?: string; + /** Whether this guardrail requires conversation history to function properly */ + requiresConversationHistory?: boolean; /** Additional metadata fields */ - [key: string]: any; + [key: string]: unknown; } /** @@ -34,8 +36,11 @@ export interface GuardrailSpecMetadata { * GuardrailSpec instances are registered for cataloguing and introspection, * but should be instantiated with user configuration to create a runnable guardrail * for actual use. + * + * @warning The mediaType field determines which content types this guardrail can process. + * Only guardrails with compatible media types will be executed. Use 'text/plain' for text content. */ -export class GuardrailSpec { +export class GuardrailSpec { constructor( public readonly name: string, public readonly description: string, @@ -54,8 +59,8 @@ export class GuardrailSpec { * * @returns JSON schema describing the config model fields. */ - schema(): Record { - return this.configSchema._def; + schema(): Record { + return this.configSchema._def as Record; } /** diff --git a/src/streaming.ts b/src/streaming.ts index 1f55ba4..d4f8c07 100644 --- a/src/streaming.ts +++ b/src/streaming.ts @@ -5,8 +5,8 @@ * with periodic guardrail checks. */ -import { GuardrailResult } from './types'; -import { GuardrailsResponse, GuardrailsBaseClient } from './base-client'; +import { GuardrailResult, TextOnlyMessageArray } from './types'; +import { GuardrailsResponse, GuardrailsBaseClient, OpenAIResponseType } from './base-client'; import { GuardrailTripwireTriggered } from './exceptions'; /** @@ -18,10 +18,10 @@ export class StreamingMixin { */ async *streamWithGuardrails( this: GuardrailsBaseClient, - llmStream: AsyncIterable, + llmStream: AsyncIterable, preflightResults: GuardrailResult[], inputResults: GuardrailResult[], - conversationHistory?: any[], + conversationHistory?: TextOnlyMessageArray, checkInterval: number = 100, suppressTripwire: boolean = false ): AsyncIterableIterator { @@ -30,7 +30,7 @@ export class StreamingMixin { for await (const chunk of llmStream) { // Extract text from chunk - const chunkText = (this as any).extractResponseText(chunk); + const chunkText = this.extractResponseText(chunk as OpenAIResponseType); if (chunkText) { accumulatedText += chunkText; chunkCount++; @@ -38,17 +38,17 @@ export class StreamingMixin { // Run output guardrails periodically if (chunkCount % checkInterval === 0) { try { - await (this as any).runStageGuardrails( + await this.runStageGuardrails( 'output', accumulatedText, - conversationHistory, + conversationHistory as TextOnlyMessageArray, suppressTripwire ); } catch (error) { if (error instanceof GuardrailTripwireTriggered) { // Create a final response with the error - const finalResponse = (this as any).createGuardrailsResponse( - chunk, + const finalResponse = this.createGuardrailsResponse( + chunk as OpenAIResponseType, preflightResults, inputResults, [error.guardrailResult] @@ -62,8 +62,8 @@ export class StreamingMixin { } // Yield the chunk wrapped in GuardrailsResponse - const response = (this as any).createGuardrailsResponse( - chunk, + const response = this.createGuardrailsResponse( + chunk as OpenAIResponseType, preflightResults, inputResults, [] // No output results yet for streaming chunks @@ -74,16 +74,16 @@ export class StreamingMixin { // Final guardrail check on complete text if (!suppressTripwire && accumulatedText) { try { - const finalOutputResults = await (this as any).runStageGuardrails( + const finalOutputResults = await this.runStageGuardrails( 'output', accumulatedText, - conversationHistory, + conversationHistory as TextOnlyMessageArray, suppressTripwire ); // Create a final response with all results - const finalResponse = (this as any).createGuardrailsResponse( - { type: 'final', accumulated_text: accumulatedText }, + const finalResponse = this.createGuardrailsResponse( + { type: 'final', accumulated_text: accumulatedText } as unknown as OpenAIResponseType, preflightResults, inputResults, finalOutputResults @@ -92,8 +92,8 @@ export class StreamingMixin { } catch (error) { if (error instanceof GuardrailTripwireTriggered) { // Create a final response with the error - const finalResponse = (this as any).createGuardrailsResponse( - { type: 'final', accumulated_text: accumulatedText }, + const finalResponse = this.createGuardrailsResponse( + { type: 'final', accumulated_text: accumulatedText } as unknown as OpenAIResponseType, preflightResults, inputResults, [error.guardrailResult] @@ -111,10 +111,10 @@ export class StreamingMixin { */ static streamWithGuardrailsSync( client: GuardrailsBaseClient, - llmStream: AsyncIterable, + llmStream: AsyncIterable, preflightResults: GuardrailResult[], inputResults: GuardrailResult[], - conversationHistory?: any[], + conversationHistory?: TextOnlyMessageArray, suppressTripwire: boolean = false ): AsyncIterableIterator { const streamingMixin = new StreamingMixin(); diff --git a/src/types.ts b/src/types.ts index 5bd5a9d..693a201 100644 --- a/src/types.ts +++ b/src/types.ts @@ -19,6 +19,18 @@ export interface GuardrailLLMContext { guardrailLlm: OpenAI; } +/** + * Extended message type for conversation handling that includes additional properties + * not present in the base Message type. + */ +export interface ConversationMessage extends Message { + type?: string; + tool_calls?: unknown[]; + text?: string; + value?: string; + [key: string]: unknown; +} + /** * Extended context interface for guardrails that need conversation history. * @@ -28,7 +40,7 @@ export interface GuardrailLLMContext { */ export interface GuardrailLLMContextWithHistory extends GuardrailLLMContext { /** Get the full conversation history */ - getConversationHistory(): any[]; + getConversationHistory(): ConversationMessage[]; } /** @@ -51,8 +63,16 @@ export interface GuardrailResult { info: { /** The processed/checked text that should be used if modifications were made */ checked_text: string; + /** The media type this guardrail was designed for */ + media_type?: string; + /** The detected content type of the input data */ + detected_content_type?: string; + /** The stage where this guardrail was executed (pre_flight, input, output) */ + stage_name?: string; + /** The name of the guardrail that produced this result */ + guardrail_name?: string; /** Additional guardrail-specific metadata */ - [key: string]: any; + [key: string]: unknown; }; } @@ -62,7 +82,7 @@ export interface GuardrailResult { * A guardrail function accepts a context object, input data, and a configuration object, * returning either a `GuardrailResult` or a Promise resolving to `GuardrailResult`. */ -export type CheckFn = ( +export type CheckFn = ( ctx: TContext, input: TIn, config: TCfg @@ -78,9 +98,63 @@ export type MaybeAwaitableResult = GuardrailResult | Promise; * * These provide sensible defaults while allowing for more specific types: * - TContext: object (any object, including interfaces) - * - TIn: unknown (any input type, most flexible) + * - TIn: TextInput (string input type for guardrails) // Future: Union type for different input types * - TCfg: object (any object, including interfaces and classes) */ export type TContext = object; -export type TIn = unknown; +export type TIn = TextInput; export type TCfg = object; + +/** + * Core message structure - clear and extensible. + */ +export type Message = { + role: string; + content: string | ContentPart[]; +}; + +/** + * Content part structure - clear and extensible. + */ +export type ContentPart = { + type: string; + [key: string]: unknown; +}; + +/** + * Text content part for structured content (Responses API). + */ +export type TextContentPart = ContentPart & { + type: 'input_text' | 'text' | 'output_text' | 'summary_text'; + text: string; +}; + + +/** + * Type alias for text-only input to guardrails. + * + * Currently represents string input for text-based guardrails. In the future, + * this may be extended to support multi-modal content types (images, audio, video) + * through a union type or more sophisticated content representation. + */ +export type TextInput = string; + +/** + * Text-only content types for guardrails. + * These types enforce that only text content is processed. + */ + +/** Plain text content */ +export type TextContent = string; + +/** Union type for all text-only content */ +export type TextOnlyContent = TextContent | TextContentPart[]; + +/** Message with text-only content */ +export type TextOnlyMessage = { + role: string; + content: TextOnlyContent; +}; + +/** Array of text-only messages */ +export type TextOnlyMessageArray = TextOnlyMessage[]; diff --git a/src/utils/content.ts b/src/utils/content.ts new file mode 100644 index 0000000..a5e74ad --- /dev/null +++ b/src/utils/content.ts @@ -0,0 +1,70 @@ +/** + * Content processing utilities for guardrails. + * + * Provides centralized logic for content type detection, text extraction, + * and message filtering for guardrail processing. + */ + +import { Message, ContentPart, TextContentPart, TextOnlyMessageArray } from '../types'; + +export class ContentUtils { + // Clear: what types are considered text + private static readonly TEXT_TYPES = ['input_text', 'text', 'output_text', 'summary_text'] as const; + + /** + * Check if a content part is text-based. + */ + static isText(part: ContentPart): boolean { + return this.TEXT_TYPES.includes(part.type as typeof this.TEXT_TYPES[number]); + } + + /** + * Extract text from a message. + */ + static extractTextFromMessage(message: Message): string { + if (typeof message.content === 'string') { + return message.content.trim(); + } + + if (Array.isArray(message.content)) { + return message.content + .filter(part => this.isText(part)) + .map(part => (part as TextContentPart).text) + .join(' ') + .trim(); + } + + return ''; + } + + /** + * Filter messages to text-only (for guardrails). + * + * Guardrails only work with text content, so this filters out + * messages that don't contain any text parts. + */ + static filterToTextOnly(messages: Message[]): TextOnlyMessageArray { + return messages + .filter(msg => this.hasTextContent(msg)) + .map(msg => ({ + role: msg.role, + content: msg.content as string | TextContentPart[] + })); + } + + /** + * Check if a message has text content. + */ + private static hasTextContent(message: Message): boolean { + if (typeof message.content === 'string') { + return true; + } + + if (Array.isArray(message.content)) { + return message.content.some(part => this.isText(part)); + } + + return false; + } + +} diff --git a/src/utils/context.ts b/src/utils/context.ts index e8b05ab..989d4aa 100644 --- a/src/utils/context.ts +++ b/src/utils/context.ts @@ -5,7 +5,6 @@ */ import { GuardrailError } from '../exceptions'; -import { TContext, TIn } from '../types'; /** * Error thrown when context validation fails. @@ -25,8 +24,8 @@ export class ContextValidationError extends GuardrailError { * @throws {ContextValidationError} If ctx does not satisfy required fields. * @throws {TypeError} If ctx's attributes cannot be introspected. */ -export function validateGuardrailContext( - guardrail: { definition: { name: string; ctxRequirements: any } }, +export function validateGuardrailContext( + guardrail: { definition: { name: string; ctxRequirements: unknown } }, ctx: TContext ): void { const model = guardrail.definition.ctxRequirements; @@ -52,16 +51,16 @@ export function validateGuardrailContext( } // Attempt to get application context schema for better error message - let appCtxFields: Record = {}; + let appCtxFields: Record = {}; try { appCtxFields = Object.getOwnPropertyNames(ctx).reduce( (acc, prop) => { - acc[prop] = typeof (ctx as any)[prop]; + acc[prop] = typeof (ctx as Record)[prop]; return acc; }, - {} as Record + {} as Record ); - } catch (exc) { + } catch { const msg = `Context must support property access, please pass Context as a class instead of '${typeof ctx}'.`; throw new ContextValidationError(msg); } diff --git a/src/utils/conversation.ts b/src/utils/conversation.ts index 3964e39..d995cde 100644 --- a/src/utils/conversation.ts +++ b/src/utils/conversation.ts @@ -16,7 +16,7 @@ const POSSIBLE_CONVERSATION_KEYS = [ * Accepts raw JSON strings, arrays, or objects that embed conversation arrays under * several common keys. Returns an empty array when no conversation data is found. */ -export function parseConversationInput(rawInput: unknown): any[] { +export function parseConversationInput(rawInput: unknown): unknown[] { if (Array.isArray(rawInput)) { return rawInput; } diff --git a/src/utils/openai-vector-store.ts b/src/utils/openai-vector-store.ts index 916db55..cc7dcfe 100644 --- a/src/utils/openai-vector-store.ts +++ b/src/utils/openai-vector-store.ts @@ -184,7 +184,8 @@ async function uploadFiles(client: OpenAI, filePaths: string[]): Promise { - while (true) { + let completed = false; + while (!completed) { const allCompleted = await Promise.all( fileIds.map(async (fileId) => { try { @@ -197,7 +198,7 @@ async function waitForFileProcessing(client: OpenAI, fileIds: string[]): Promise ); if (allCompleted.every((status) => status)) { - return; + completed = true; } // Wait 1 second before checking again diff --git a/src/utils/output.ts b/src/utils/output.ts index f0de377..2783a6c 100644 --- a/src/utils/output.ts +++ b/src/utils/output.ts @@ -18,7 +18,7 @@ const _WRAPPER_DICT_KEY = 'response'; */ export class OutputSchema { /** The type of the output. */ - private outputType: any; + private outputType: unknown; /** Whether the output type is wrapped in a dictionary. */ private isWrapped: boolean; @@ -35,7 +35,7 @@ export class OutputSchema { * @param outputType - The target TypeScript type of the LLM output. * @param strictJsonSchema - Whether to enforce strict JSON schema generation. */ - constructor(outputType: any, strictJsonSchema: boolean = true) { + constructor(outputType: unknown, strictJsonSchema: boolean = true) { this.outputType = outputType; this.strictJsonSchema = strictJsonSchema; @@ -89,7 +89,7 @@ export class OutputSchema { * @param partial - Whether to allow partial JSON parsing. * @returns The validated object. */ - validateJson(jsonStr: string, partial: boolean = false): unknown { + validateJson(jsonStr: string): unknown { const validated = validateJson(jsonStr, this.outputSchema); if (this.isWrapped) { @@ -117,7 +117,7 @@ export class OutputSchema { * @param type - The type to generate a schema for. * @returns The JSON schema. */ - private generateJsonSchema(type: any): Record { + private generateJsonSchema(type: unknown): Record { // This is a basic implementation - you might want to use a proper schema generator if (type === String || type === 'string') { return { type: 'string' }; @@ -171,7 +171,7 @@ export class OutputSchema { * @param type - The type to check. * @returns True if the type is a subclass of BaseModel or dict. */ - private isSubclassOfBaseModelOrDict(type: any): boolean { + private isSubclassOfBaseModelOrDict(type: unknown): boolean { // In TypeScript, we'll use a simplified check // In a full implementation, you might want to check for specific base classes return ( @@ -190,7 +190,7 @@ export class OutputSchema { * @returns An OutputSchema instance. */ export function createOutputSchema( - outputType: any, + outputType: unknown, strictJsonSchema: boolean = true ): OutputSchema { return new OutputSchema(outputType, strictJsonSchema); @@ -202,7 +202,7 @@ export function createOutputSchema( * @param type - The type to check. * @returns True if the type can be represented as a JSON Schema object. */ -export function canRepresentAsJsonSchemaObject(type: any): boolean { +export function canRepresentAsJsonSchemaObject(type: unknown): boolean { if (type === null || type === undefined || type === String) { return false; } diff --git a/src/utils/parsing.ts b/src/utils/parsing.ts index 1aee5dc..b362ea0 100644 --- a/src/utils/parsing.ts +++ b/src/utils/parsing.ts @@ -7,6 +7,9 @@ * - formatEntries: render entries as JSON or plain text. */ +import { OpenAI } from 'openai'; +import { TextOnlyMessage } from '../types'; + /** * Parsed text entry with role metadata. */ @@ -18,27 +21,23 @@ export interface Entry { } /** - * Type alias for OpenAI response types. + * Type aliases for OpenAI response types. */ -export type TResponse = any; -export type TResponseInputItem = any; -export type TResponseOutputItem = any; -export type TResponseStreamEvent = any; +export type TResponse = + | OpenAI.Completions.Completion + | OpenAI.Chat.Completions.ChatCompletion + | OpenAI.Chat.Completions.ChatCompletionChunk + | OpenAI.Responses.Response; + +export type TResponseInputItem = OpenAI.Chat.Completions.ChatCompletionMessageParam; +export type TResponseOutputItem = OpenAI.Chat.Completions.ChatCompletionMessage; +export type TResponseStreamEvent = OpenAI.Chat.Completions.ChatCompletionChunk; -/** - * Convert an object to a mapping or pass through if it's already a mapping. - */ -function toMapping(item: any): Record | null { - if (item && typeof item === 'object' && !Array.isArray(item)) { - return item; - } - return null; -} /** * Parse both input and output messages (type='message'). */ -function parseMessage(item: Record): Entry[] { +function parseMessage(item: TextOnlyMessage): Entry[] { const role = item.role; const contents = item.content; @@ -48,58 +47,20 @@ function parseMessage(item: Record): Entry[] { const parts: string[] = []; if (Array.isArray(contents)) { + // Handle mixed content types (objects and strings) for (const part of contents) { - if (typeof part === 'object' && part !== null) { - if (part.type === 'input_text' || part.type === 'output_text') { - parts.push(part.text || ''); - } else if (typeof part === 'string') { - parts.push(part); - } else { - console.warn('Unknown message part:', part); - } - } else if (typeof part === 'string') { + if (typeof part === 'string') { parts.push(part); + } else if (typeof part === 'object' && part !== null && 'text' in part) { + parts.push(part.text); } + // Skip unknown object types (like { type: 'unknown', foo: 'bar' }) } } return [{ role, content: parts.join('') }]; } -/** - * Generate handler for single-string fields. - */ -function scalarHandler(role: string, key: string): (item: Record) => Entry[] { - return (item: Record): Entry[] => { - const val = item[key]; - return typeof val === 'string' ? [{ role, content: val }] : []; - }; -} - -/** - * Generate handler for list fields. - */ -function listHandler( - role: string, - listKey: string, - textKey: string -): (item: Record) => Entry[] { - return (item: Record): Entry[] => { - const list = item[listKey]; - if (!Array.isArray(list)) return []; - - const entries: Entry[] = []; - for (const listItem of list) { - if (typeof listItem === 'object' && listItem !== null) { - const text = listItem[textKey]; - if (typeof text === 'string') { - entries.push({ role, content: text }); - } - } - } - return entries; - }; -} /** * Parse response items into Entry objects. @@ -119,10 +80,13 @@ export function parseResponseItems( } // Handle different response types - if (response.choices && Array.isArray(response.choices)) { + if ('choices' in response && response.choices && Array.isArray(response.choices)) { for (const choice of response.choices) { - if (choice.message) { - const messageEntries = parseMessage(choice.message); + if ('message' in choice && choice.message && choice.message.content) { + const messageEntries = parseMessage({ + role: choice.message.role, + content: choice.message.content + }); entries.push(...messageEntries); } } @@ -183,13 +147,7 @@ export function formatEntriesAsText(entries: Entry[]): string { */ export function formatEntries( entries: Entry[], - format: 'json' | 'text' = 'text', - options: { - indent?: number; - filterRole?: string; - lastN?: number; - separator?: string; - } = {} + format: 'json' | 'text' = 'text' ): string { switch (format) { case 'json': @@ -217,7 +175,7 @@ export function extractTextContent(response: TResponse): string { * @param response - The response to extract JSON from. * @returns Extracted JSON content or null if parsing fails. */ -export function extractJsonContent(response: TResponse): any { +export function extractJsonContent(response: TResponse): Record | null { const entries = parseResponseItemsAsJson(response); if (entries.length === 0) return null; diff --git a/src/utils/schema.ts b/src/utils/schema.ts index 847247d..82353b9 100644 --- a/src/utils/schema.ts +++ b/src/utils/schema.ts @@ -144,17 +144,17 @@ export function resolveRef( const ref = schema.$ref; if (typeof ref === 'string' && ref.startsWith('#/')) { const path = ref.substring(2).split('/'); - let current: any = root; + let current: unknown = root; for (const segment of path) { if (current && typeof current === 'object' && segment in current) { - current = current[segment]; + current = (current as Record)[segment]; } else { throw new Error(`Invalid $ref path: ${ref}`); } } - return resolveRef(current, root); + return resolveRef(current as Record, root); } // Recursively resolve refs in nested objects @@ -184,8 +184,7 @@ export function resolveRef( */ export function validateJson( jsonStr: string, - schema: Record, - partial: boolean = false + schema: Record ): unknown { try { const parsed = JSON.parse(jsonStr); diff --git a/src/utils/vector-store.ts b/src/utils/vector-store.ts index c127ba0..2a1c585 100644 --- a/src/utils/vector-store.ts +++ b/src/utils/vector-store.ts @@ -12,7 +12,7 @@ export interface VectorStoreConfig { /** The type of vector store to create. */ type: 'memory' | 'pinecone' | 'weaviate' | 'chroma'; /** Configuration specific to the vector store type. */ - config: Record; + config: Record; /** Whether to create the store in read-only mode. */ readOnly?: boolean; } @@ -40,7 +40,7 @@ export interface Document { /** Text content of the document. */ content: string; /** Optional metadata for the document. */ - metadata?: Record; + metadata?: Record; /** Optional embedding vector. */ embedding?: number[]; } @@ -83,7 +83,7 @@ class MemoryVectorStore implements VectorStore { private documents: Map = new Map(); private embeddings: Map = new Map(); - constructor(private config: Record) {} + constructor(private config: Record) {} async addDocuments(documents: Document[]): Promise { for (const doc of documents) { @@ -144,61 +144,61 @@ class MemoryVectorStore implements VectorStore { * Placeholder implementations for other vector store types. */ class PineconeVectorStore implements VectorStore { - constructor(private config: Record) {} + constructor(private config: Record) {} - async addDocuments(documents: Document[]): Promise { + async addDocuments(_documents: Document[]): Promise { throw new Error('Pinecone vector store not implemented'); } - async search(query: string, limit?: number): Promise { + async search(_query: string, _limit?: number): Promise { throw new Error('Pinecone vector store not implemented'); } - async deleteDocuments(documentIds: string[]): Promise { + async deleteDocuments(_documentIds: string[]): Promise { throw new Error('Pinecone vector store not implemented'); } - async getDocument(id: string): Promise { + async getDocument(_id: string): Promise { throw new Error('Pinecone vector store not implemented'); } } class WeaviateVectorStore implements VectorStore { - constructor(private config: Record) {} + constructor(private config: Record) {} - async addDocuments(documents: Document[]): Promise { + async addDocuments(_documents: Document[]): Promise { throw new Error('Weaviate vector store not implemented'); } - async search(query: string, limit?: number): Promise { + async search(_query: string, _limit?: number): Promise { throw new Error('Weaviate vector store not implemented'); } - async deleteDocuments(documentIds: string[]): Promise { + async deleteDocuments(_documentIds: string[]): Promise { throw new Error('Weaviate vector store not implemented'); } - async getDocument(id: string): Promise { + async getDocument(_id: string): Promise { throw new Error('Weaviate vector store not implemented'); } } class ChromaVectorStore implements VectorStore { - constructor(private config: Record) {} + constructor(private config: Record) {} - async addDocuments(documents: Document[]): Promise { + async addDocuments(_documents: Document[]): Promise { throw new Error('Chroma vector store not implemented'); } - async search(query: string, limit?: number): Promise { + async search(_query: string, _limit?: number): Promise { throw new Error('Chroma vector store not implemented'); } - async deleteDocuments(documentIds: string[]): Promise { + async deleteDocuments(_documentIds: string[]): Promise { throw new Error('Chroma vector store not implemented'); } - async getDocument(id: string): Promise { + async getDocument(_id: string): Promise { throw new Error('Chroma vector store not implemented'); } }