mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
feat(knowledge): expose Cohere reranker controls (#4429)
* feat(knowledge): expose Cohere reranker controls on knowledge block Add a self-hosted Cohere API key field (mirroring the agent block's hosted-key pattern), a configurable reranker input pool size (1-100), and surface meta.warnings from Cohere rerank responses via logger.warn. All new contract fields are optional and nullable for full backwards compatibility. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(knowledge): address PR feedback on Cohere reranker controls - Drop required:true on apiKey field — server has BYOK→env→rotation fallback chain, so self-hosted users with COHERE_API_KEY env should not be blocked - Drop .min(1) on rerankerApiKey contract field so empty strings coerce to undefined via the transform (matches the existing query field pattern) - Log a warning when rerankerInputCount is clamped up to topK so users notice their setting was overridden Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(knowledge): mirror agent block API key visibility for Cohere reranker Restore required:true on the Cohere API Key field and hide it server-side via a new NEXT_PUBLIC_COHERE_CONFIGURED public env flag — same pattern the Agent block uses for Azure (NEXT_PUBLIC_AZURE_CONFIGURED). Self-hosters who set COHERE_API_KEY in their environment also set NEXT_PUBLIC_COHERE_CONFIGURED=true, which removes the field from the UI; everyone else sees a required field. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(knowledge): treat empty rerankerInputCount as unset An empty string from the Documents Sent to Reranker input passed the undefined/null guard, so Number('') = 0 → clamped to 1, sending only 1 document to the reranker instead of falling back to the 4× topK auto default. Add the empty-string check to the guard. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
5d53847c2e
commit
57dc745bab
@@ -35,6 +35,8 @@ API_ENCRYPTION_KEY=your_api_encryption_key # Use `openssl rand -hex 32` to gener
|
||||
# AZURE_ANTHROPIC_API_KEY= # Azure Anthropic API key
|
||||
# AZURE_ANTHROPIC_API_VERSION= # Azure Anthropic API version (e.g., 2023-06-01)
|
||||
# NEXT_PUBLIC_AZURE_CONFIGURED=true # Set when Azure credentials are pre-configured above. Hides endpoint/key/version fields in Agent block UI.
|
||||
# COHERE_API_KEY= # Cohere API key for the Knowledge block reranker (rerank-v4.0-pro/-fast, rerank-v3.5). Alternatively set COHERE_API_KEY_1/2/3 for rotation.
|
||||
# NEXT_PUBLIC_COHERE_CONFIGURED=true # Set when COHERE_API_KEY (or rotation keys) are pre-configured above. Hides the Cohere API Key field on the Knowledge block UI.
|
||||
|
||||
# Admin API (Optional - for self-hosted GitOps)
|
||||
# ADMIN_API_KEY= # Use `openssl rand -hex 32` to generate. Enables admin API for workflow export/import.
|
||||
|
||||
@@ -247,9 +247,21 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
|
||||
const hasFilters = structuredFilters && structuredFilters.length > 0
|
||||
|
||||
/** Oversample candidates when reranking so the reranker has more to choose from.
|
||||
* Cap at 100 to bound Cohere request cost (1 search unit = ≤100 docs). */
|
||||
const candidateTopK = useReranker ? Math.min(100, validatedData.topK * 4) : validatedData.topK
|
||||
/** Oversample vector results when reranking so the reranker has more to choose from.
|
||||
* Cap at 100 to bound Cohere request cost (1 search unit = ≤100 docs). When the caller
|
||||
* supplies `rerankerInputCount`, honor it but never let it drop below `topK`
|
||||
* (which would defeat the purpose) or exceed 100 (which would split into >1 search units). */
|
||||
const rawInputCount = validatedData.rerankerInputCount
|
||||
if (useReranker && rawInputCount !== undefined && rawInputCount < validatedData.topK) {
|
||||
logger.warn(
|
||||
`[${requestId}] rerankerInputCount (${rawInputCount}) is below topK (${validatedData.topK}); raising to topK`
|
||||
)
|
||||
}
|
||||
const candidateTopK = useReranker
|
||||
? rawInputCount !== undefined
|
||||
? Math.min(100, Math.max(validatedData.topK, rawInputCount))
|
||||
: Math.min(100, validatedData.topK * 4)
|
||||
: validatedData.topK
|
||||
|
||||
if (!hasQuery && hasFilters) {
|
||||
results = await handleTagOnlySearch({
|
||||
@@ -300,7 +312,12 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const { results: ranked, isBYOK } = await rerank(
|
||||
validatedData.query!,
|
||||
results.map((r) => ({ id: r.id, text: r.content })),
|
||||
{ model: rerankerModel, topN: validatedData.topK, workspaceId }
|
||||
{
|
||||
model: rerankerModel,
|
||||
topN: validatedData.topK,
|
||||
workspaceId,
|
||||
apiKey: validatedData.rerankerApiKey,
|
||||
}
|
||||
)
|
||||
rerankBilled = true
|
||||
rerankIsBYOK = isBYOK
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { PackageSearchIcon } from '@/components/icons'
|
||||
import { DEFAULT_RERANKER_MODEL, SUPPORTED_RERANKER_MODELS } from '@/lib/knowledge/reranker-models'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { getCohereRerankerApiKeyCondition } from '@/blocks/utils'
|
||||
|
||||
export const KnowledgeBlock: BlockConfig = {
|
||||
type: 'knowledge',
|
||||
@@ -105,6 +106,28 @@ export const KnowledgeBlock: BlockConfig = {
|
||||
and: { field: 'rerankerEnabled', value: true },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'rerankerInputCount',
|
||||
title: 'Documents Sent to Reranker',
|
||||
type: 'short-input',
|
||||
placeholder: 'Auto (4× results, capped at 100)',
|
||||
mode: 'advanced',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'search',
|
||||
and: { field: 'rerankerEnabled', value: true },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'apiKey',
|
||||
title: 'Cohere API Key',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter your Cohere API key',
|
||||
password: true,
|
||||
connectionDroppable: false,
|
||||
required: true,
|
||||
condition: getCohereRerankerApiKeyCondition(),
|
||||
},
|
||||
|
||||
// --- List Documents ---
|
||||
{
|
||||
@@ -419,6 +442,11 @@ export const KnowledgeBlock: BlockConfig = {
|
||||
tagFilters: { type: 'string', description: 'Tag filter criteria' },
|
||||
rerankerEnabled: { type: 'boolean', description: 'Apply Cohere reranking to search results' },
|
||||
rerankerModel: { type: 'string', description: 'Cohere rerank model identifier' },
|
||||
rerankerInputCount: {
|
||||
type: 'number',
|
||||
description: 'Number of vector results sent to the Cohere reranker (1–100)',
|
||||
},
|
||||
apiKey: { type: 'string', description: 'Cohere API key (self-hosted only)' },
|
||||
documentTags: { type: 'string', description: 'Document tags' },
|
||||
chunkSearch: { type: 'string', description: 'Search filter for chunks' },
|
||||
chunkEnabledFilter: { type: 'string', description: 'Filter chunks by enabled status' },
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { isAzureConfigured, isHosted, isOllamaConfigured } from '@/lib/core/config/feature-flags'
|
||||
import {
|
||||
isAzureConfigured,
|
||||
isCohereConfigured,
|
||||
isHosted,
|
||||
isOllamaConfigured,
|
||||
} from '@/lib/core/config/feature-flags'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import { buildCanonicalIndex } from '@/lib/workflows/subblocks/visibility'
|
||||
import type { BlockOutput, OutputFieldDefinition, SubBlockConfig } from '@/blocks/types'
|
||||
@@ -184,6 +189,27 @@ export function getApiKeyCondition() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Visibility condition for the Cohere reranker API key field on the Knowledge block.
|
||||
* Hidden on hosted Sim (platform supplies the key via workspace BYOK or rotating env keys)
|
||||
* and on self-hosted deployments that have set `NEXT_PUBLIC_COHERE_CONFIGURED=true` to
|
||||
* indicate `COHERE_API_KEY` is pre-configured server-side. Otherwise shown (and required)
|
||||
* whenever reranking is enabled for a search operation, mirroring the agent block's
|
||||
* `getApiKeyCondition` pattern.
|
||||
*/
|
||||
export function getCohereRerankerApiKeyCondition() {
|
||||
return () => {
|
||||
if (isHosted || isCohereConfigured) {
|
||||
return { field: 'operation', value: '__never_show__' }
|
||||
}
|
||||
return {
|
||||
field: 'operation',
|
||||
value: 'search',
|
||||
and: { field: 'rerankerEnabled', value: true },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the standard provider credential subblocks used by LLM-based blocks.
|
||||
* This includes: Vertex AI OAuth, API Key, Azure (OpenAI + Anthropic), Vertex AI config, and Bedrock config.
|
||||
|
||||
@@ -36,6 +36,24 @@ export const knowledgeSearchBodySchema = z
|
||||
.transform((val) => val || undefined),
|
||||
rerankerEnabled: z.boolean().optional().default(false),
|
||||
rerankerModel: rerankerModelSchema.optional().default(DEFAULT_RERANKER_MODEL),
|
||||
/**
|
||||
* Number of vector results sent to Cohere as the documents array for reranking. Capped at 100
|
||||
* so each rerank call stays within a single Cohere search unit (1 query × ≤100 docs); see
|
||||
* `RERANK_MODEL_PRICING` in `providers/models.ts`.
|
||||
*/
|
||||
rerankerInputCount: z
|
||||
.number()
|
||||
.int('rerankerInputCount must be an integer')
|
||||
.min(1, 'rerankerInputCount must be at least 1')
|
||||
.max(100, 'rerankerInputCount cannot exceed 100')
|
||||
.optional()
|
||||
.nullable()
|
||||
.transform((val) => val ?? undefined),
|
||||
rerankerApiKey: z
|
||||
.string()
|
||||
.optional()
|
||||
.nullable()
|
||||
.transform((val) => val || undefined),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
|
||||
@@ -430,6 +430,7 @@ export const env = createEnv({
|
||||
NEXT_PUBLIC_E2B_ENABLED: z.string().optional(),
|
||||
NEXT_PUBLIC_BEDROCK_DEFAULT_CREDENTIALS: z.string().optional(), // Hide Bedrock credential fields when deployment uses AWS default credential chain (IAM roles, instance profiles, ECS task roles, IRSA)
|
||||
NEXT_PUBLIC_AZURE_CONFIGURED: z.string().optional(), // Hide Azure credential fields when endpoint/key/version are pre-configured server-side
|
||||
NEXT_PUBLIC_COHERE_CONFIGURED: z.string().optional(), // Hide Cohere API key field on Knowledge block when COHERE_API_KEY is pre-configured server-side
|
||||
NEXT_PUBLIC_COPILOT_TRAINING_ENABLED: z.string().optional(),
|
||||
NEXT_PUBLIC_ENABLE_PLAYGROUND: z.string().optional(), // Enable component playground at /playground
|
||||
NEXT_PUBLIC_DOCUMENTATION_URL: z.string().url().optional(), // Custom documentation URL
|
||||
@@ -496,6 +497,7 @@ export const env = createEnv({
|
||||
NEXT_PUBLIC_E2B_ENABLED: process.env.NEXT_PUBLIC_E2B_ENABLED,
|
||||
NEXT_PUBLIC_BEDROCK_DEFAULT_CREDENTIALS: process.env.NEXT_PUBLIC_BEDROCK_DEFAULT_CREDENTIALS,
|
||||
NEXT_PUBLIC_AZURE_CONFIGURED: process.env.NEXT_PUBLIC_AZURE_CONFIGURED,
|
||||
NEXT_PUBLIC_COHERE_CONFIGURED: process.env.NEXT_PUBLIC_COHERE_CONFIGURED,
|
||||
NEXT_PUBLIC_COPILOT_TRAINING_ENABLED: process.env.NEXT_PUBLIC_COPILOT_TRAINING_ENABLED,
|
||||
NEXT_PUBLIC_ENABLE_PLAYGROUND: process.env.NEXT_PUBLIC_ENABLE_PLAYGROUND,
|
||||
NEXT_PUBLIC_POSTHOG_ENABLED: process.env.NEXT_PUBLIC_POSTHOG_ENABLED,
|
||||
|
||||
@@ -156,6 +156,14 @@ export const isOllamaConfigured = Boolean(env.OLLAMA_URL)
|
||||
*/
|
||||
export const isAzureConfigured = isTruthy(getEnv('NEXT_PUBLIC_AZURE_CONFIGURED'))
|
||||
|
||||
/**
|
||||
* Whether a Cohere API key is pre-configured server-side for the Knowledge block reranker
|
||||
* (`COHERE_API_KEY` or `COHERE_API_KEY_1/2/3`). When true, the Cohere API Key field is hidden
|
||||
* in the Knowledge block UI.
|
||||
* Set NEXT_PUBLIC_COHERE_CONFIGURED=true in self-hosted deployments that ship a Cohere key.
|
||||
*/
|
||||
export const isCohereConfigured = isTruthy(getEnv('NEXT_PUBLIC_COHERE_CONFIGURED'))
|
||||
|
||||
/**
|
||||
* Are invitations disabled globally
|
||||
* When true, workspace invitations are disabled for all users
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
|
||||
import { getBYOKKey } from '@/lib/api-key/byok'
|
||||
import { getRotatingApiKey } from '@/lib/core/config/api-keys'
|
||||
import { env } from '@/lib/core/config/env'
|
||||
import { isHosted } from '@/lib/core/config/feature-flags'
|
||||
import { isRetryableError, retryWithExponentialBackoff } from '@/lib/knowledge/documents/utils'
|
||||
import {
|
||||
DEFAULT_RERANKER_MODEL,
|
||||
@@ -56,8 +57,18 @@ class RerankAPIError extends Error {
|
||||
}
|
||||
|
||||
async function resolveCohereKey(
|
||||
workspaceId?: string | null
|
||||
workspaceId?: string | null,
|
||||
userApiKey?: string
|
||||
): Promise<{ apiKey: string; isBYOK: boolean }> {
|
||||
/**
|
||||
* Mirrors the agent block hosted-key pattern (`injectHostedKeyIfNeeded`):
|
||||
* on self-hosted the user-supplied key from the block field flows through
|
||||
* unchanged; on hosted Sim we always source the key from workspace BYOK or
|
||||
* platform env, so any user-supplied value is ignored.
|
||||
*/
|
||||
if (!isHosted && userApiKey) {
|
||||
return { apiKey: userApiKey, isBYOK: false }
|
||||
}
|
||||
if (workspaceId) {
|
||||
const byokResult = await getBYOKKey(workspaceId, 'cohere')
|
||||
if (byokResult) {
|
||||
@@ -77,8 +88,19 @@ async function resolveCohereKey(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subset of Cohere v2/rerank response fields we read.
|
||||
* Reference: https://docs.cohere.com/v2/reference/rerank
|
||||
* - `results[].index` maps back to the position in the documents we sent.
|
||||
* - `results[].relevance_score` is normalized 0–1.
|
||||
* - `meta.warnings` is documented as an array of strings; we surface them in logs
|
||||
* so issues like document truncation don't disappear silently.
|
||||
*/
|
||||
interface CohereRerankResponse {
|
||||
results: Array<{ index: number; relevance_score: number }>
|
||||
meta?: {
|
||||
warnings?: string[]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,6 +114,8 @@ export async function rerank<T extends RerankItem>(
|
||||
model: string
|
||||
topN?: number
|
||||
workspaceId?: string | null
|
||||
/** User-supplied Cohere key from the Knowledge block field. Honored only on self-hosted. */
|
||||
apiKey?: string
|
||||
}
|
||||
): Promise<RerankResponse<T>> {
|
||||
if (items.length === 0) return { results: [], isBYOK: false }
|
||||
@@ -100,7 +124,7 @@ export async function rerank<T extends RerankItem>(
|
||||
throw new Error(`Unsupported reranker model: ${options.model}`)
|
||||
}
|
||||
|
||||
const { apiKey, isBYOK } = await resolveCohereKey(options.workspaceId)
|
||||
const { apiKey, isBYOK } = await resolveCohereKey(options.workspaceId, options.apiKey)
|
||||
const cappedItems =
|
||||
items.length > MAX_DOCUMENTS_PER_RERANK ? items.slice(0, MAX_DOCUMENTS_PER_RERANK) : items
|
||||
if (items.length > MAX_DOCUMENTS_PER_RERANK) {
|
||||
@@ -151,6 +175,13 @@ export async function rerank<T extends RerankItem>(
|
||||
}
|
||||
)
|
||||
|
||||
if (response.meta?.warnings && response.meta.warnings.length > 0) {
|
||||
logger.warn('Cohere rerank returned warnings', {
|
||||
model: options.model,
|
||||
warnings: response.meta.warnings,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
results: response.results
|
||||
.filter((r) => r.index >= 0 && r.index < cappedItems.length)
|
||||
|
||||
@@ -55,6 +55,19 @@ export const knowledgeSearchTool: ToolConfig<any, KnowledgeSearchResponse> = {
|
||||
description:
|
||||
'Cohere rerank model to use (one of: rerank-v4.0-pro, rerank-v4.0-fast, rerank-v3.5)',
|
||||
},
|
||||
rerankerInputCount: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Number of vector results sent to the Cohere reranker (1–100). Defaults to topK × 4 capped at 100.',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Cohere API key for reranker (self-hosted deployments only)',
|
||||
},
|
||||
},
|
||||
|
||||
schemaEnrichment: {
|
||||
@@ -84,13 +97,29 @@ export const knowledgeSearchTool: ToolConfig<any, KnowledgeSearchResponse> = {
|
||||
typeof params.rerankerModel === 'string' && params.rerankerModel.length > 0
|
||||
? params.rerankerModel
|
||||
: DEFAULT_RERANKER_MODEL
|
||||
const rerankerApiKey =
|
||||
typeof params.apiKey === 'string' && params.apiKey.length > 0 ? params.apiKey : undefined
|
||||
const rawInputCount =
|
||||
params.rerankerInputCount !== undefined &&
|
||||
params.rerankerInputCount !== null &&
|
||||
params.rerankerInputCount !== ''
|
||||
? Number(params.rerankerInputCount)
|
||||
: Number.NaN
|
||||
const rerankerInputCount = Number.isFinite(rawInputCount)
|
||||
? Math.max(1, Math.min(100, Math.floor(rawInputCount)))
|
||||
: undefined
|
||||
|
||||
const requestBody = {
|
||||
knowledgeBaseIds,
|
||||
query: params.query,
|
||||
topK: params.topK ? Math.max(1, Math.min(100, Number(params.topK))) : 10,
|
||||
...(structuredFilters.length > 0 && { tagFilters: structuredFilters }),
|
||||
...(rerankerEnabled && { rerankerEnabled: true, rerankerModel }),
|
||||
...(rerankerEnabled && {
|
||||
rerankerEnabled: true,
|
||||
rerankerModel,
|
||||
...(rerankerInputCount !== undefined && { rerankerInputCount }),
|
||||
...(rerankerApiKey && { rerankerApiKey }),
|
||||
}),
|
||||
...(workflowId && { workflowId }),
|
||||
}
|
||||
|
||||
|
||||
@@ -275,6 +275,12 @@ app:
|
||||
# in the Agent block UI — users just pick an Azure model and run.
|
||||
NEXT_PUBLIC_AZURE_CONFIGURED: "" # Set to "true" to hide Azure credential fields
|
||||
|
||||
# Cohere Reranker (Knowledge block)
|
||||
# Set COHERE_API_KEY (or COHERE_API_KEY_1/2/3 for rotation) and NEXT_PUBLIC_COHERE_CONFIGURED=true
|
||||
# to pre-configure the Cohere reranker server-side. When configured, the Cohere API Key field is
|
||||
# hidden in the Knowledge block UI.
|
||||
NEXT_PUBLIC_COHERE_CONFIGURED: "" # Set to "true" to hide the Cohere API Key field on the Knowledge block
|
||||
|
||||
# AWS S3 Cloud Storage Configuration (optional - for file storage)
|
||||
# If configured, files will be stored in S3 instead of local storage
|
||||
AWS_REGION: "" # AWS region (e.g., "us-east-1")
|
||||
|
||||
Reference in New Issue
Block a user