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
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
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,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)
|