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,16 @@
|
|
|
1
|
+
# Schema Documentation Example
|
|
2
|
+
|
|
3
|
+
This is a placeholder schema documentation file. Replace with actual efarmz database schema documentation.
|
|
4
|
+
|
|
5
|
+
## Tables
|
|
6
|
+
|
|
7
|
+
### Example Table
|
|
8
|
+
- Column 1: Description
|
|
9
|
+
- Column 2: Description
|
|
10
|
+
|
|
11
|
+
## Queries
|
|
12
|
+
|
|
13
|
+
Common queries for analysis:
|
|
14
|
+
```sql
|
|
15
|
+
SELECT * FROM example_table LIMIT 100;
|
|
16
|
+
```
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import eslint from '@eslint/js';
|
|
2
|
+
import tseslint from 'typescript-eslint';
|
|
3
|
+
|
|
4
|
+
export default tseslint.config(
|
|
5
|
+
eslint.configs.recommended,
|
|
6
|
+
...tseslint.configs.strictTypeChecked,
|
|
7
|
+
{
|
|
8
|
+
languageOptions: {
|
|
9
|
+
parserOptions: {
|
|
10
|
+
projectService: true,
|
|
11
|
+
tsconfigRootDir: import.meta.dirname,
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
ignores: ['dist/', 'node_modules/'],
|
|
17
|
+
}
|
|
18
|
+
);
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "efarmz-slackbot-data",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Bot Slack qui traduit des questions en langage naturel vers des requêtes SQL via LLM",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "tsx watch src/main.ts",
|
|
8
|
+
"build": "tsc && tsc-alias --resolve-full-paths",
|
|
9
|
+
"start": "node dist/main.js",
|
|
10
|
+
"test": "vitest run",
|
|
11
|
+
"test:watch": "vitest",
|
|
12
|
+
"lint": "eslint src",
|
|
13
|
+
"typecheck": "tsc --noEmit"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"slack",
|
|
17
|
+
"llm",
|
|
18
|
+
"sql",
|
|
19
|
+
"anthropic"
|
|
20
|
+
],
|
|
21
|
+
"author": "Benjamin Marguerite",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@anthropic-ai/sdk": "^0.28.0",
|
|
25
|
+
"@slack/bolt": "^3.20.0",
|
|
26
|
+
"dotenv": "^17.4.2",
|
|
27
|
+
"pg": "^8.12.0",
|
|
28
|
+
"pino": "^9.5.0",
|
|
29
|
+
"zod": "^3.24.1"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@eslint/js": "^9.14.0",
|
|
33
|
+
"@semantic-release/changelog": "^6.0.2",
|
|
34
|
+
"@semantic-release/commit-analyzer": "^9.0.2",
|
|
35
|
+
"@semantic-release/git": "^10.0.1",
|
|
36
|
+
"@semantic-release/github": "^10.0.3",
|
|
37
|
+
"@semantic-release/npm": "^9.0.2",
|
|
38
|
+
"@semantic-release/release-notes-generator": "^10.0.3",
|
|
39
|
+
"@types/node": "^22.9.0",
|
|
40
|
+
"@types/pg": "^8.12.1",
|
|
41
|
+
"@typescript-eslint/eslint-plugin": "^8.14.0",
|
|
42
|
+
"@typescript-eslint/parser": "^8.14.0",
|
|
43
|
+
"eslint": "^9.14.0",
|
|
44
|
+
"prettier": "^3.3.3",
|
|
45
|
+
"tsc-alias": "^1.8.17",
|
|
46
|
+
"tsx": "^4.19.2",
|
|
47
|
+
"typescript": "^5.6.3",
|
|
48
|
+
"typescript-eslint": "^8.14.0",
|
|
49
|
+
"vitest": "^2.1.8"
|
|
50
|
+
},
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=22.0.0"
|
|
53
|
+
}
|
|
54
|
+
}
|
package/releaserc.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"plugins": [
|
|
3
|
+
"@semantic-release/commit-analyzer",
|
|
4
|
+
"@semantic-release/changelog",
|
|
5
|
+
"@semantic-release/release-notes-generator",
|
|
6
|
+
"@semantic-release/git",
|
|
7
|
+
[
|
|
8
|
+
"@semantic-release/github",
|
|
9
|
+
{
|
|
10
|
+
"successComment": false,
|
|
11
|
+
"failTitle": false
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
]
|
|
15
|
+
}
|
package/src/.gitkeep
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import AgentContext from "../agent/AgentContext.js";
|
|
3
|
+
import Conversation from "@/domain/entities/Conversation.js";
|
|
4
|
+
import ConversationMessage from "@/domain/entities/ConversationMessage.js";
|
|
5
|
+
import Question from "@/domain/value-objects/Question.js";
|
|
6
|
+
import QueryResult from "@/domain/value-objects/QueryResult.js";
|
|
7
|
+
import ThreadId from "@/domain/value-objects/ThreadId.js";
|
|
8
|
+
|
|
9
|
+
describe(`AgentContext`, () => {
|
|
10
|
+
describe(`init()`, () => {
|
|
11
|
+
it(`should create context with single user message when no conversation exists`, () => {
|
|
12
|
+
const question = Question.create(`How many orders yesterday?`);
|
|
13
|
+
const ctx = AgentContext.init(null, question);
|
|
14
|
+
|
|
15
|
+
expect(ctx.messages).toHaveLength(1);
|
|
16
|
+
expect(ctx.messages[0]).toEqual({
|
|
17
|
+
role: `user`,
|
|
18
|
+
content: `How many orders yesterday?`,
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it(`should preserve conversation message order when creating context`, () => {
|
|
23
|
+
const threadId = ThreadId.create(`C0123456789`, `1234567890.123456`);
|
|
24
|
+
let conversation = Conversation.create(threadId);
|
|
25
|
+
conversation = conversation.append(
|
|
26
|
+
ConversationMessage.create(`user`, `First question`),
|
|
27
|
+
);
|
|
28
|
+
conversation = conversation.append(
|
|
29
|
+
ConversationMessage.create(`assistant`, `First answer`),
|
|
30
|
+
);
|
|
31
|
+
conversation = conversation.append(
|
|
32
|
+
ConversationMessage.create(`user`, `Second question`),
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const question = Question.create(`Third question`);
|
|
36
|
+
const ctx = AgentContext.init(conversation, question);
|
|
37
|
+
|
|
38
|
+
expect(ctx.messages).toHaveLength(4);
|
|
39
|
+
expect(ctx.messages[0]).toEqual({
|
|
40
|
+
role: `user`,
|
|
41
|
+
content: `First question`,
|
|
42
|
+
});
|
|
43
|
+
expect(ctx.messages[1]).toEqual({
|
|
44
|
+
role: `assistant`,
|
|
45
|
+
content: `First answer`,
|
|
46
|
+
});
|
|
47
|
+
expect(ctx.messages[2]).toEqual({
|
|
48
|
+
role: `user`,
|
|
49
|
+
content: `Second question`,
|
|
50
|
+
});
|
|
51
|
+
expect(ctx.messages[3]).toEqual({
|
|
52
|
+
role: `user`,
|
|
53
|
+
content: `Third question`,
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it(`should initialize with zero tokens`, () => {
|
|
58
|
+
const question = Question.create(`Test`);
|
|
59
|
+
const ctx = AgentContext.init(null, question);
|
|
60
|
+
|
|
61
|
+
expect(ctx.totalInputTokens).toBe(0);
|
|
62
|
+
expect(ctx.totalOutputTokens).toBe(0);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it(`should initialize with empty executed sqls`, () => {
|
|
66
|
+
const question = Question.create(`Test`);
|
|
67
|
+
const ctx = AgentContext.init(null, question);
|
|
68
|
+
|
|
69
|
+
expect(ctx.executedSqls).toHaveLength(0);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it(`should initialize with null last query result`, () => {
|
|
73
|
+
const question = Question.create(`Test`);
|
|
74
|
+
const ctx = AgentContext.init(null, question);
|
|
75
|
+
|
|
76
|
+
expect(ctx.lastQueryResult).toBeNull();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe(`appendAssistantMessage()`, () => {
|
|
81
|
+
it(`should add assistant message to messages array`, () => {
|
|
82
|
+
const question = Question.create(`Test`);
|
|
83
|
+
const ctx = AgentContext.init(null, question);
|
|
84
|
+
|
|
85
|
+
ctx.appendAssistantMessage(`This is an answer`, 100, 50);
|
|
86
|
+
|
|
87
|
+
expect(ctx.messages).toHaveLength(2);
|
|
88
|
+
expect(ctx.messages[1]).toEqual({
|
|
89
|
+
role: `assistant`,
|
|
90
|
+
content: `This is an answer`,
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it(`should increment token counters`, () => {
|
|
95
|
+
const question = Question.create(`Test`);
|
|
96
|
+
const ctx = AgentContext.init(null, question);
|
|
97
|
+
|
|
98
|
+
ctx.appendAssistantMessage(`Answer 1`, 100, 50);
|
|
99
|
+
ctx.appendAssistantMessage(`Answer 2`, 80, 40);
|
|
100
|
+
|
|
101
|
+
expect(ctx.totalInputTokens).toBe(180);
|
|
102
|
+
expect(ctx.totalOutputTokens).toBe(90);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it(`should accumulate tokens correctly across multiple calls`, () => {
|
|
106
|
+
const question = Question.create(`Test`);
|
|
107
|
+
const ctx = AgentContext.init(null, question);
|
|
108
|
+
|
|
109
|
+
ctx.appendAssistantMessage(`Answer 1`, 100, 50);
|
|
110
|
+
expect(ctx.totalInputTokens).toBe(100);
|
|
111
|
+
expect(ctx.totalOutputTokens).toBe(50);
|
|
112
|
+
|
|
113
|
+
ctx.appendAssistantMessage(`Answer 2`, 100, 50);
|
|
114
|
+
expect(ctx.totalInputTokens).toBe(200);
|
|
115
|
+
expect(ctx.totalOutputTokens).toBe(100);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe(`appendToolResult()`, () => {
|
|
120
|
+
it(`should add tool result message with isError false`, () => {
|
|
121
|
+
const question = Question.create(`Test`);
|
|
122
|
+
const ctx = AgentContext.init(null, question);
|
|
123
|
+
|
|
124
|
+
ctx.appendToolResult(`tool_123`, `Query executed successfully`, false);
|
|
125
|
+
|
|
126
|
+
expect(ctx.messages).toHaveLength(2);
|
|
127
|
+
expect(ctx.messages[1]).toEqual({
|
|
128
|
+
role: `user`,
|
|
129
|
+
content: {
|
|
130
|
+
type: `tool_result`,
|
|
131
|
+
toolUseId: `tool_123`,
|
|
132
|
+
content: `Query executed successfully`,
|
|
133
|
+
isError: false,
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it(`should add tool result message with isError true`, () => {
|
|
139
|
+
const question = Question.create(`Test`);
|
|
140
|
+
const ctx = AgentContext.init(null, question);
|
|
141
|
+
|
|
142
|
+
ctx.appendToolResult(`tool_456`, `SQL syntax error`, true);
|
|
143
|
+
|
|
144
|
+
expect(ctx.messages).toHaveLength(2);
|
|
145
|
+
const msg = ctx.messages[1];
|
|
146
|
+
if (!msg) throw new Error(`Expected message at index 1`);
|
|
147
|
+
expect(msg.role).toBe(`user`);
|
|
148
|
+
if (typeof msg.content !== `string`) {
|
|
149
|
+
expect(msg.content.isError).toBe(true);
|
|
150
|
+
expect(msg.content.content).toBe(`SQL syntax error`);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe(`recordSql()`, () => {
|
|
156
|
+
it(`should add sql to executed sqls list`, () => {
|
|
157
|
+
const question = Question.create(`Test`);
|
|
158
|
+
const ctx = AgentContext.init(null, question);
|
|
159
|
+
|
|
160
|
+
const result = QueryResult.create([`id`, `name`], [
|
|
161
|
+
{ id: 1, name: `Alice` },
|
|
162
|
+
]);
|
|
163
|
+
ctx.recordSql(`SELECT * FROM users`, result);
|
|
164
|
+
|
|
165
|
+
expect(ctx.executedSqls).toHaveLength(1);
|
|
166
|
+
expect(ctx.executedSqls[0]).toBe(`SELECT * FROM users`);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it(`should update last query result`, () => {
|
|
170
|
+
const question = Question.create(`Test`);
|
|
171
|
+
const ctx = AgentContext.init(null, question);
|
|
172
|
+
|
|
173
|
+
const result1 = QueryResult.create([`id`], [{ id: 1 }]);
|
|
174
|
+
ctx.recordSql(`SELECT 1`, result1);
|
|
175
|
+
|
|
176
|
+
expect(ctx.lastQueryResult).toBe(result1);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it(`should replace last query result when new sql is recorded`, () => {
|
|
180
|
+
const question = Question.create(`Test`);
|
|
181
|
+
const ctx = AgentContext.init(null, question);
|
|
182
|
+
|
|
183
|
+
const result1 = QueryResult.create([`id`], [{ id: 1 }]);
|
|
184
|
+
const result2 = QueryResult.create([`id`], [{ id: 2 }, { id: 3 }]);
|
|
185
|
+
|
|
186
|
+
ctx.recordSql(`SELECT 1`, result1);
|
|
187
|
+
expect(ctx.lastQueryResult).toBe(result1);
|
|
188
|
+
|
|
189
|
+
ctx.recordSql(`SELECT 2, 3`, result2);
|
|
190
|
+
expect(ctx.lastQueryResult).toBe(result2);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it(`should track multiple sqls in order`, () => {
|
|
194
|
+
const question = Question.create(`Test`);
|
|
195
|
+
const ctx = AgentContext.init(null, question);
|
|
196
|
+
|
|
197
|
+
const result = QueryResult.create([], []);
|
|
198
|
+
ctx.recordSql(`SELECT 1`, result);
|
|
199
|
+
ctx.recordSql(`SELECT 2`, result);
|
|
200
|
+
ctx.recordSql(`SELECT 3`, result);
|
|
201
|
+
|
|
202
|
+
expect(ctx.executedSqls).toEqual([`SELECT 1`, `SELECT 2`, `SELECT 3`]);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe(`integration`, () => {
|
|
207
|
+
it(`should accumulate state correctly through a full agent loop simulation`, () => {
|
|
208
|
+
const threadId = ThreadId.create(`C0123456789`, `1234567890.123456`);
|
|
209
|
+
let conversation = Conversation.create(threadId);
|
|
210
|
+
conversation = conversation.append(
|
|
211
|
+
ConversationMessage.create(`user`, `Previous question`),
|
|
212
|
+
);
|
|
213
|
+
conversation = conversation.append(
|
|
214
|
+
ConversationMessage.create(`assistant`, `Previous answer`),
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
const question = Question.create(`New question`);
|
|
218
|
+
const ctx = AgentContext.init(conversation, question);
|
|
219
|
+
|
|
220
|
+
// Simulate first LLM turn
|
|
221
|
+
ctx.appendAssistantMessage(
|
|
222
|
+
`I will run a query to answer this.`,
|
|
223
|
+
150,
|
|
224
|
+
100,
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
// Simulate tool use and result
|
|
228
|
+
const queryResult = QueryResult.create(
|
|
229
|
+
[`count`],
|
|
230
|
+
[{ count: 42 }],
|
|
231
|
+
);
|
|
232
|
+
ctx.recordSql(`SELECT COUNT(*) as count FROM orders`, queryResult);
|
|
233
|
+
ctx.appendToolResult(`tool_use_1`, `42`, false);
|
|
234
|
+
|
|
235
|
+
// Simulate final turn
|
|
236
|
+
ctx.appendAssistantMessage(
|
|
237
|
+
`There are 42 orders.`,
|
|
238
|
+
100,
|
|
239
|
+
60,
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
// Verify final state
|
|
243
|
+
expect(ctx.messages).toHaveLength(6); // 2 from conversation + 1 initial question + 1 assistant answer + 1 tool result + 1 final answer
|
|
244
|
+
expect(ctx.totalInputTokens).toBe(250);
|
|
245
|
+
expect(ctx.totalOutputTokens).toBe(160);
|
|
246
|
+
expect(ctx.executedSqls).toEqual([
|
|
247
|
+
`SELECT COUNT(*) as count FROM orders`,
|
|
248
|
+
]);
|
|
249
|
+
expect(ctx.lastQueryResult).toBe(queryResult);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it(`should return readonly views of internal arrays`, () => {
|
|
253
|
+
const question = Question.create(`Test`);
|
|
254
|
+
const ctx = AgentContext.init(null, question);
|
|
255
|
+
|
|
256
|
+
const messages = ctx.messages as unknown[];
|
|
257
|
+
const sqls = ctx.executedSqls as unknown[];
|
|
258
|
+
|
|
259
|
+
expect(Array.isArray(messages)).toBe(true);
|
|
260
|
+
expect(Array.isArray(sqls)).toBe(true);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import Conversation from "@/domain/entities/Conversation.js";
|
|
2
|
+
import Question from "@/domain/value-objects/Question.js";
|
|
3
|
+
import type QueryResult from "@/domain/value-objects/QueryResult.js";
|
|
4
|
+
import type LLMMessage from "./LLMMessage.js";
|
|
5
|
+
|
|
6
|
+
class AgentContext {
|
|
7
|
+
private readonly _messages: LLMMessage[] = [];
|
|
8
|
+
private readonly _executedSqls: string[] = [];
|
|
9
|
+
private _lastQueryResult: QueryResult | null = null;
|
|
10
|
+
private _totalInputTokens = 0;
|
|
11
|
+
private _totalOutputTokens = 0;
|
|
12
|
+
|
|
13
|
+
private constructor() {}
|
|
14
|
+
|
|
15
|
+
static init(conversation: Conversation | null, question: Question): AgentContext {
|
|
16
|
+
const ctx = new AgentContext();
|
|
17
|
+
|
|
18
|
+
if (conversation) {
|
|
19
|
+
for (const msg of conversation.messages) {
|
|
20
|
+
ctx._messages.push({
|
|
21
|
+
role: msg.role as `user` | `assistant`,
|
|
22
|
+
content: msg.content,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
ctx._messages.push({
|
|
28
|
+
role: `user`,
|
|
29
|
+
content: question.text,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return ctx;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
appendUserMessage(content: string): void {
|
|
36
|
+
this._messages.push({
|
|
37
|
+
role: `user`,
|
|
38
|
+
content,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
appendAssistantMessage(
|
|
43
|
+
content: string,
|
|
44
|
+
inputTokens: number,
|
|
45
|
+
outputTokens: number,
|
|
46
|
+
): void {
|
|
47
|
+
this._messages.push({
|
|
48
|
+
role: `assistant`,
|
|
49
|
+
content,
|
|
50
|
+
});
|
|
51
|
+
this._totalInputTokens += inputTokens;
|
|
52
|
+
this._totalOutputTokens += outputTokens;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
appendToolResult(toolUseId: string, content: string, isError: boolean): void {
|
|
56
|
+
this._messages.push({
|
|
57
|
+
role: `user`,
|
|
58
|
+
content: {
|
|
59
|
+
type: `tool_result`,
|
|
60
|
+
toolUseId,
|
|
61
|
+
content,
|
|
62
|
+
isError,
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
recordSql(sql: string, result: QueryResult): void {
|
|
68
|
+
this._executedSqls.push(sql);
|
|
69
|
+
this._lastQueryResult = result;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
get messages(): readonly LLMMessage[] {
|
|
73
|
+
return this._messages;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
get executedSqls(): readonly string[] {
|
|
77
|
+
return this._executedSqls;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
get lastQueryResult(): QueryResult | null {
|
|
81
|
+
return this._lastQueryResult;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
get totalInputTokens(): number {
|
|
85
|
+
return this._totalInputTokens;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
get totalOutputTokens(): number {
|
|
89
|
+
return this._totalOutputTokens;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export default AgentContext;
|