godcrm/backend/routes/v3/ai-agents/sharedEmbedding.js
GOD CRM Release f89e074dd1
Some checks failed
CI / Lint / Typecheck / Test / Build (push) Has been cancelled
CI / PostgreSQL Integration Tests (push) Has been cancelled
GOD CRM — public scrubbed snapshot
Governed substrate for autonomous agents: scoped identity (passports),
audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
2026-08-10 04:01:45 +03:00

205 lines
6.8 KiB
JavaScript

/**
* Shared embedding/vector utilities for AI agents routes.
* Extracted from shared.js for modular organization.
*/
import { dbGet, dbAll, isPostgres } from '../../../database/connection.js';
import { apiLogger } from '../../../utils/logger.js';
import { getSecret } from '../../../services/secrets/getSecret.js';
import { safeParseJSON, resolveAgentRelations } from './shared.js';
/**
* Default embedding model configuration
*/
export const DEFAULT_EMBEDDING_MODEL = 'text-embedding-3-small';
export const DEFAULT_EMBEDDING_DIMENSIONS = 1536;
/**
* Resolve API key and model for embedding generation
*/
export async function resolveEmbeddingConfig(agentId, spaceId) {
let apiKey = null;
let model = DEFAULT_EMBEDDING_MODEL;
let baseUrl = 'https://api.openai.com/v1';
let agentName = 'System Default';
// 1. Try to use specific agent if provided
if (agentId) {
const agentRow = await dbGet(`
SELECT tr.data, tr.table_id, ut.project_id, p.space_id
FROM table_rows tr
JOIN universal_tables ut ON tr.table_id = ut.id
JOIN projects p ON ut.project_id = p.id
WHERE tr.id = ? AND (ut.name LIKE '%Agents%' OR ut.name LIKE '%agents%')
`, [agentId]);
if (agentRow) {
const agentConfig = safeParseJSON(agentRow.data, {});
await resolveAgentRelations(agentConfig, agentRow.table_id);
agentName = agentConfig.name || 'Custom Agent';
model = agentConfig.model || model;
if (agentConfig.operator_id) {
const operatorRow = await dbGet(`
SELECT tr.data
FROM table_rows tr
JOIN universal_tables ut ON tr.table_id = ut.id
WHERE tr.id = ? AND ut.name LIKE '%Operators%'
`, [agentConfig.operator_id]);
if (operatorRow) {
const operatorData = safeParseJSON(operatorRow.data, {});
if (operatorData.api_key) {
apiKey = operatorData.api_key;
baseUrl = operatorData.base_url || baseUrl;
}
}
}
}
}
// 2. Try to find default embedding agent in space
if (!apiKey && spaceId) {
const embeddingAgent = await dbGet(
isPostgres()
? `SELECT tr.id, tr.data, tr.table_id
FROM table_rows tr
JOIN universal_tables ut ON tr.table_id = ut.id
JOIN projects p ON ut.project_id = p.id
WHERE p.space_id = $1
AND (ut.name LIKE '%Agents%' OR ut.name LIKE '%agents%')
AND (
tr.data->>'agent_type' = 'embedding'
OR tr.data->>'name' LIKE '%Embedding%'
)
AND (
tr.data->>'is_active' = '1'
OR tr.data->>'is_active' = 'true'
OR tr.data->>'status' = 'active'
)
ORDER BY tr.created_at ASC
LIMIT 1`
: `SELECT tr.id, tr.data, tr.table_id
FROM table_rows tr
JOIN universal_tables ut ON tr.table_id = ut.id
JOIN projects p ON ut.project_id = p.id
WHERE p.space_id = ?
AND (ut.name LIKE '%Agents%' OR ut.name LIKE '%agents%')
AND (
json_extract(tr.data, '$.agent_type') = 'embedding'
OR json_extract(tr.data, '$.name') LIKE '%Embedding%'
)
AND (
json_extract(tr.data, '$.is_active') = '1'
OR json_extract(tr.data, '$.is_active') = 'true'
OR json_extract(tr.data, '$.status') = 'active'
)
ORDER BY tr.created_at ASC
LIMIT 1`,
[spaceId]);
if (embeddingAgent) {
const agentConfig = safeParseJSON(embeddingAgent.data, {});
await resolveAgentRelations(agentConfig, embeddingAgent.table_id);
agentName = agentConfig.name || 'Embedding Agent';
model = agentConfig.model || model;
if (agentConfig.operator_id) {
const operatorRow = await dbGet(`
SELECT tr.data
FROM table_rows tr
JOIN universal_tables ut ON tr.table_id = ut.id
WHERE tr.id = ? AND ut.name LIKE '%Operators%'
`, [agentConfig.operator_id]);
if (operatorRow) {
const operatorData = safeParseJSON(operatorRow.data, {});
if (operatorData.api_key) {
apiKey = operatorData.api_key;
baseUrl = operatorData.base_url || baseUrl;
}
}
}
}
}
// 3. Fallback to AI API Keys table
if (!apiKey) {
const keyRow = await dbGet(
isPostgres()
? `SELECT tr.data FROM table_rows tr
JOIN universal_tables ut ON tr.table_id = ut.id
WHERE ut.name LIKE '%API Keys%'
AND tr.data->>'status' = 'active'
AND (tr.data->>'name' ILIKE '%openai%' OR tr.data->>'name' ILIKE '%embedding%')
ORDER BY tr.created_at DESC LIMIT 1`
: `SELECT tr.data FROM table_rows tr
JOIN universal_tables ut ON tr.table_id = ut.id
WHERE ut.name LIKE '%API Keys%'
AND json_extract(tr.data, '$.status') = 'active'
AND (json_extract(tr.data, '$.name') LIKE '%OpenAI%' OR json_extract(tr.data, '$.name') LIKE '%Embedding%')
ORDER BY tr.created_at DESC LIMIT 1`,
[]
);
if (keyRow) {
const keyData = safeParseJSON(keyRow.data, {});
if (keyData.api_key) {
apiKey = keyData.api_key;
agentName = 'AI API Keys Fallback';
apiLogger.debug({ context: 'Vector' }, 'Using API key from AI API Keys table');
}
}
}
// 4. Fallback to vault (ADR-0040 — was process.env.OPENAI_API_KEY)
if (!apiKey) {
apiKey = await getSecret('openai_api_key', 'OPENAI_API_KEY');
agentName = 'Vault Fallback';
apiLogger.debug({ context: 'Vector' }, 'Using vault fallback (openai_api_key)');
}
return { apiKey, model, baseUrl, agentName };
}
/**
* Generate embedding using OpenAI API
*/
export async function generateEmbedding(text, apiKey, model = DEFAULT_EMBEDDING_MODEL, baseUrl = 'https://api.openai.com/v1') {
if (!apiKey) {
throw new Error('No API key configured for embedding generation');
}
const response = await fetch(`${baseUrl}/embeddings`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: model,
input: text
})
});
if (!response.ok) {
const error = await response.text();
apiLogger.error({ err: error, context: 'Vector' }, 'OpenAI API error');
throw new Error(`OpenAI API error: ${response.status}`);
}
const result = await response.json();
return result.data[0].embedding;
}
/**
* Apply formula template to row data
*/
export function applyFormula(formula, rowData) {
if (!formula) return '';
return formula.replace(/\{\{(\w+)\}\}/g, (match, key) => {
const value = rowData[key];
return value != null ? String(value) : '';
});
}