IA Assistant version initiale
This commit is contained in:
1 parent
3e8508d48b
commit
662003fdd3
29 files changed
+4253
-10
No files matched your search
@@ -2727,4 +2727,154 @@ db.exec('CREATE INDEX IF NOT EXISTS idx_fraisop_date ON frais_operations(date_op
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ── Migration : Assistant IA embarqué (04/09/26) ──────────────────────────
|
||||
// Chat conversationnel branché sur les mêmes outils lecture seule que le
|
||||
// serveur MCP externe (mcp-server/tools.js, réutilisé tel quel — jamais
|
||||
// modifié — via un pont MCP in-process, voir backend/src/ai/mcpBridge.js).
|
||||
// Contrôle d'accès à 3 niveaux : ai_config.actif (global), users.ai_enabled
|
||||
// (par utilisateur, décidé par l'admin), ai_providers.actif (par
|
||||
// fournisseur). Deux adaptateurs prévus dès le départ (type 'anthropic'
|
||||
// natif et 'openai_compatible', ce dernier pouvant pointer vers OpenAI,
|
||||
// Mistral, un LLM local, ou OpenRouter via base_url) — seul l'adaptateur
|
||||
// Anthropic est implémenté dans cette première passe. Voir
|
||||
// backend/src/routes/ai.js et project_ia_embarquee.md (mémoire projet) pour
|
||||
// le détail de l'architecture.
|
||||
{
|
||||
const usersColsAi = db.prepare('PRAGMA table_info(users)').all().map(c => c.name);
|
||||
if (!usersColsAi.includes('ai_enabled')) {
|
||||
db.exec('ALTER TABLE users ADD COLUMN ai_enabled INTEGER NOT NULL DEFAULT 0');
|
||||
}
|
||||
}
|
||||
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS ai_providers (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
nom TEXT NOT NULL,
|
||||
type TEXT NOT NULL CHECK(type IN ('anthropic','openai_compatible')),
|
||||
base_url TEXT,
|
||||
modele TEXT NOT NULL,
|
||||
api_key_chiffree TEXT NOT NULL,
|
||||
actif INTEGER NOT NULL DEFAULT 1,
|
||||
is_default INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)
|
||||
`);
|
||||
db.exec('CREATE INDEX IF NOT EXISTS idx_ai_providers_actif ON ai_providers(actif)');
|
||||
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS ai_config (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
actif INTEGER NOT NULL DEFAULT 0,
|
||||
provider_defaut_id INTEGER REFERENCES ai_providers(id) ON DELETE SET NULL,
|
||||
quota_quotidien INTEGER,
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)
|
||||
`);
|
||||
db.exec(`INSERT OR IGNORE INTO ai_config (id, actif) VALUES (1, 0)`);
|
||||
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS ai_usage_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
investisseur_id INTEGER REFERENCES investisseurs(id) ON DELETE SET NULL,
|
||||
provider_id INTEGER REFERENCES ai_providers(id) ON DELETE SET NULL,
|
||||
modele TEXT,
|
||||
tokens_entree INTEGER,
|
||||
tokens_sortie INTEGER,
|
||||
outils_appeles TEXT,
|
||||
erreur TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)
|
||||
`);
|
||||
db.exec('CREATE INDEX IF NOT EXISTS idx_ai_usage_user ON ai_usage_log(user_id)');
|
||||
db.exec('CREATE INDEX IF NOT EXISTS idx_ai_usage_date ON ai_usage_log(created_at)');
|
||||
|
||||
// Clés API internes auto-provisionnées pour l'assistant IA (une par couple
|
||||
// user/investisseur, jamais affichées dans Mon compte → Clés API) : lui
|
||||
// donnent accès à /api/v1 exactement comme un client MCP externe, en
|
||||
// réutilisant l'infrastructure api_keys existante sans la dupliquer — voir
|
||||
// backend/src/ai/internalApiKey.js.
|
||||
{
|
||||
const apiKeysCols = db.prepare('PRAGMA table_info(api_keys)').all().map(c => c.name);
|
||||
if (!apiKeysCols.includes('internal')) {
|
||||
db.exec('ALTER TABLE api_keys ADD COLUMN internal INTEGER NOT NULL DEFAULT 0');
|
||||
}
|
||||
}
|
||||
|
||||
// Conversations de l'assistant IA — plusieurs fils nommés par couple
|
||||
// user/investisseur (même granularité de scope que le reste de l'appli, cf.
|
||||
// requireInvestisseur), consultables et supprimables individuellement
|
||||
// (page dédiée /assistant-ia + widget flottant, qui partagent la même
|
||||
// conversation via son id). titre : dérivé du premier message utilisateur
|
||||
// à la création (voir routes/ai.js), jamais renommé automatiquement ensuite.
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS ai_conversations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
investisseur_id INTEGER REFERENCES investisseurs(id) ON DELETE CASCADE,
|
||||
titre TEXT NOT NULL DEFAULT 'Nouvelle conversation',
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)
|
||||
`);
|
||||
db.exec('CREATE INDEX IF NOT EXISTS idx_ai_conversations_scope ON ai_conversations(user_id, investisseur_id, updated_at DESC)');
|
||||
|
||||
// Messages de l'assistant IA — rattachés à une conversation (ai_conversations).
|
||||
// outils_appeles : JSON (tableau de noms d'outil) pour les messages assistant
|
||||
// uniquement, sinon NULL. Voir backend/src/ai/conversationHistory.js.
|
||||
// user_id/investisseur_id restent présents sur chaque message (redondants
|
||||
// avec ai_conversations mais pratiques pour des requêtes directes futures,
|
||||
// et c'est le schéma d'origine avant l'introduction du multi-conversations).
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS ai_messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
conversation_id INTEGER REFERENCES ai_conversations(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
investisseur_id INTEGER REFERENCES investisseurs(id) ON DELETE CASCADE,
|
||||
role TEXT NOT NULL CHECK(role IN ('user','assistant')),
|
||||
content TEXT NOT NULL,
|
||||
outils_appeles TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)
|
||||
`);
|
||||
// Migration : ajoute conversation_id si la table ai_messages existait déjà
|
||||
// (créée avant l'introduction du multi-conversations, 04/09/26).
|
||||
{
|
||||
const aiMsgCols = db.prepare('PRAGMA table_info(ai_messages)').all().map(c => c.name);
|
||||
if (!aiMsgCols.includes('conversation_id')) {
|
||||
db.exec('ALTER TABLE ai_messages ADD COLUMN conversation_id INTEGER REFERENCES ai_conversations(id) ON DELETE CASCADE');
|
||||
}
|
||||
}
|
||||
db.exec('CREATE INDEX IF NOT EXISTS idx_ai_messages_conversation ON ai_messages(conversation_id, id)');
|
||||
db.exec('CREATE INDEX IF NOT EXISTS idx_ai_messages_thread ON ai_messages(user_id, investisseur_id, created_at)');
|
||||
|
||||
// Backfill : les messages enregistrés avant l'introduction du
|
||||
// multi-conversations n'ont pas de conversation_id — on les regroupe dans
|
||||
// une conversation unique par couple user/investisseur pour ne rien perdre
|
||||
// de l'historique existant. Idempotent (ne trouve plus rien à faire une
|
||||
// fois exécuté, puisque conversation_id n'est alors plus jamais NULL).
|
||||
{
|
||||
const orphanScopes = db.prepare(`
|
||||
SELECT DISTINCT user_id, investisseur_id FROM ai_messages WHERE conversation_id IS NULL
|
||||
`).all();
|
||||
for (const { user_id, investisseur_id } of orphanScopes) {
|
||||
const firstUserMsg = db.prepare(`
|
||||
SELECT content FROM ai_messages
|
||||
WHERE user_id = ? AND investisseur_id IS ? AND conversation_id IS NULL AND role = 'user'
|
||||
ORDER BY id ASC LIMIT 1
|
||||
`).get(user_id, investisseur_id);
|
||||
const rawTitre = firstUserMsg ? firstUserMsg.content.replace(/\s+/g, ' ').trim() : 'Conversation';
|
||||
const titre = rawTitre.length > 60 ? rawTitre.slice(0, 57) + '…' : (rawTitre || 'Conversation');
|
||||
const { lastInsertRowid: convId } = db.prepare(`
|
||||
INSERT INTO ai_conversations (user_id, investisseur_id, titre) VALUES (?, ?, ?)
|
||||
`).run(user_id, investisseur_id, titre);
|
||||
db.prepare(`
|
||||
UPDATE ai_messages SET conversation_id = ?
|
||||
WHERE user_id = ? AND investisseur_id IS ? AND conversation_id IS NULL
|
||||
`).run(convId, user_id, investisseur_id);
|
||||
}
|
||||
}
|
||||
|
||||
export default db;
|
||||
Reference in new issue
Block a user