efarmz-slackbot-data 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.clever.json +12 -0
- package/.dockerignore +13 -0
- package/.env.example +28 -0
- package/.github/workflows/deploy-production.yaml +34 -0
- package/.prettierrc +6 -0
- package/.tasks/F1-bootstrap.md +110 -0
- package/.tasks/F2-domain-layer.md +173 -0
- package/.tasks/F3-application-layer.md +166 -0
- package/.tasks/F4-infrastructure-layer.md +229 -0
- package/.tasks/F5-config-main.md +160 -0
- package/.tasks/F6-schemas-deployment.md +129 -0
- package/CLAUDE.md +163 -0
- package/Dockerfile +15 -0
- package/PRD.md +119 -0
- package/docs/schemas/.gitkeep +0 -0
- package/docs/schemas/_guidelines.md +89 -0
- package/docs/schemas/efarmz_db.md +759 -0
- package/docs/schemas/example.md +16 -0
- package/eslint.config.mjs +18 -0
- package/package.json +54 -0
- package/releaserc.json +15 -0
- package/src/.gitkeep +0 -0
- package/src/application/agent/.gitkeep +0 -0
- package/src/application/agent/AgentContext.test.ts +263 -0
- package/src/application/agent/AgentContext.ts +93 -0
- package/src/application/agent/AgentLoop.test.ts +275 -0
- package/src/application/agent/AgentLoop.ts +101 -0
- package/src/application/agent/AgentRunResult.ts +11 -0
- package/src/application/agent/LLMMessage.ts +16 -0
- package/src/application/agent/tools/RunSqlTool.ts +23 -0
- package/src/application/formatting/.gitkeep +0 -0
- package/src/application/formatting/CsvRenderer.test.ts +162 -0
- package/src/application/formatting/CsvRenderer.ts +34 -0
- package/src/application/formatting/MonospaceTableRenderer.test.ts +129 -0
- package/src/application/formatting/MonospaceTableRenderer.ts +58 -0
- package/src/application/formatting/RenderedResponse.ts +7 -0
- package/src/application/formatting/ResponseRenderer.test.ts +159 -0
- package/src/application/formatting/ResponseRenderer.ts +39 -0
- package/src/application/formatting/ScalarRenderer.test.ts +36 -0
- package/src/application/formatting/ScalarRenderer.ts +12 -0
- package/src/application/usecases/.gitkeep +0 -0
- package/src/application/usecases/AnswerQuestion.test.ts +362 -0
- package/src/application/usecases/AnswerQuestion.ts +69 -0
- package/src/application/usecases/ParseQuestion.test.ts +39 -0
- package/src/application/usecases/ParseQuestion.ts +9 -0
- package/src/config/.gitkeep +0 -0
- package/src/config/Container.test.ts +35 -0
- package/src/config/Container.ts +74 -0
- package/src/config/constants.ts +9 -0
- package/src/config/env.test.ts +103 -0
- package/src/config/env.ts +41 -0
- package/src/domain/entities/.gitkeep +0 -0
- package/src/domain/entities/Conversation.test.ts +69 -0
- package/src/domain/entities/Conversation.ts +26 -0
- package/src/domain/entities/ConversationMessage.test.ts +49 -0
- package/src/domain/entities/ConversationMessage.ts +18 -0
- package/src/domain/entities/index.ts +2 -0
- package/src/domain/errors/.gitkeep +0 -0
- package/src/domain/errors/AgentLoopExceededError.ts +12 -0
- package/src/domain/errors/DomainError.test.ts +106 -0
- package/src/domain/errors/DomainError.ts +11 -0
- package/src/domain/errors/InvalidSqlError.ts +15 -0
- package/src/domain/errors/LLMError.ts +15 -0
- package/src/domain/errors/SchemaLoadError.ts +15 -0
- package/src/domain/errors/SqlExecutionError.ts +15 -0
- package/src/domain/errors/index.ts +15 -0
- package/src/domain/ports/.gitkeep +0 -0
- package/src/domain/ports/AdminLogger.ts +16 -0
- package/src/domain/ports/ConversationRepository.ts +10 -0
- package/src/domain/ports/LLMProvider.ts +33 -0
- package/src/domain/ports/Logger.ts +8 -0
- package/src/domain/ports/SchemaCatalog.ts +5 -0
- package/src/domain/ports/SlackMessenger.ts +8 -0
- package/src/domain/ports/SqlExecutor.ts +8 -0
- package/src/domain/ports/SqlValidator.ts +5 -0
- package/src/domain/ports/index.ts +17 -0
- package/src/domain/value-objects/.gitkeep +0 -0
- package/src/domain/value-objects/LLMProviderName.ts +6 -0
- package/src/domain/value-objects/QueryResult.test.ts +51 -0
- package/src/domain/value-objects/QueryResult.ts +18 -0
- package/src/domain/value-objects/Question.test.ts +59 -0
- package/src/domain/value-objects/Question.ts +22 -0
- package/src/domain/value-objects/QuestionFlags.test.ts +59 -0
- package/src/domain/value-objects/QuestionFlags.ts +18 -0
- package/src/domain/value-objects/ResponseRendering.ts +7 -0
- package/src/domain/value-objects/SqlQuery.test.ts +40 -0
- package/src/domain/value-objects/SqlQuery.ts +12 -0
- package/src/domain/value-objects/ThreadId.test.ts +68 -0
- package/src/domain/value-objects/ThreadId.ts +27 -0
- package/src/domain/value-objects/index.ts +13 -0
- package/src/infrastructure/llm/.gitkeep +0 -0
- package/src/infrastructure/llm/AnthropicLLMProvider.test.ts +229 -0
- package/src/infrastructure/llm/AnthropicLLMProvider.ts +45 -0
- package/src/infrastructure/llm/index.ts +4 -0
- package/src/infrastructure/llm/mappers/AnthropicMessageMapper.test.ts +173 -0
- package/src/infrastructure/llm/mappers/AnthropicMessageMapper.ts +34 -0
- package/src/infrastructure/llm/prompts/SystemPromptBuilder.test.ts +41 -0
- package/src/infrastructure/llm/prompts/SystemPromptBuilder.ts +31 -0
- package/src/infrastructure/llm/prompts/ToolDefinitions.ts +7 -0
- package/src/infrastructure/logging/.gitkeep +0 -0
- package/src/infrastructure/logging/PinoLogger.test.ts +59 -0
- package/src/infrastructure/logging/PinoLogger.ts +28 -0
- package/src/infrastructure/logging/index.ts +1 -0
- package/src/infrastructure/persistence/.gitkeep +0 -0
- package/src/infrastructure/persistence/InMemoryConversationRepository.test.ts +325 -0
- package/src/infrastructure/persistence/InMemoryConversationRepository.ts +69 -0
- package/src/infrastructure/persistence/PostgresPoolFactory.ts +11 -0
- package/src/infrastructure/persistence/PostgresSqlExecutor.test.ts +130 -0
- package/src/infrastructure/persistence/PostgresSqlExecutor.ts +34 -0
- package/src/infrastructure/persistence/index.ts +3 -0
- package/src/infrastructure/schemas/.gitkeep +0 -0
- package/src/infrastructure/schemas/FileSystemSchemaCatalog.test.ts +163 -0
- package/src/infrastructure/schemas/FileSystemSchemaCatalog.ts +35 -0
- package/src/infrastructure/schemas/index.ts +4 -0
- package/src/infrastructure/slack/.gitkeep +0 -0
- package/src/infrastructure/slack/BoltSlackMessenger.test.ts +59 -0
- package/src/infrastructure/slack/BoltSlackMessenger.ts +36 -0
- package/src/infrastructure/slack/SlackAdminLogger.test.ts +54 -0
- package/src/infrastructure/slack/SlackAdminLogger.ts +27 -0
- package/src/infrastructure/slack/SlackApp.ts +9 -0
- package/src/infrastructure/slack/handlers/AppMentionHandler.ts +52 -0
- package/src/infrastructure/slack/handlers/DirectMessageHandler.ts +65 -0
- package/src/infrastructure/slack/index.ts +5 -0
- package/src/infrastructure/sql/.gitkeep +0 -0
- package/src/infrastructure/sql/RegexSqlValidator.test.ts +242 -0
- package/src/infrastructure/sql/RegexSqlValidator.ts +53 -0
- package/src/infrastructure/sql/index.ts +1 -0
- package/src/main.ts +19 -0
- package/tsconfig.json +23 -0
- package/vitest.config.ts +15 -0
- package/vitest.setup.ts +23 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "F4 — Infrastructure Layer"
|
|
3
|
+
type: feature
|
|
4
|
+
status: todo
|
|
5
|
+
prd: .tasks/PRD.md
|
|
6
|
+
depends_on: F3
|
|
7
|
+
github_issue: 29
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# F4 — Infrastructure Layer
|
|
11
|
+
|
|
12
|
+
## Objectif
|
|
13
|
+
|
|
14
|
+
Implémenter tous les adapters concrets qui réalisent les ports définis dans `src/domain/ports/`. Cette couche peut importer les SDKs tiers (`pg`, `@anthropic-ai/sdk`, `@slack/bolt`, `pino`) mais ne doit jamais importer `src/application/`.
|
|
15
|
+
|
|
16
|
+
## Fichiers à créer
|
|
17
|
+
|
|
18
|
+
### SQL (`src/infrastructure/sql/`)
|
|
19
|
+
|
|
20
|
+
**`RegexSqlValidator.ts`** — implémente `SqlValidator`
|
|
21
|
+
|
|
22
|
+
Règles de validation (dans l'ordre) :
|
|
23
|
+
1. Trim + normalisation whitespace
|
|
24
|
+
2. Doit commencer par `SELECT` ou `WITH` (insensitive) → sinon `InvalidSqlError`
|
|
25
|
+
3. Refus des keywords mutables : `\b(INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|TRUNCATE|GRANT|REVOKE|COPY|CALL|VACUUM|MERGE)\b` (insensitive) → `InvalidSqlError`
|
|
26
|
+
4. Refus de `;` sauf en toute fin de requête (optionnel) → `InvalidSqlError`
|
|
27
|
+
5. Si absent : append `LIMIT {SQL_MAX_ROWS}` avant le `;` final ou en fin de chaîne
|
|
28
|
+
|
|
29
|
+
Retourne le SQL nettoyé (string), ou throw `InvalidSqlError`.
|
|
30
|
+
|
|
31
|
+
Tests exhaustifs requis (voir ci-dessous).
|
|
32
|
+
|
|
33
|
+
### Persistence (`src/infrastructure/persistence/`)
|
|
34
|
+
|
|
35
|
+
**`PostgresPoolFactory.ts`**
|
|
36
|
+
- Crée un `Pool` pg depuis `DATABASE_URL`
|
|
37
|
+
- `idleTimeoutMillis: 30000`, `connectionTimeoutMillis: 5000`
|
|
38
|
+
- Export : factory function `createPool(databaseUrl: string): Pool`
|
|
39
|
+
|
|
40
|
+
**`PostgresSqlExecutor.ts`** — implémente `SqlExecutor`
|
|
41
|
+
```typescript
|
|
42
|
+
class PostgresSqlExecutor implements SqlExecutor {
|
|
43
|
+
constructor(private readonly pool: Pool, private readonly timeoutMs: number) {}
|
|
44
|
+
|
|
45
|
+
async execute(query: SqlQuery): Promise<QueryResult> {
|
|
46
|
+
const client = await this.pool.connect();
|
|
47
|
+
try {
|
|
48
|
+
await client.query(`SET statement_timeout = ${this.timeoutMs}`);
|
|
49
|
+
const result = await client.query(query.raw);
|
|
50
|
+
return QueryResult.create(
|
|
51
|
+
result.fields.map(f => f.name),
|
|
52
|
+
result.rows,
|
|
53
|
+
);
|
|
54
|
+
} finally {
|
|
55
|
+
client.release();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
- Wrap les erreurs PG en `SqlExecutionError`
|
|
61
|
+
|
|
62
|
+
**`InMemoryConversationRepository.ts`** — implémente `ConversationRepository`
|
|
63
|
+
- Stockage : `Map<ThreadId, { conversation: Conversation; lastAccess: number }>`
|
|
64
|
+
- LRU : si `Map.size >= CONVERSATION_MAX_THREADS`, supprimer l'entrée avec le `lastAccess` le plus ancien
|
|
65
|
+
- TTL : au `get`, vérifier si `Date.now() - lastAccess > CONVERSATION_TTL_MS` → retourner `null` et purger
|
|
66
|
+
- `save` met à jour `lastAccess`
|
|
67
|
+
|
|
68
|
+
### Schemas (`src/infrastructure/schemas/`)
|
|
69
|
+
|
|
70
|
+
**`FileSystemSchemaCatalog.ts`** — implémente `SchemaCatalog`
|
|
71
|
+
- Au constructeur : lire tous les `*.md` dans `SCHEMAS_DIR` (sync, fail-fast si erreur → `SchemaLoadError`)
|
|
72
|
+
- Concatène le contenu de tous les fichiers avec séparateurs
|
|
73
|
+
- `getDocumentation()` retourne la chaîne concaténée (mémoïsée)
|
|
74
|
+
|
|
75
|
+
### LLM (`src/infrastructure/llm/`)
|
|
76
|
+
|
|
77
|
+
**`prompts/SystemPromptBuilder.ts`**
|
|
78
|
+
```typescript
|
|
79
|
+
class SystemPromptBuilder {
|
|
80
|
+
static build(schemaDoc: string): AnthropicSystemContent {
|
|
81
|
+
return [
|
|
82
|
+
{
|
|
83
|
+
type: 'text',
|
|
84
|
+
text: SYSTEM_PROMPT_HEADER,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
type: 'text',
|
|
88
|
+
text: schemaDoc,
|
|
89
|
+
cache_control: { type: 'ephemeral' },
|
|
90
|
+
},
|
|
91
|
+
];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
Le `SYSTEM_PROMPT_HEADER` décrit le rôle du bot, les règles (SELECT uniquement, toujours utiliser `run_sql`), et le format de réponse attendu.
|
|
96
|
+
|
|
97
|
+
**`prompts/ToolDefinitions.ts`**
|
|
98
|
+
- Export de la définition JSON du tool `run_sql` (réexporte depuis `application/agent/tools/RunSqlTool.ts`)
|
|
99
|
+
|
|
100
|
+
**`mappers/AnthropicMessageMapper.ts`**
|
|
101
|
+
- Convertit `LLMMessage[]` (domain) → `Anthropic.MessageParam[]` (SDK)
|
|
102
|
+
- Convertit `Anthropic.Message` (SDK) → `LLMResponse` (domain)
|
|
103
|
+
|
|
104
|
+
**`AnthropicLLMProvider.ts`** — implémente `LLMProvider`
|
|
105
|
+
```typescript
|
|
106
|
+
class AnthropicLLMProvider implements LLMProvider {
|
|
107
|
+
constructor(
|
|
108
|
+
private readonly client: Anthropic,
|
|
109
|
+
private readonly model: string,
|
|
110
|
+
private readonly schemaCatalog: SchemaCatalog,
|
|
111
|
+
) {}
|
|
112
|
+
|
|
113
|
+
async generate(request: LLMRequest): Promise<LLMResponse> {
|
|
114
|
+
const response = await this.client.messages.create({
|
|
115
|
+
model: this.model,
|
|
116
|
+
max_tokens: 4096,
|
|
117
|
+
system: SystemPromptBuilder.build(this.schemaCatalog.getDocumentation()),
|
|
118
|
+
messages: AnthropicMessageMapper.toSDK(request.messages),
|
|
119
|
+
tools: request.tools,
|
|
120
|
+
});
|
|
121
|
+
return AnthropicMessageMapper.fromSDK(response);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
- Wrap les erreurs Anthropic en `LLMError`
|
|
126
|
+
|
|
127
|
+
### Slack (`src/infrastructure/slack/`)
|
|
128
|
+
|
|
129
|
+
**`SlackApp.ts`** — Setup Bolt
|
|
130
|
+
- `App` avec `token` + `signingSecret` + `receiver: ExpressReceiver`
|
|
131
|
+
- Export : `{ app, receiver }`
|
|
132
|
+
|
|
133
|
+
**`BoltSlackMessenger.ts`** — implémente `SlackMessenger`
|
|
134
|
+
```typescript
|
|
135
|
+
class BoltSlackMessenger implements SlackMessenger {
|
|
136
|
+
async post(threadId: ThreadId, text: string): Promise<void> {
|
|
137
|
+
await this.client.chat.postMessage({
|
|
138
|
+
channel: threadId.channelId,
|
|
139
|
+
thread_ts: threadId.threadTs,
|
|
140
|
+
text,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async postCsv(threadId, text, csvBuffer, filename): Promise<void> {
|
|
145
|
+
await this.client.chat.postMessage({ ... text ... });
|
|
146
|
+
await this.client.files.uploadV2({
|
|
147
|
+
channel_id: threadId.channelId,
|
|
148
|
+
thread_ts: threadId.threadTs,
|
|
149
|
+
filename,
|
|
150
|
+
file: csvBuffer,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
> Note : `ThreadId` doit encoder `channelId` + `threadTs` pour permettre le posting. Adapter la définition du VO si nécessaire.
|
|
157
|
+
|
|
158
|
+
**`SlackAdminLogger.ts`** — implémente `AdminLogger`
|
|
159
|
+
- Si `SLACK_ADMIN_CHANNEL_ID` absent → no-op (log warning au boot)
|
|
160
|
+
- Post un message formaté dans le channel admin avec : user, question, sqls[], rowCount, durationMs, tokens
|
|
161
|
+
|
|
162
|
+
**`handlers/AppMentionHandler.ts`**
|
|
163
|
+
```typescript
|
|
164
|
+
// app.event('app_mention', async ({ event, ack, say }) => {
|
|
165
|
+
// ack(); // 200 OK immédiat
|
|
166
|
+
// const dedupKey = event.event_id;
|
|
167
|
+
// if (eventCache.has(dedupKey)) return;
|
|
168
|
+
// eventCache.set(dedupKey, true, TTL_5MIN);
|
|
169
|
+
// setImmediate(async () => { await answerQuestion.execute(...) });
|
|
170
|
+
// });
|
|
171
|
+
```
|
|
172
|
+
- Ack immédiat
|
|
173
|
+
- Déduplication par `event_id` (cache Map avec TTL 5 min)
|
|
174
|
+
- Traitement en `setImmediate` / promise détachée
|
|
175
|
+
- Catch-all : log + message Slack générique d'erreur
|
|
176
|
+
|
|
177
|
+
**`handlers/DirectMessageHandler.ts`**
|
|
178
|
+
- Même logique que `AppMentionHandler` pour `message.im`
|
|
179
|
+
- Filtrer les bots (`event.bot_id` présent → ignorer)
|
|
180
|
+
|
|
181
|
+
### Logging (`src/infrastructure/logging/`)
|
|
182
|
+
|
|
183
|
+
**`PinoLogger.ts`** — implémente `Logger`
|
|
184
|
+
```typescript
|
|
185
|
+
class PinoLogger implements Logger {
|
|
186
|
+
private readonly pino: pino.Logger;
|
|
187
|
+
constructor(level: string) {
|
|
188
|
+
this.pino = pino({ level });
|
|
189
|
+
}
|
|
190
|
+
debug(msg, meta?) { this.pino.debug(meta ?? {}, msg); }
|
|
191
|
+
info(msg, meta?) { this.pino.info(meta ?? {}, msg); }
|
|
192
|
+
warn(msg, meta?) { this.pino.warn(meta ?? {}, msg); }
|
|
193
|
+
error(msg, meta?) { this.pino.error(meta ?? {}, msg); }
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Tests unitaires (`src/infrastructure/__tests__/`)
|
|
198
|
+
|
|
199
|
+
**`RegexSqlValidator.test.ts`** — tests exhaustifs
|
|
200
|
+
- `SELECT * FROM orders` → valide
|
|
201
|
+
- `WITH cte AS (...) SELECT ...` → valide
|
|
202
|
+
- `SELECT ...; DROP TABLE orders` → `InvalidSqlError`
|
|
203
|
+
- `INSERT INTO orders ...` → `InvalidSqlError`
|
|
204
|
+
- `UPDATE orders SET ...` → `InvalidSqlError`
|
|
205
|
+
- `DELETE FROM orders ...` → `InvalidSqlError`
|
|
206
|
+
- `DROP TABLE orders` → `InvalidSqlError`
|
|
207
|
+
- `CREATE TABLE ...` → `InvalidSqlError`
|
|
208
|
+
- `ALTER TABLE ...` → `InvalidSqlError`
|
|
209
|
+
- `TRUNCATE orders` → `InvalidSqlError`
|
|
210
|
+
- `GRANT SELECT ...` → `InvalidSqlError`
|
|
211
|
+
- `select * from orders where name like 'drop%'` → valide (keyword dans string)
|
|
212
|
+
- SQL sans LIMIT → LIMIT ajouté
|
|
213
|
+
- SQL avec LIMIT existant → LIMIT non-dupliqué
|
|
214
|
+
- SQL avec `;` au milieu → `InvalidSqlError`
|
|
215
|
+
|
|
216
|
+
**`InMemoryConversationRepository.test.ts`**
|
|
217
|
+
- `get` après TTL → retourne `null`
|
|
218
|
+
- `save` → LRU éviction si `MAX_THREADS` atteint
|
|
219
|
+
- `purge` → supprime l'entrée
|
|
220
|
+
|
|
221
|
+
## Acceptance Criteria
|
|
222
|
+
|
|
223
|
+
- [ ] `yarn typecheck` passe
|
|
224
|
+
- [ ] `yarn test` : tests `RegexSqlValidator` et `InMemoryConversationRepository` passent
|
|
225
|
+
- [ ] `RegexSqlValidator` couvre tous les keywords interdits
|
|
226
|
+
- [ ] `PostgresSqlExecutor` set `statement_timeout` avant chaque requête
|
|
227
|
+
- [ ] `AnthropicLLMProvider` utilise `cache_control: { type: 'ephemeral' }` sur le bloc system
|
|
228
|
+
- [ ] Handlers Slack ack < 3s (traitement en `setImmediate`)
|
|
229
|
+
- [ ] Déduplication par `event_id` dans les deux handlers
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "F5 — Config & Composition Root"
|
|
3
|
+
type: feature
|
|
4
|
+
status: todo
|
|
5
|
+
prd: .tasks/PRD.md
|
|
6
|
+
depends_on: F4
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# F5 — Config & Composition Root
|
|
10
|
+
|
|
11
|
+
## Objectif
|
|
12
|
+
|
|
13
|
+
Assembler toutes les couches via la DI manuelle (`Container`), valider les variables d'environnement au boot, et créer l'entry point `main.ts`. C'est le seul endroit où les implémentations concrètes sont instanciées et câblées ensemble.
|
|
14
|
+
|
|
15
|
+
## Fichiers à créer
|
|
16
|
+
|
|
17
|
+
### `src/config/env.ts`
|
|
18
|
+
|
|
19
|
+
Validation via zod au boot — fail-fast si variable manquante ou invalide.
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { z } from 'zod';
|
|
23
|
+
|
|
24
|
+
const envSchema = z.object({
|
|
25
|
+
PORT: z.coerce.number().default(3000),
|
|
26
|
+
NODE_ENV: z.enum([`development`, `production`, `test`]).default(`development`),
|
|
27
|
+
LOG_LEVEL: z.enum([`debug`, `info`, `warn`, `error`]).default(`info`),
|
|
28
|
+
|
|
29
|
+
SLACK_BOT_TOKEN: z.string().min(1),
|
|
30
|
+
SLACK_SIGNING_SECRET: z.string().min(1),
|
|
31
|
+
SLACK_ADMIN_CHANNEL_ID: z.string().optional(),
|
|
32
|
+
|
|
33
|
+
LLM_PROVIDER: z.enum([`anthropic`]).default(`anthropic`),
|
|
34
|
+
LLM_MODEL: z.string().default(`claude-haiku-4-5-20251001`),
|
|
35
|
+
ANTHROPIC_API_KEY: z.string().min(1),
|
|
36
|
+
AGENT_MAX_TURNS: z.coerce.number().default(5),
|
|
37
|
+
|
|
38
|
+
DATABASE_URL: z.string().url(),
|
|
39
|
+
SQL_TIMEOUT_MS: z.coerce.number().default(30000),
|
|
40
|
+
SQL_MAX_ROWS: z.coerce.number().default(100),
|
|
41
|
+
SQL_CSV_THRESHOLD: z.coerce.number().default(15),
|
|
42
|
+
|
|
43
|
+
CONVERSATION_MAX_MESSAGES: z.coerce.number().default(8),
|
|
44
|
+
CONVERSATION_TTL_MS: z.coerce.number().default(3600000),
|
|
45
|
+
CONVERSATION_MAX_THREADS: z.coerce.number().default(200),
|
|
46
|
+
|
|
47
|
+
SCHEMAS_DIR: z.string().default(`./docs/schemas`),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const parsed = envSchema.safeParse(process.env);
|
|
51
|
+
if (!parsed.success) {
|
|
52
|
+
console.error(`Invalid environment variables:`, parsed.error.format());
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const env = parsed.data;
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### `src/config/constants.ts`
|
|
60
|
+
|
|
61
|
+
Constantes dérivées de `env` ou fixes :
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
export const SYSTEM_PROMPT_HEADER = `...`; // prompt system du bot
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### `src/config/Container.ts`
|
|
68
|
+
|
|
69
|
+
Composition root — instancie et câble toutes les dépendances.
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
import { env } from './env.js';
|
|
73
|
+
import { PinoLogger } from '@/infrastructure/logging/PinoLogger.js';
|
|
74
|
+
import { FileSystemSchemaCatalog } from '@/infrastructure/schemas/FileSystemSchemaCatalog.js';
|
|
75
|
+
import { RegexSqlValidator } from '@/infrastructure/sql/RegexSqlValidator.js';
|
|
76
|
+
import { createPool } from '@/infrastructure/persistence/PostgresPoolFactory.js';
|
|
77
|
+
import { PostgresSqlExecutor } from '@/infrastructure/persistence/PostgresSqlExecutor.js';
|
|
78
|
+
import { InMemoryConversationRepository } from '@/infrastructure/persistence/InMemoryConversationRepository.js';
|
|
79
|
+
import { AnthropicLLMProvider } from '@/infrastructure/llm/AnthropicLLMProvider.js';
|
|
80
|
+
import { BoltSlackMessenger } from '@/infrastructure/slack/BoltSlackMessenger.js';
|
|
81
|
+
import { SlackAdminLogger } from '@/infrastructure/slack/SlackAdminLogger.js';
|
|
82
|
+
import { AgentLoop } from '@/application/agent/AgentLoop.js';
|
|
83
|
+
import { ResponseRenderer } from '@/application/formatting/ResponseRenderer.js';
|
|
84
|
+
import { AnswerQuestion } from '@/application/usecases/AnswerQuestion.js';
|
|
85
|
+
import { ParseQuestion } from '@/application/usecases/ParseQuestion.js';
|
|
86
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
87
|
+
|
|
88
|
+
export function createContainer() {
|
|
89
|
+
const logger = new PinoLogger(env.LOG_LEVEL);
|
|
90
|
+
|
|
91
|
+
const schemaCatalog = new FileSystemSchemaCatalog(env.SCHEMAS_DIR, logger);
|
|
92
|
+
|
|
93
|
+
const sqlValidator = new RegexSqlValidator(env.SQL_MAX_ROWS);
|
|
94
|
+
const pool = createPool(env.DATABASE_URL);
|
|
95
|
+
const sqlExecutor = new PostgresSqlExecutor(pool, env.SQL_TIMEOUT_MS);
|
|
96
|
+
const conversationRepository = new InMemoryConversationRepository(
|
|
97
|
+
env.CONVERSATION_TTL_MS,
|
|
98
|
+
env.CONVERSATION_MAX_THREADS,
|
|
99
|
+
env.CONVERSATION_MAX_MESSAGES,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const anthropicClient = new Anthropic({ apiKey: env.ANTHROPIC_API_KEY });
|
|
103
|
+
const llmProvider = new AnthropicLLMProvider(anthropicClient, env.LLM_MODEL, schemaCatalog);
|
|
104
|
+
|
|
105
|
+
// Slack (app + client instanciés avant pour les messengers)
|
|
106
|
+
const { app } = createSlackApp(env); // setup Bolt
|
|
107
|
+
const slackMessenger = new BoltSlackMessenger(app.client);
|
|
108
|
+
const adminLogger = new SlackAdminLogger(app.client, env.SLACK_ADMIN_CHANNEL_ID, logger);
|
|
109
|
+
|
|
110
|
+
const agentLoop = new AgentLoop(llmProvider, sqlValidator, sqlExecutor, schemaCatalog, env.AGENT_MAX_TURNS);
|
|
111
|
+
const responseRenderer = new ResponseRenderer(env.SQL_CSV_THRESHOLD);
|
|
112
|
+
|
|
113
|
+
const answerQuestion = new AnswerQuestion(
|
|
114
|
+
conversationRepository,
|
|
115
|
+
agentLoop,
|
|
116
|
+
responseRenderer,
|
|
117
|
+
slackMessenger,
|
|
118
|
+
adminLogger,
|
|
119
|
+
logger,
|
|
120
|
+
);
|
|
121
|
+
const parseQuestion = new ParseQuestion();
|
|
122
|
+
|
|
123
|
+
return { app, answerQuestion, parseQuestion, logger };
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### `src/main.ts`
|
|
128
|
+
|
|
129
|
+
Entry point minimal — boot, setup handlers, start server.
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
import { createContainer } from '@/config/Container.js';
|
|
133
|
+
import { AppMentionHandler } from '@/infrastructure/slack/handlers/AppMentionHandler.js';
|
|
134
|
+
import { DirectMessageHandler } from '@/infrastructure/slack/handlers/DirectMessageHandler.js';
|
|
135
|
+
import { env } from '@/config/env.js';
|
|
136
|
+
|
|
137
|
+
async function main() {
|
|
138
|
+
const { app, answerQuestion, parseQuestion, logger } = createContainer();
|
|
139
|
+
|
|
140
|
+
AppMentionHandler.register(app, answerQuestion, parseQuestion, logger);
|
|
141
|
+
DirectMessageHandler.register(app, answerQuestion, parseQuestion, logger);
|
|
142
|
+
|
|
143
|
+
await app.start(env.PORT);
|
|
144
|
+
logger.info(`Bot started on port ${env.PORT}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
main().catch((err) => {
|
|
148
|
+
console.error(`Fatal error at startup:`, err);
|
|
149
|
+
process.exit(1);
|
|
150
|
+
});
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Acceptance Criteria
|
|
154
|
+
|
|
155
|
+
- [ ] `yarn typecheck` passe
|
|
156
|
+
- [ ] `yarn build` produit `dist/main.js` sans erreur
|
|
157
|
+
- [ ] `yarn start` démarre sur le port configuré (avec env valide)
|
|
158
|
+
- [ ] Boot échoue immédiatement si une variable requise est manquante (exit code 1)
|
|
159
|
+
- [ ] `FileSystemSchemaCatalog` lève `SchemaLoadError` au boot si `SCHEMAS_DIR` invalide
|
|
160
|
+
- [ ] Aucune instanciation de classe concrète en dehors de `Container.ts` et `main.ts`
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "F6 — Documentation des schémas & Déploiement"
|
|
3
|
+
type: feature
|
|
4
|
+
status: todo
|
|
5
|
+
prd: .tasks/PRD.md
|
|
6
|
+
depends_on: F5
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# F6 — Documentation des schémas & Déploiement
|
|
10
|
+
|
|
11
|
+
## Objectif
|
|
12
|
+
|
|
13
|
+
Rédiger la documentation des schémas PostgreSQL efarmz (chargée dans le system prompt du LLM), finaliser le Dockerfile pour Clever Cloud, et rédiger le README de setup. Aucun code de production à écrire dans cette feature.
|
|
14
|
+
|
|
15
|
+
## Livrables
|
|
16
|
+
|
|
17
|
+
### 1. Documentation des schémas (`docs/schemas/`)
|
|
18
|
+
|
|
19
|
+
Un fichier `.md` par schéma PostgreSQL. Format attendu par `FileSystemSchemaCatalog` :
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
docs/schemas/
|
|
23
|
+
├── efarmz_db.md # schéma principal (commandes, clients, produits, …)
|
|
24
|
+
├── efarmz_intercom.md # schéma Intercom (conversations, contacts)
|
|
25
|
+
└── ... # autres schémas si nécessaire
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Format de chaque fichier** (exemple `efarmz_db.md`) :
|
|
29
|
+
|
|
30
|
+
```markdown
|
|
31
|
+
# Schéma efarmz_db
|
|
32
|
+
|
|
33
|
+
## Table: orders
|
|
34
|
+
Description courte du contenu de la table.
|
|
35
|
+
|
|
36
|
+
| Colonne | Type | Description |
|
|
37
|
+
|----------------|-------------|--------------------------------------|
|
|
38
|
+
| id | uuid | Identifiant unique de la commande |
|
|
39
|
+
| customer_id | uuid | Référence vers customers.id |
|
|
40
|
+
| status | varchar | Statut : pending, confirmed, shipped |
|
|
41
|
+
| total_amount | numeric | Montant total TTC en euros |
|
|
42
|
+
| created_at | timestamptz | Date de création |
|
|
43
|
+
|
|
44
|
+
## Table: customers
|
|
45
|
+
...
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Contenu fourni par l'utilisateur** — ce fichier est un placeholder vide à compléter.
|
|
49
|
+
|
|
50
|
+
> Important : plus la doc est précise (descriptions des colonnes, valeurs d'enum, relations FK, exemples de valeurs), meilleure sera la qualité du SQL généré.
|
|
51
|
+
|
|
52
|
+
### 2. `Dockerfile` finalisé
|
|
53
|
+
|
|
54
|
+
Multi-stage, Node 22 slim, copie `docs/` dans l'image runtime :
|
|
55
|
+
|
|
56
|
+
```dockerfile
|
|
57
|
+
FROM node:22-slim AS builder
|
|
58
|
+
WORKDIR /app
|
|
59
|
+
COPY package*.json ./
|
|
60
|
+
RUN npm ci --omit=dev
|
|
61
|
+
COPY tsconfig.json ./
|
|
62
|
+
COPY src ./src
|
|
63
|
+
RUN npm run build
|
|
64
|
+
|
|
65
|
+
FROM node:22-slim AS runtime
|
|
66
|
+
WORKDIR /app
|
|
67
|
+
ENV NODE_ENV=production
|
|
68
|
+
COPY --from=builder /app/dist ./dist
|
|
69
|
+
COPY --from=builder /app/node_modules ./node_modules
|
|
70
|
+
COPY docs ./docs
|
|
71
|
+
EXPOSE 3000
|
|
72
|
+
CMD ["node", "dist/main.js"]
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 3. Clever Cloud — configuration
|
|
76
|
+
|
|
77
|
+
Fichier `clevercloud/` ou variables dans l'interface Clever Cloud :
|
|
78
|
+
|
|
79
|
+
- **Type** : Node.js ou Docker (préférer Docker pour le contrôle de la version Node)
|
|
80
|
+
- **Build** : `docker build`
|
|
81
|
+
- **Port** : 3000 (variable `PORT`)
|
|
82
|
+
- **Toutes les variables d'env** depuis `.env.example` à configurer dans l'interface CC
|
|
83
|
+
|
|
84
|
+
### 4. Rôle PostgreSQL read-only
|
|
85
|
+
|
|
86
|
+
Script SQL à exécuter manuellement sur la base de prod :
|
|
87
|
+
|
|
88
|
+
```sql
|
|
89
|
+
-- À adapter pour chaque schéma accessible
|
|
90
|
+
CREATE USER slackbot_readonly WITH PASSWORD 'mot_de_passe_fort';
|
|
91
|
+
GRANT CONNECT ON DATABASE efarmz TO slackbot_readonly;
|
|
92
|
+
GRANT USAGE ON SCHEMA efarmz_db TO slackbot_readonly;
|
|
93
|
+
GRANT SELECT ON ALL TABLES IN SCHEMA efarmz_db TO slackbot_readonly;
|
|
94
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA efarmz_db GRANT SELECT ON TABLES TO slackbot_readonly;
|
|
95
|
+
-- Répéter pour efarmz_intercom, etc.
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 5. `README.md`
|
|
99
|
+
|
|
100
|
+
Sections :
|
|
101
|
+
|
|
102
|
+
1. **Pré-requis** : Node 22, Docker, ngrok (dev local), compte Clever Cloud
|
|
103
|
+
2. **Setup Slack App** :
|
|
104
|
+
- Créer une app sur api.slack.com
|
|
105
|
+
- Activer les scopes : `app_mentions:read`, `chat:write`, `chat:write.public`, `files:write`, `im:history`, `im:read`, `im:write`, `users:read`
|
|
106
|
+
- Activer les event subscriptions : `app_mention`, `message.im`
|
|
107
|
+
- URL webhook : `https://<clever-domain>/slack/events`
|
|
108
|
+
- Activer l'App Home → DM tab
|
|
109
|
+
3. **Variables d'environnement** : référence à `.env.example`, description de chaque variable
|
|
110
|
+
4. **Setup local (ngrok)** :
|
|
111
|
+
```bash
|
|
112
|
+
cp .env.example .env
|
|
113
|
+
# Remplir .env
|
|
114
|
+
yarn dev
|
|
115
|
+
ngrok http 3000
|
|
116
|
+
# Configurer l'URL ngrok dans la Slack App
|
|
117
|
+
```
|
|
118
|
+
5. **Init PostgreSQL read-only** : script SQL ci-dessus
|
|
119
|
+
6. **Déploiement Clever Cloud** : push Docker, config env vars
|
|
120
|
+
7. **Ajout d'un nouveau schéma** : déposer un `.md` dans `docs/schemas/`, redéployer
|
|
121
|
+
8. **Flags utilisateur** : `--sql`, `--debug`, `--no-context`
|
|
122
|
+
|
|
123
|
+
## Acceptance Criteria
|
|
124
|
+
|
|
125
|
+
- [ ] Au moins un fichier `docs/schemas/*.md` présent (même placeholder)
|
|
126
|
+
- [ ] `docker build .` réussit depuis zéro
|
|
127
|
+
- [ ] `README.md` couvre setup local (ngrok) et déploiement Clever Cloud
|
|
128
|
+
- [ ] Script SQL init du rôle read-only documenté dans le README
|
|
129
|
+
- [ ] `.env.example` contient toutes les variables avec commentaires explicatifs
|
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# CLAUDE.md — efarmz-slackbot-data
|
|
2
|
+
|
|
3
|
+
Bot Slack qui traduit des questions en langage naturel vers des requêtes SQL via LLM (boucle agentique), exécute en read-only sur les bases analytiques efarmz, et renvoie le résultat formaté dans Slack.
|
|
4
|
+
|
|
5
|
+
> Plan détaillé : `.plans/efarmz-slackbot-data-712fe7.md`
|
|
6
|
+
|
|
7
|
+
## Stack (différente du starter Next.js)
|
|
8
|
+
|
|
9
|
+
- **Runtime** : Node.js 22 + TypeScript strict (ESM, `noUncheckedIndexedAccess`)
|
|
10
|
+
- **Slack** : `@slack/bolt` (HTTP receiver Express, pas Socket Mode)
|
|
11
|
+
- **DB** : `pg` (PAS de Drizzle — service standalone, pas de migrations)
|
|
12
|
+
- **LLM** : `@anthropic-ai/sdk` par défaut (Haiku 4.5), derrière une interface `LLMProvider`
|
|
13
|
+
- **Validation** : `zod`
|
|
14
|
+
- **Logs** : `pino`
|
|
15
|
+
- **Tests** : `vitest`
|
|
16
|
+
- **Hosting** : Clever Cloud (Docker)
|
|
17
|
+
|
|
18
|
+
Pas de Next.js, pas de React, pas de Tailwind, pas de Drizzle, pas de Trigger.dev, pas d'auth utilisateur (le workspace Slack est la frontière de sécurité).
|
|
19
|
+
|
|
20
|
+
## Architecture DDD / Hexagonal
|
|
21
|
+
|
|
22
|
+
Trois couches, **dépendances unidirectionnelles** : `domain` ← `application` ← `infrastructure` ← `main`.
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
src/
|
|
26
|
+
├── domain/ # PURE — aucun import externe (sauf zod pour validation)
|
|
27
|
+
│ ├── entities/ # Conversation, ConversationMessage, QueryResult
|
|
28
|
+
│ ├── value-objects/ # ThreadId, Question, QuestionFlags, SqlQuery, ...
|
|
29
|
+
│ ├── errors/ # DomainError + sous-classes
|
|
30
|
+
│ └── ports/ # Interfaces : LLMProvider, SqlExecutor, SqlValidator, ...
|
|
31
|
+
├── application/ # Use cases (orchestration) — dépend de domain seulement
|
|
32
|
+
│ ├── usecases/ # AnswerQuestion, ParseQuestion
|
|
33
|
+
│ ├── agent/ # AgentLoop, RunSqlTool
|
|
34
|
+
│ └── formatting/ # ResponseRenderer + Scalar/Monospace/Csv renderers
|
|
35
|
+
├── infrastructure/ # Adapters — implémente les ports de domain
|
|
36
|
+
│ ├── llm/ # AnthropicLLMProvider
|
|
37
|
+
│ ├── persistence/ # PostgresSqlExecutor, InMemoryConversationRepository
|
|
38
|
+
│ ├── sql/ # RegexSqlValidator
|
|
39
|
+
│ ├── schemas/ # FileSystemSchemaCatalog
|
|
40
|
+
│ ├── slack/ # BoltSlackMessenger, handlers
|
|
41
|
+
│ └── logging/ # PinoLogger
|
|
42
|
+
├── config/ # env.ts (zod), Container (DI manuel), constants
|
|
43
|
+
└── main.ts # Entry point
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Dependency Rule (non-négociable)
|
|
47
|
+
|
|
48
|
+
- `domain/` n'importe **JAMAIS** de `application/`, `infrastructure/`, `config/` ni de SDK tiers (`@slack/bolt`, `pg`, `@anthropic-ai/sdk`).
|
|
49
|
+
- `application/` importe **uniquement** de `domain/`.
|
|
50
|
+
- `infrastructure/` peut importer `domain/` (pour implémenter ses ports) — jamais `application/`.
|
|
51
|
+
- `main.ts` + `config/Container.ts` sont les seuls endroits où on assemble (compose) les implémentations.
|
|
52
|
+
|
|
53
|
+
Si tu te retrouves à importer `pg` ou `@slack/bolt` dans `domain/`, c'est une erreur d'architecture — il faut un nouveau port.
|
|
54
|
+
|
|
55
|
+
## Conventions DDD
|
|
56
|
+
|
|
57
|
+
**Value Objects** : immuables, validés à la construction via factory statique. Constructeur privé.
|
|
58
|
+
```typescript
|
|
59
|
+
// src/domain/value-objects/SqlQuery.ts
|
|
60
|
+
class SqlQuery {
|
|
61
|
+
private constructor(public readonly raw: string) {}
|
|
62
|
+
static create(raw: string, validator: SqlValidator): SqlQuery {
|
|
63
|
+
return new SqlQuery(validator.validate(raw));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Entities** : ont une identité, encapsulent leurs invariants. Pas de DTO anémique.
|
|
69
|
+
```typescript
|
|
70
|
+
// src/domain/entities/Conversation.ts — append plafonne à N messages
|
|
71
|
+
class Conversation {
|
|
72
|
+
append(message: ConversationMessage): Conversation { /* invariants ici */ }
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Ports** : interfaces dans `domain/ports/`, granularité fine (ISP). Pas de port fourre-tout.
|
|
77
|
+
- ✅ `SlackMessenger` (post) + `AdminLogger` (logQuery) séparés
|
|
78
|
+
- ❌ `SlackService` qui mélange tout
|
|
79
|
+
|
|
80
|
+
**Erreurs** : sous-classes de `DomainError`, jamais `throw new Error("string")` brut.
|
|
81
|
+
|
|
82
|
+
**DI** : manuelle via `config/Container.ts`. Pas d'inversify/tsyringe.
|
|
83
|
+
|
|
84
|
+
## Boucle agentique
|
|
85
|
+
|
|
86
|
+
`AgentLoop.run()` itère sur `tool_use` jusqu'à `end_turn` ou `AGENT_MAX_TURNS`. À chaque tour avec `run_sql` :
|
|
87
|
+
1. `SqlValidator.validate(input.sql)` → si KO, renvoie `tool_result.is_error = true` au LLM
|
|
88
|
+
2. `SqlExecutor.execute(query)` → succès ou erreur PG renvoyée comme `tool_result.is_error`
|
|
89
|
+
3. Le LLM corrige automatiquement au tour suivant — pas de logique de retry manuelle à écrire
|
|
90
|
+
|
|
91
|
+
Si tu modifies la boucle, garde la règle : **toute erreur SQL doit être renvoyée comme `tool_result`, pas thrown**. Sinon le LLM ne peut pas corriger.
|
|
92
|
+
|
|
93
|
+
## Sécurité SQL (4 couches, toutes obligatoires)
|
|
94
|
+
|
|
95
|
+
1. **Utilisateur PG read-only** dédié (rôle `slackbot_readonly`) — impossible de muter même si tout le reste est bypassé
|
|
96
|
+
2. **`RegexSqlValidator`** : doit commencer par `SELECT`/`WITH`, refuse les keywords mutables (INSERT/UPDATE/DELETE/DROP/CREATE/ALTER/TRUNCATE/GRANT/REVOKE/COPY/CALL/VACUUM/MERGE), refuse `;` en milieu de requête, force `LIMIT SQL_MAX_ROWS`
|
|
97
|
+
3. **`SET statement_timeout = SQL_TIMEOUT_MS`** par session
|
|
98
|
+
4. **`SqlQuery` VO** : constructeur privé — impossible d'exécuter un SQL non validé (vérifié au compile-time)
|
|
99
|
+
|
|
100
|
+
**Ne jamais** ajouter un chemin qui contourne le validator — tout `SqlExecutor.execute` doit recevoir un `SqlQuery`, pas une `string`.
|
|
101
|
+
|
|
102
|
+
## Doc des schémas
|
|
103
|
+
|
|
104
|
+
`docs/schemas/*.md` — un fichier par schéma PostgreSQL (efarmz_db, efarmz_intercom, …). Chargés au boot par `FileSystemSchemaCatalog`, concaténés dans le system prompt avec `cache_control: { type: "ephemeral" }` (TTL 5 min Anthropic) pour économiser les tokens en rafale.
|
|
105
|
+
|
|
106
|
+
Quand on ajoute un schéma : déposer le `.md` dans `docs/schemas/`, redéployer. Pas de code à changer.
|
|
107
|
+
|
|
108
|
+
## Format de réponse
|
|
109
|
+
|
|
110
|
+
`ResponseRenderer` décide :
|
|
111
|
+
- 0 ligne → "Aucun résultat."
|
|
112
|
+
- 1×1 → `ScalarRenderer` (valeur en gras)
|
|
113
|
+
- ≤ `SQL_CSV_THRESHOLD` lignes → `MonospaceTableRenderer` (bloc code aligné inline)
|
|
114
|
+
- \> `SQL_CSV_THRESHOLD` → monospace tronqué **+** CSV uploadé via `files.uploadV2`
|
|
115
|
+
|
|
116
|
+
Flags utilisateur dans le message Slack :
|
|
117
|
+
- `--sql` : ajoute le SQL en bloc code
|
|
118
|
+
- `--debug` : `--sql` + `EXPLAIN`
|
|
119
|
+
- `--no-context` : ignore l'historique du thread
|
|
120
|
+
|
|
121
|
+
## Conventions de code
|
|
122
|
+
|
|
123
|
+
- **Strings** : backticks (cohérent avec règles globales)
|
|
124
|
+
- **Imports** : paths absolus `@/` (alias TS configuré sur `src/`), jamais `../`
|
|
125
|
+
- **ESM** : tout est ESM, imports avec `.js` extension dans les paths relatifs si nécessaire
|
|
126
|
+
- **Naming** : `PascalCase.ts` pour classes/types, `camelCase.ts` pour fonctions/helpers
|
|
127
|
+
- **1 fichier = 1 export default** (cohérent avec convention globale)
|
|
128
|
+
- **Pas de comments** sauf si le *why* est non-évident (cf. règles globales)
|
|
129
|
+
|
|
130
|
+
## Commandes
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
yarn dev # tsx watch src/main.ts
|
|
134
|
+
yarn build # tsc
|
|
135
|
+
yarn start # node dist/main.js
|
|
136
|
+
yarn test # vitest run
|
|
137
|
+
yarn test:watch # vitest
|
|
138
|
+
yarn lint # eslint
|
|
139
|
+
yarn typecheck # tsc --noEmit
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Variables d'environnement
|
|
143
|
+
|
|
144
|
+
Voir `.env.example`. Toutes validées via `config/env.ts` (zod). Le boot échoue si une variable est manquante ou invalide (fail-fast).
|
|
145
|
+
|
|
146
|
+
## Hors-scope V1
|
|
147
|
+
|
|
148
|
+
À refuser si demandé sans discussion préalable :
|
|
149
|
+
- UI admin web
|
|
150
|
+
- Persistance DB du bot lui-même (LRU mémoire suffit pour les conversations)
|
|
151
|
+
- Implémentation OpenAI concrète (l'interface est prête, l'impl viendra plus tard)
|
|
152
|
+
- Graphiques, exports custom
|
|
153
|
+
- Slash commands (`/efarmz-stats ...`)
|
|
154
|
+
- i18n des réponses du bot
|
|
155
|
+
- Auth utilisateur niveau bot (workspace = frontière)
|
|
156
|
+
|
|
157
|
+
## Points d'attention récurrents
|
|
158
|
+
|
|
159
|
+
- **Ack Slack < 3s** : les handlers doivent répondre `200 OK` immédiatement, traitement en arrière-plan
|
|
160
|
+
- **Idempotence** : Slack retry — dédupliquer par `event_id` (cache 5 min)
|
|
161
|
+
- **Prompt caching** : toujours marquer le bloc system (doc schémas) avec `cache_control: { type: "ephemeral" }`
|
|
162
|
+
- **PII** : les résultats peuvent contenir emails/noms — workspace privé only, accepté
|
|
163
|
+
- **Coûts** : surveiller via `SlackAdminLogger` (channel admin)
|