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.
Files changed (131) hide show
  1. package/.clever.json +12 -0
  2. package/.dockerignore +13 -0
  3. package/.env.example +28 -0
  4. package/.github/workflows/deploy-production.yaml +34 -0
  5. package/.prettierrc +6 -0
  6. package/.tasks/F1-bootstrap.md +110 -0
  7. package/.tasks/F2-domain-layer.md +173 -0
  8. package/.tasks/F3-application-layer.md +166 -0
  9. package/.tasks/F4-infrastructure-layer.md +229 -0
  10. package/.tasks/F5-config-main.md +160 -0
  11. package/.tasks/F6-schemas-deployment.md +129 -0
  12. package/CLAUDE.md +163 -0
  13. package/Dockerfile +15 -0
  14. package/PRD.md +119 -0
  15. package/docs/schemas/.gitkeep +0 -0
  16. package/docs/schemas/_guidelines.md +89 -0
  17. package/docs/schemas/efarmz_db.md +759 -0
  18. package/docs/schemas/example.md +16 -0
  19. package/eslint.config.mjs +18 -0
  20. package/package.json +54 -0
  21. package/releaserc.json +15 -0
  22. package/src/.gitkeep +0 -0
  23. package/src/application/agent/.gitkeep +0 -0
  24. package/src/application/agent/AgentContext.test.ts +263 -0
  25. package/src/application/agent/AgentContext.ts +93 -0
  26. package/src/application/agent/AgentLoop.test.ts +275 -0
  27. package/src/application/agent/AgentLoop.ts +101 -0
  28. package/src/application/agent/AgentRunResult.ts +11 -0
  29. package/src/application/agent/LLMMessage.ts +16 -0
  30. package/src/application/agent/tools/RunSqlTool.ts +23 -0
  31. package/src/application/formatting/.gitkeep +0 -0
  32. package/src/application/formatting/CsvRenderer.test.ts +162 -0
  33. package/src/application/formatting/CsvRenderer.ts +34 -0
  34. package/src/application/formatting/MonospaceTableRenderer.test.ts +129 -0
  35. package/src/application/formatting/MonospaceTableRenderer.ts +58 -0
  36. package/src/application/formatting/RenderedResponse.ts +7 -0
  37. package/src/application/formatting/ResponseRenderer.test.ts +159 -0
  38. package/src/application/formatting/ResponseRenderer.ts +39 -0
  39. package/src/application/formatting/ScalarRenderer.test.ts +36 -0
  40. package/src/application/formatting/ScalarRenderer.ts +12 -0
  41. package/src/application/usecases/.gitkeep +0 -0
  42. package/src/application/usecases/AnswerQuestion.test.ts +362 -0
  43. package/src/application/usecases/AnswerQuestion.ts +69 -0
  44. package/src/application/usecases/ParseQuestion.test.ts +39 -0
  45. package/src/application/usecases/ParseQuestion.ts +9 -0
  46. package/src/config/.gitkeep +0 -0
  47. package/src/config/Container.test.ts +35 -0
  48. package/src/config/Container.ts +74 -0
  49. package/src/config/constants.ts +9 -0
  50. package/src/config/env.test.ts +103 -0
  51. package/src/config/env.ts +41 -0
  52. package/src/domain/entities/.gitkeep +0 -0
  53. package/src/domain/entities/Conversation.test.ts +69 -0
  54. package/src/domain/entities/Conversation.ts +26 -0
  55. package/src/domain/entities/ConversationMessage.test.ts +49 -0
  56. package/src/domain/entities/ConversationMessage.ts +18 -0
  57. package/src/domain/entities/index.ts +2 -0
  58. package/src/domain/errors/.gitkeep +0 -0
  59. package/src/domain/errors/AgentLoopExceededError.ts +12 -0
  60. package/src/domain/errors/DomainError.test.ts +106 -0
  61. package/src/domain/errors/DomainError.ts +11 -0
  62. package/src/domain/errors/InvalidSqlError.ts +15 -0
  63. package/src/domain/errors/LLMError.ts +15 -0
  64. package/src/domain/errors/SchemaLoadError.ts +15 -0
  65. package/src/domain/errors/SqlExecutionError.ts +15 -0
  66. package/src/domain/errors/index.ts +15 -0
  67. package/src/domain/ports/.gitkeep +0 -0
  68. package/src/domain/ports/AdminLogger.ts +16 -0
  69. package/src/domain/ports/ConversationRepository.ts +10 -0
  70. package/src/domain/ports/LLMProvider.ts +33 -0
  71. package/src/domain/ports/Logger.ts +8 -0
  72. package/src/domain/ports/SchemaCatalog.ts +5 -0
  73. package/src/domain/ports/SlackMessenger.ts +8 -0
  74. package/src/domain/ports/SqlExecutor.ts +8 -0
  75. package/src/domain/ports/SqlValidator.ts +5 -0
  76. package/src/domain/ports/index.ts +17 -0
  77. package/src/domain/value-objects/.gitkeep +0 -0
  78. package/src/domain/value-objects/LLMProviderName.ts +6 -0
  79. package/src/domain/value-objects/QueryResult.test.ts +51 -0
  80. package/src/domain/value-objects/QueryResult.ts +18 -0
  81. package/src/domain/value-objects/Question.test.ts +59 -0
  82. package/src/domain/value-objects/Question.ts +22 -0
  83. package/src/domain/value-objects/QuestionFlags.test.ts +59 -0
  84. package/src/domain/value-objects/QuestionFlags.ts +18 -0
  85. package/src/domain/value-objects/ResponseRendering.ts +7 -0
  86. package/src/domain/value-objects/SqlQuery.test.ts +40 -0
  87. package/src/domain/value-objects/SqlQuery.ts +12 -0
  88. package/src/domain/value-objects/ThreadId.test.ts +68 -0
  89. package/src/domain/value-objects/ThreadId.ts +27 -0
  90. package/src/domain/value-objects/index.ts +13 -0
  91. package/src/infrastructure/llm/.gitkeep +0 -0
  92. package/src/infrastructure/llm/AnthropicLLMProvider.test.ts +229 -0
  93. package/src/infrastructure/llm/AnthropicLLMProvider.ts +45 -0
  94. package/src/infrastructure/llm/index.ts +4 -0
  95. package/src/infrastructure/llm/mappers/AnthropicMessageMapper.test.ts +173 -0
  96. package/src/infrastructure/llm/mappers/AnthropicMessageMapper.ts +34 -0
  97. package/src/infrastructure/llm/prompts/SystemPromptBuilder.test.ts +41 -0
  98. package/src/infrastructure/llm/prompts/SystemPromptBuilder.ts +31 -0
  99. package/src/infrastructure/llm/prompts/ToolDefinitions.ts +7 -0
  100. package/src/infrastructure/logging/.gitkeep +0 -0
  101. package/src/infrastructure/logging/PinoLogger.test.ts +59 -0
  102. package/src/infrastructure/logging/PinoLogger.ts +28 -0
  103. package/src/infrastructure/logging/index.ts +1 -0
  104. package/src/infrastructure/persistence/.gitkeep +0 -0
  105. package/src/infrastructure/persistence/InMemoryConversationRepository.test.ts +325 -0
  106. package/src/infrastructure/persistence/InMemoryConversationRepository.ts +69 -0
  107. package/src/infrastructure/persistence/PostgresPoolFactory.ts +11 -0
  108. package/src/infrastructure/persistence/PostgresSqlExecutor.test.ts +130 -0
  109. package/src/infrastructure/persistence/PostgresSqlExecutor.ts +34 -0
  110. package/src/infrastructure/persistence/index.ts +3 -0
  111. package/src/infrastructure/schemas/.gitkeep +0 -0
  112. package/src/infrastructure/schemas/FileSystemSchemaCatalog.test.ts +163 -0
  113. package/src/infrastructure/schemas/FileSystemSchemaCatalog.ts +35 -0
  114. package/src/infrastructure/schemas/index.ts +4 -0
  115. package/src/infrastructure/slack/.gitkeep +0 -0
  116. package/src/infrastructure/slack/BoltSlackMessenger.test.ts +59 -0
  117. package/src/infrastructure/slack/BoltSlackMessenger.ts +36 -0
  118. package/src/infrastructure/slack/SlackAdminLogger.test.ts +54 -0
  119. package/src/infrastructure/slack/SlackAdminLogger.ts +27 -0
  120. package/src/infrastructure/slack/SlackApp.ts +9 -0
  121. package/src/infrastructure/slack/handlers/AppMentionHandler.ts +52 -0
  122. package/src/infrastructure/slack/handlers/DirectMessageHandler.ts +65 -0
  123. package/src/infrastructure/slack/index.ts +5 -0
  124. package/src/infrastructure/sql/.gitkeep +0 -0
  125. package/src/infrastructure/sql/RegexSqlValidator.test.ts +242 -0
  126. package/src/infrastructure/sql/RegexSqlValidator.ts +53 -0
  127. package/src/infrastructure/sql/index.ts +1 -0
  128. package/src/main.ts +19 -0
  129. package/tsconfig.json +23 -0
  130. package/vitest.config.ts +15 -0
  131. package/vitest.setup.ts +23 -0
package/.clever.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "apps": [
3
+ {
4
+ "app_id": "app_155a0f63-4ede-493c-814f-72381c07538d",
5
+ "org_id": "orga_41b0bd88-3bce-4bc8-ba86-fa63451216a3",
6
+ "deploy_url": "https://push-n3-par-clevercloud-customers.services.clever-cloud.com/app_155a0f63-4ede-493c-814f-72381c07538d.git",
7
+ "git_ssh_url": "git+ssh://git@push-n3-par-clevercloud-customers.services.clever-cloud.com/app_155a0f63-4ede-493c-814f-72381c07538d.git",
8
+ "name": "efarmz-slackbot-data",
9
+ "alias": "app_155a0f63-4ede-493c-814f-72381c07538d"
10
+ }
11
+ ]
12
+ }
package/.dockerignore ADDED
@@ -0,0 +1,13 @@
1
+ node_modules
2
+ dist
3
+ .git
4
+ .gitignore
5
+ *.md
6
+ .env
7
+ .env.*
8
+ !.env.example
9
+ .plans
10
+ .tasks
11
+ .claude
12
+ *.log
13
+ .DS_Store
package/.env.example ADDED
@@ -0,0 +1,28 @@
1
+ # Slack
2
+ SLACK_BOT_TOKEN=xoxb-...
3
+ SLACK_SIGNING_SECRET=...
4
+ SLACK_APP_TOKEN=xapp-... # Optionnel si HTTP mode
5
+
6
+ # PostgreSQL (read-only user)
7
+ DATABASE_URL=postgres://slackbot_readonly:...@host:5432/efarmz_db
8
+
9
+ # Anthropic
10
+ ANTHROPIC_API_KEY=sk-ant-...
11
+
12
+ # App config
13
+ PORT=3000
14
+ LOG_LEVEL=info
15
+ ADMIN_CHANNEL_ID=C...
16
+
17
+ # SQL limits
18
+ SQL_TIMEOUT_MS=30000
19
+ SQL_MAX_ROWS=100
20
+ SQL_CSV_THRESHOLD=15
21
+
22
+ # Agent config
23
+ AGENT_MAX_TURNS=5
24
+
25
+ # Conversation cache
26
+ CONVERSATION_TTL_MS=3600000
27
+ CONVERSATION_MAX_THREADS=200
28
+ CONVERSATION_MAX_MESSAGES=8
@@ -0,0 +1,34 @@
1
+ name: Clever Cloud Production Deployment
2
+ env:
3
+ HUSKY: 0
4
+ on:
5
+ push:
6
+ branches:
7
+ - master
8
+ jobs:
9
+ Deploy-Production:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v3
13
+ with:
14
+ fetch-depth: 0
15
+ - name: Authenticate with private NPM package
16
+ run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
17
+ - uses: actions/setup-node@v4
18
+ with:
19
+ node-version: 21
20
+ corepack-enable: true
21
+ cache: yarn
22
+ env:
23
+ SKIP_YARN_COREPACK_CHECK: true
24
+ - uses: 47ng/actions-clever-cloud@v2.0.0
25
+ with:
26
+ appID: app_155a0f63-4ede-493c-814f-72381c07538d
27
+ force: true
28
+ env:
29
+ CLEVER_TOKEN: ${{ secrets.CLEVER_TOKEN }}
30
+ CLEVER_SECRET: ${{ secrets.CLEVER_SECRET }}
31
+ - name: Semantic Release
32
+ uses: cycjimmy/semantic-release-action@v4
33
+ env:
34
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
package/.prettierrc ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "semi": true,
3
+ "singleQuote": true,
4
+ "tabWidth": 2,
5
+ "trailingComma": "es5"
6
+ }
@@ -0,0 +1,110 @@
1
+ ---
2
+ title: "F1 — Bootstrap projet"
3
+ type: feature
4
+ status: todo
5
+ prd: .tasks/PRD.md
6
+ ---
7
+
8
+ # F1 — Bootstrap projet
9
+
10
+ ## Objectif
11
+
12
+ Initialiser le projet Node.js/TypeScript avec toute la configuration tooling nécessaire pour que les features suivantes puissent être développées dans un environnement stable, typé et testé.
13
+
14
+ ## Scope
15
+
16
+ - `package.json` avec toutes les dépendances (prod + dev)
17
+ - `tsconfig.json` : strict, ESM, `noUncheckedIndexedAccess`, path alias `@/` → `src/`
18
+ - `.env.example` : toutes les variables documentées
19
+ - `Dockerfile` multi-stage (build + runtime Node 22 slim)
20
+ - `.dockerignore`
21
+ - `vitest.config.ts`
22
+ - `.eslintrc` (ou `eslint.config.mjs`) avec règles TypeScript strict
23
+ - `.prettierrc`
24
+ - Structure de dossiers vide (`src/domain/`, `src/application/`, `src/infrastructure/`, `src/config/`, `docs/schemas/`)
25
+
26
+ ## Dépendances à installer
27
+
28
+ ### Production
29
+
30
+ ```
31
+ @slack/bolt
32
+ @anthropic-ai/sdk
33
+ pg
34
+ zod
35
+ pino
36
+ ```
37
+
38
+ ### Dev
39
+
40
+ ```
41
+ typescript
42
+ tsx
43
+ vitest
44
+ @types/node
45
+ @types/pg
46
+ eslint
47
+ @typescript-eslint/eslint-plugin
48
+ @typescript-eslint/parser
49
+ prettier
50
+ ```
51
+
52
+ ## Scripts `package.json`
53
+
54
+ ```json
55
+ {
56
+ "dev": "tsx watch src/main.ts",
57
+ "build": "tsc",
58
+ "start": "node dist/main.js",
59
+ "test": "vitest run",
60
+ "test:watch": "vitest",
61
+ "lint": "eslint src",
62
+ "typecheck": "tsc --noEmit"
63
+ }
64
+ ```
65
+
66
+ ## `tsconfig.json` — points clés
67
+
68
+ ```json
69
+ {
70
+ "compilerOptions": {
71
+ "target": "ES2022",
72
+ "module": "ESNext",
73
+ "moduleResolution": "bundler",
74
+ "strict": true,
75
+ "noUncheckedIndexedAccess": true,
76
+ "esModuleInterop": true,
77
+ "paths": { "@/*": ["./src/*"] },
78
+ "outDir": "dist",
79
+ "rootDir": "src"
80
+ }
81
+ }
82
+ ```
83
+
84
+ ## Dockerfile — structure attendue
85
+
86
+ ```dockerfile
87
+ FROM node:22-slim AS builder
88
+ WORKDIR /app
89
+ COPY package*.json ./
90
+ RUN npm ci
91
+ COPY . .
92
+ RUN npm run build
93
+
94
+ FROM node:22-slim AS runtime
95
+ WORKDIR /app
96
+ COPY --from=builder /app/dist ./dist
97
+ COPY --from=builder /app/node_modules ./node_modules
98
+ COPY --from=builder /app/docs ./docs
99
+ ENV NODE_ENV=production
100
+ CMD ["node", "dist/main.js"]
101
+ ```
102
+
103
+ ## Acceptance Criteria
104
+
105
+ - [ ] `yarn typecheck` passe sans erreur sur un `src/main.ts` vide
106
+ - [ ] `yarn test` passe (0 tests = succès)
107
+ - [ ] `yarn lint` passe sans erreur
108
+ - [ ] `yarn build` produit un `dist/` valide
109
+ - [ ] `docker build .` réussit
110
+ - [ ] Structure de dossiers conforme au plan
@@ -0,0 +1,173 @@
1
+ ---
2
+ title: "F2 — Domain Layer"
3
+ type: feature
4
+ status: todo
5
+ prd: .tasks/PRD.md
6
+ depends_on: F1
7
+ ---
8
+
9
+ # F2 — Domain Layer
10
+
11
+ ## Objectif
12
+
13
+ Implémenter la couche domain pure : entités, value objects immuables, erreurs typées et ports (interfaces). Aucun import externe sauf `zod`. Tout doit être testable sans mock.
14
+
15
+ ## Fichiers à créer
16
+
17
+ ### Entities (`src/domain/entities/`)
18
+
19
+ **`Conversation.ts`** — Aggregate root (thread)
20
+ - Propriétés : `threadId: ThreadId`, `messages: ConversationMessage[]`
21
+ - Invariants : max `CONVERSATION_MAX_MESSAGES` messages (LRU — purge les plus anciens)
22
+ - Méthodes : `append(message): Conversation`, `purge(): Conversation`
23
+ - Constructeur privé + factory statique `create(threadId): Conversation`
24
+
25
+ **`ConversationMessage.ts`** — Entity
26
+ - Propriétés : `role: 'user' | 'assistant'`, `content: string`, `timestamp: Date`
27
+ - Constructeur privé + factory `create(role, content): ConversationMessage`
28
+
29
+ **`QueryResult.ts`** — Value object (résultat SQL)
30
+ - Propriétés : `columns: string[]`, `rows: Record<string, unknown>[]`, `rowCount: number`
31
+ - Constructeur privé + factory `create(columns, rows): QueryResult`
32
+
33
+ ### Value Objects (`src/domain/value-objects/`)
34
+
35
+ **`ThreadId.ts`** — branded string
36
+ ```typescript
37
+ type ThreadId = string & { readonly __brand: 'ThreadId' };
38
+ // factory: ThreadId.create(raw: string): ThreadId
39
+ ```
40
+
41
+ **`Question.ts`** — texte + flags parsés
42
+ - Propriétés : `raw: string`, `text: string` (stripped), `flags: QuestionFlags`
43
+ - Factory `create(raw: string): Question` — délègue le parsing à `QuestionFlags`
44
+
45
+ **`QuestionFlags.ts`** — flags extraits du texte
46
+ - Propriétés : `showSql: boolean`, `debug: boolean`, `noContext: boolean`
47
+ - Factory `parse(text: string): QuestionFlags` — regex sur `--sql`, `--debug`, `--no-context`
48
+
49
+ **`SqlQuery.ts`** — SQL validé, constructeur privé
50
+ ```typescript
51
+ class SqlQuery {
52
+ private constructor(public readonly raw: string) {}
53
+ static create(raw: string, validator: SqlValidator): SqlQuery {
54
+ return new SqlQuery(validator.validate(raw));
55
+ }
56
+ }
57
+ ```
58
+
59
+ **`LLMProviderName.ts`** — enum
60
+ ```typescript
61
+ enum LLMProviderName { ANTHROPIC = 'anthropic', OPENAI = 'openai' }
62
+ ```
63
+
64
+ **`ResponseRendering.ts`** — enum
65
+ ```typescript
66
+ enum ResponseRendering { TEXT = 'TEXT', TABLE = 'TABLE', CSV = 'CSV' }
67
+ ```
68
+
69
+ ### Errors (`src/domain/errors/`)
70
+
71
+ **`DomainError.ts`** — base abstraite
72
+ ```typescript
73
+ abstract class DomainError extends Error {
74
+ abstract readonly code: string;
75
+ }
76
+ ```
77
+
78
+ **`InvalidSqlError.ts`** — SQL refusé par le validator
79
+ **`SqlExecutionError.ts`** — erreur PG à l'exécution
80
+ **`LLMError.ts`** — erreur API LLM
81
+ **`SchemaLoadError.ts`** — échec lecture docs/schemas (fatal au boot)
82
+ **`AgentLoopExceededError.ts`** — MAX_TURNS atteint
83
+
84
+ ### Ports (`src/domain/ports/`)
85
+
86
+ **`LLMProvider.ts`**
87
+ ```typescript
88
+ interface LLMProvider {
89
+ generate(request: LLMRequest): Promise<LLMResponse>;
90
+ }
91
+ // types LLMRequest, LLMResponse, LLMToolUse définis ici
92
+ ```
93
+
94
+ **`SqlExecutor.ts`**
95
+ ```typescript
96
+ interface SqlExecutor {
97
+ execute(query: SqlQuery): Promise<QueryResult>;
98
+ }
99
+ ```
100
+
101
+ **`SqlValidator.ts`**
102
+ ```typescript
103
+ interface SqlValidator {
104
+ validate(raw: string): string; // retourne SQL nettoyé ou throw InvalidSqlError
105
+ }
106
+ ```
107
+
108
+ **`ConversationRepository.ts`**
109
+ ```typescript
110
+ interface ConversationRepository {
111
+ get(threadId: ThreadId): Promise<Conversation | null>;
112
+ save(conversation: Conversation): Promise<void>;
113
+ purge(threadId: ThreadId): Promise<void>;
114
+ }
115
+ ```
116
+
117
+ **`SchemaCatalog.ts`**
118
+ ```typescript
119
+ interface SchemaCatalog {
120
+ getDocumentation(): string;
121
+ }
122
+ ```
123
+
124
+ **`SlackMessenger.ts`**
125
+ ```typescript
126
+ interface SlackMessenger {
127
+ post(threadId: ThreadId, text: string): Promise<void>;
128
+ postCsv(threadId: ThreadId, text: string, csvBuffer: Buffer, filename: string): Promise<void>;
129
+ }
130
+ ```
131
+
132
+ **`AdminLogger.ts`**
133
+ ```typescript
134
+ interface AdminLogger {
135
+ logQuery(entry: AdminLogEntry): Promise<void>;
136
+ }
137
+ // type AdminLogEntry: { userId, question, sqls, rowCount, durationMs, inputTokens, outputTokens }
138
+ ```
139
+
140
+ **`Logger.ts`**
141
+ ```typescript
142
+ interface Logger {
143
+ debug(msg: string, meta?: object): void;
144
+ info(msg: string, meta?: object): void;
145
+ warn(msg: string, meta?: object): void;
146
+ error(msg: string, meta?: object): void;
147
+ }
148
+ ```
149
+
150
+ ## Tests unitaires (`src/domain/__tests__/`)
151
+
152
+ **`Conversation.test.ts`**
153
+ - `append` plafonne à `CONVERSATION_MAX_MESSAGES` messages (purge les plus anciens)
154
+ - `purge` retourne une conversation vide
155
+ - Constructeur privé — impossible d'instancier directement
156
+
157
+ **`Question.test.ts`**
158
+ - Strip de mention `<@U123>` du texte
159
+ - Parsing de `--sql`, `--debug`, `--no-context`
160
+ - Flags corrects quand aucun flag présent
161
+
162
+ **`SqlQuery.test.ts`**
163
+ - Impossible de créer un `SqlQuery` sans passer par le validator
164
+ - `SqlQuery.create` appelle `validator.validate` avec le SQL brut
165
+ - L'instance expose `.raw` avec le SQL retourné par le validator
166
+
167
+ ## Acceptance Criteria
168
+
169
+ - [ ] `yarn typecheck` passe
170
+ - [ ] `yarn test` : tous les tests domain passent
171
+ - [ ] Aucun import externe dans `src/domain/` sauf `zod` (si utilisé pour validation interne)
172
+ - [ ] `SqlQuery` n'est instanciable que via `SqlQuery.create(raw, validator)`
173
+ - [ ] `DomainError` est la base de toutes les erreurs du projet
@@ -0,0 +1,166 @@
1
+ ---
2
+ title: "F3 — Application Layer"
3
+ type: feature
4
+ status: todo
5
+ prd: .tasks/PRD.md
6
+ depends_on: F2
7
+ ---
8
+
9
+ # F3 — Application Layer
10
+
11
+ ## Objectif
12
+
13
+ Implémenter les use cases, la boucle agentique et les formatters de réponse. Cette couche orchestre le domain sans dépendre d'aucun SDK tiers — uniquement des ports définis dans `src/domain/ports/`.
14
+
15
+ ## Fichiers à créer
16
+
17
+ ### Use Cases (`src/application/usecases/`)
18
+
19
+ **`ParseQuestion.ts`**
20
+ - Input : `rawText: string` (message Slack brut)
21
+ - Output : `Question`
22
+ - Logique : strip `<@U…>` via regex, crée `Question.create(cleaned)`
23
+ - Pas d'effet de bord, pas de dépendance externe
24
+
25
+ **`AnswerQuestion.ts`** — Use case principal
26
+ ```typescript
27
+ class AnswerQuestion {
28
+ constructor(
29
+ private readonly conversationRepository: ConversationRepository,
30
+ private readonly agentLoop: AgentLoop,
31
+ private readonly responseRenderer: ResponseRenderer,
32
+ private readonly slackMessenger: SlackMessenger,
33
+ private readonly adminLogger: AdminLogger,
34
+ ) {}
35
+
36
+ async execute(question: Question, threadId: ThreadId): Promise<void>
37
+ }
38
+ ```
39
+ Flow :
40
+ 1. `conversationRepository.get(threadId)` si `!question.flags.noContext`
41
+ 2. `agentLoop.run({ conversation, question })` → `AgentRunResult`
42
+ 3. `conversationRepository.save(updatedConversation)`
43
+ 4. `responseRenderer.render(agentRunResult)` → `RenderedResponse`
44
+ 5. `slackMessenger.post(threadId, ...)` ou `postCsv` selon le rendu
45
+ 6. `adminLogger.logQuery({ ... })`
46
+
47
+ ### Agent (`src/application/agent/`)
48
+
49
+ **`AgentContext.ts`** — State mutable de la boucle
50
+ - Propriétés : `messages: LLMMessage[]` (historique + tours courants)
51
+ - Méthodes : `appendUserMessage(text)`, `appendAssistantMessage(response)`, `appendToolResult(toolUseId, result | error)`
52
+ - Initialisé depuis la `Conversation` existante + la question courante
53
+
54
+ **`AgentLoop.ts`** — Boucle `tool_use` → `end_turn`
55
+ ```typescript
56
+ class AgentLoop {
57
+ constructor(
58
+ private readonly llmProvider: LLMProvider,
59
+ private readonly sqlValidator: SqlValidator,
60
+ private readonly sqlExecutor: SqlExecutor,
61
+ private readonly schemaCatalog: SchemaCatalog,
62
+ private readonly maxTurns: number,
63
+ ) {}
64
+
65
+ async run(params: { conversation: Conversation | null; question: Question }): Promise<AgentRunResult>
66
+ }
67
+ ```
68
+ Algorithme :
69
+ ```
70
+ context = AgentContext.init(conversation, question)
71
+ for (turn = 0; turn < maxTurns; turn++):
72
+ response = await llmProvider.generate({
73
+ system: SystemPromptBuilder.build(schemaCatalog.getDocumentation()),
74
+ messages: context.messages,
75
+ tools: [RunSqlTool.definition],
76
+ })
77
+ context.appendAssistantMessage(response)
78
+ if response.stopReason === 'end_turn':
79
+ return { finalText: response.text, sqls: context.executedSqls, queryResult: context.lastQueryResult }
80
+ if response.toolUse?.name === 'run_sql':
81
+ try:
82
+ query = SqlQuery.create(response.toolUse.input.sql, sqlValidator)
83
+ result = await sqlExecutor.execute(query)
84
+ context.appendToolResult(response.toolUse.id, result)
85
+ context.recordSql(query.raw, result)
86
+ catch (e):
87
+ context.appendToolResult(response.toolUse.id, e, isError: true)
88
+ throw new AgentLoopExceededError(maxTurns)
89
+ ```
90
+
91
+ **`tools/RunSqlTool.ts`** — Définition JSON du tool Anthropic
92
+ ```typescript
93
+ const RunSqlTool = {
94
+ definition: {
95
+ name: 'run_sql',
96
+ description: 'Execute a read-only SQL SELECT query on the efarmz PostgreSQL databases.',
97
+ input_schema: {
98
+ type: 'object',
99
+ properties: {
100
+ sql: { type: 'string', description: 'The SQL SELECT query to execute' },
101
+ },
102
+ required: ['sql'],
103
+ },
104
+ },
105
+ } as const;
106
+ ```
107
+
108
+ ### Formatting (`src/application/formatting/`)
109
+
110
+ **`ResponseRenderer.ts`** — Délègue aux renderers spécialisés
111
+ ```typescript
112
+ class ResponseRenderer {
113
+ render(params: {
114
+ finalText: string;
115
+ queryResult: QueryResult | null;
116
+ executedSqls: string[];
117
+ flags: QuestionFlags;
118
+ }): RenderedResponse
119
+ }
120
+ // RenderedResponse = { text: string; csvBuffer?: Buffer; csvFilename?: string }
121
+ ```
122
+ Décision :
123
+ - Pas de `queryResult` → retourner `finalText` tel quel
124
+ - 0 ligne → `"Aucun résultat."`
125
+ - 1×1 → `ScalarRenderer`
126
+ - ≤ `SQL_CSV_THRESHOLD` → `MonospaceTableRenderer`
127
+ - > `SQL_CSV_THRESHOLD` → `MonospaceTableRenderer` (10 premières) + `CsvRenderer` → buffer
128
+
129
+ **`ScalarRenderer.ts`**
130
+ - Retourne `**{valeur}**` (valeur en gras Slack markdown)
131
+
132
+ **`MonospaceTableRenderer.ts`**
133
+ - Aligne les colonnes avec padding
134
+ - Retourne un bloc code Slack ` ```\ncol1 col2\n...``` `
135
+ - Tronque à N lignes si demandé
136
+
137
+ **`CsvRenderer.ts`**
138
+ - Génère un `Buffer` CSV (header + lignes, séparateur `,`, valeurs entre `"` si nécessaire)
139
+
140
+ ## Tests unitaires (`src/application/__tests__/`)
141
+
142
+ **`ParseQuestion.test.ts`**
143
+ - Strip `<@U123456>` du texte
144
+ - Gère plusieurs mentions
145
+ - Flags `--sql`, `--debug`, `--no-context` extraits correctement
146
+
147
+ **`AgentLoop.test.ts`** (avec `FakeLLMProvider`)
148
+ - `end_turn` immédiat → retourne `finalText`
149
+ - 1 tour `run_sql` → appelle `sqlExecutor.execute`, puis `end_turn`
150
+ - `run_sql` avec erreur SQL → `tool_result.is_error = true`, LLM reçoit l'erreur au tour suivant
151
+ - `MAX_TURNS` atteint → throw `AgentLoopExceededError`
152
+
153
+ **`ResponseRenderer.test.ts`** (sur fixtures)
154
+ - 0 ligne → `"Aucun résultat."`
155
+ - 1×1 → valeur en gras
156
+ - 5 lignes → table monospace (pas de CSV)
157
+ - 20 lignes → table tronquée + `csvBuffer` non-null
158
+ - `--sql` → SQL ajouté en bas du message
159
+
160
+ ## Acceptance Criteria
161
+
162
+ - [ ] `yarn typecheck` passe
163
+ - [ ] `yarn test` : tous les tests application passent
164
+ - [ ] Aucun import de SDK tiers dans `src/application/` (uniquement `src/domain/`)
165
+ - [ ] `AgentLoop` ne throw jamais sur erreur SQL — renvoie toujours `tool_result.is_error`
166
+ - [ ] `ResponseRenderer` couvre les 4 cas de rendu (0, scalar, table, CSV)