@api-client/core 0.19.1 → 0.19.2
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/build/src/mocking/ModelingMock.d.ts +2 -0
- package/build/src/mocking/ModelingMock.d.ts.map +1 -1
- package/build/src/mocking/ModelingMock.js +2 -0
- package/build/src/mocking/ModelingMock.js.map +1 -1
- package/build/src/mocking/lib/Ai.d.ts +11 -0
- package/build/src/mocking/lib/Ai.d.ts.map +1 -0
- package/build/src/mocking/lib/Ai.js +53 -0
- package/build/src/mocking/lib/Ai.js.map +1 -0
- package/build/src/modeling/ai/DataDomainDelta.d.ts +146 -0
- package/build/src/modeling/ai/DataDomainDelta.d.ts.map +1 -0
- package/build/src/modeling/ai/DataDomainDelta.js +729 -0
- package/build/src/modeling/ai/DataDomainDelta.js.map +1 -0
- package/build/src/modeling/ai/DomainSerialization.d.ts +20 -0
- package/build/src/modeling/ai/DomainSerialization.d.ts.map +1 -0
- package/build/src/modeling/ai/DomainSerialization.js +185 -0
- package/build/src/modeling/ai/DomainSerialization.js.map +1 -0
- package/build/src/modeling/ai/domain_response_schema.d.ts +806 -0
- package/build/src/modeling/ai/domain_response_schema.d.ts.map +1 -0
- package/build/src/modeling/ai/domain_response_schema.js +289 -0
- package/build/src/modeling/ai/domain_response_schema.js.map +1 -0
- package/build/src/modeling/ai/domain_tools.d.ts +68 -0
- package/build/src/modeling/ai/domain_tools.d.ts.map +1 -0
- package/build/src/modeling/ai/domain_tools.js +71 -0
- package/build/src/modeling/ai/domain_tools.js.map +1 -0
- package/build/src/modeling/ai/index.d.ts +10 -0
- package/build/src/modeling/ai/index.d.ts.map +1 -0
- package/build/src/modeling/ai/index.js +9 -0
- package/build/src/modeling/ai/index.js.map +1 -0
- package/build/src/modeling/ai/message_parser.d.ts +23 -0
- package/build/src/modeling/ai/message_parser.d.ts.map +1 -0
- package/build/src/modeling/ai/message_parser.js +93 -0
- package/build/src/modeling/ai/message_parser.js.map +1 -0
- package/build/src/modeling/ai/prompts/domain_system.d.ts +6 -0
- package/build/src/modeling/ai/prompts/domain_system.d.ts.map +1 -0
- package/build/src/modeling/ai/prompts/domain_system.js +80 -0
- package/build/src/modeling/ai/prompts/domain_system.js.map +1 -0
- package/build/src/modeling/ai/tools/DataDomain.tools.d.ts +25 -0
- package/build/src/modeling/ai/tools/DataDomain.tools.d.ts.map +1 -0
- package/build/src/modeling/ai/tools/DataDomain.tools.js +334 -0
- package/build/src/modeling/ai/tools/DataDomain.tools.js.map +1 -0
- package/build/src/modeling/ai/tools/Semantic.tools.d.ts +48 -0
- package/build/src/modeling/ai/tools/Semantic.tools.d.ts.map +1 -0
- package/build/src/modeling/ai/tools/Semantic.tools.js +36 -0
- package/build/src/modeling/ai/tools/Semantic.tools.js.map +1 -0
- package/build/src/modeling/ai/tools/config.d.ts +13 -0
- package/build/src/modeling/ai/tools/config.d.ts.map +1 -0
- package/build/src/modeling/ai/tools/config.js +2 -0
- package/build/src/modeling/ai/tools/config.js.map +1 -0
- package/build/src/modeling/ai/types.d.ts +302 -0
- package/build/src/modeling/ai/types.d.ts.map +1 -0
- package/build/src/modeling/ai/types.js +40 -0
- package/build/src/modeling/ai/types.js.map +1 -0
- package/build/src/models/AiMessage.d.ts +185 -0
- package/build/src/models/AiMessage.d.ts.map +1 -0
- package/build/src/models/AiMessage.js +203 -0
- package/build/src/models/AiMessage.js.map +1 -0
- package/build/src/models/AiSession.d.ts +80 -0
- package/build/src/models/AiSession.d.ts.map +1 -0
- package/build/src/models/AiSession.js +102 -0
- package/build/src/models/AiSession.js.map +1 -0
- package/build/src/models/kinds.d.ts +2 -0
- package/build/src/models/kinds.d.ts.map +1 -1
- package/build/src/models/kinds.js +2 -0
- package/build/src/models/kinds.js.map +1 -1
- package/build/src/sdk/AiSdk.d.ts +93 -0
- package/build/src/sdk/AiSdk.d.ts.map +1 -0
- package/build/src/sdk/AiSdk.js +348 -0
- package/build/src/sdk/AiSdk.js.map +1 -0
- package/build/src/sdk/RouteBuilder.d.ts +7 -0
- package/build/src/sdk/RouteBuilder.d.ts.map +1 -1
- package/build/src/sdk/RouteBuilder.js +18 -0
- package/build/src/sdk/RouteBuilder.js.map +1 -1
- package/build/src/sdk/Sdk.d.ts +2 -0
- package/build/src/sdk/Sdk.d.ts.map +1 -1
- package/build/src/sdk/Sdk.js +2 -0
- package/build/src/sdk/Sdk.js.map +1 -1
- package/build/src/sdk/SdkBase.d.ts +4 -0
- package/build/src/sdk/SdkBase.d.ts.map +1 -1
- package/build/src/sdk/SdkBase.js.map +1 -1
- package/build/src/sdk/SdkMock.d.ts +15 -0
- package/build/src/sdk/SdkMock.d.ts.map +1 -1
- package/build/src/sdk/SdkMock.js +118 -0
- package/build/src/sdk/SdkMock.js.map +1 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/data/models/example-generator-api.json +22 -22
- package/package.json +3 -3
- package/src/mocking/ModelingMock.ts +2 -0
- package/src/mocking/lib/Ai.ts +71 -0
- package/src/modeling/ai/DataDomainDelta.ts +798 -0
- package/src/modeling/ai/DomainSerialization.ts +199 -0
- package/src/modeling/ai/domain_response_schema.ts +301 -0
- package/src/modeling/ai/domain_tools.ts +76 -0
- package/src/modeling/ai/message_parser.ts +101 -0
- package/src/modeling/ai/prompts/domain_system.ts +79 -0
- package/src/modeling/ai/readme.md +8 -0
- package/src/modeling/ai/tools/DataDomain.tools.ts +365 -0
- package/src/modeling/ai/tools/Semantic.tools.ts +38 -0
- package/src/modeling/ai/tools/config.ts +13 -0
- package/src/modeling/ai/tools/readme.md +3 -0
- package/src/modeling/ai/types.ts +306 -0
- package/src/models/AiMessage.ts +335 -0
- package/src/models/AiSession.ts +160 -0
- package/src/models/kinds.ts +2 -0
- package/src/sdk/AiSdk.ts +395 -0
- package/src/sdk/RouteBuilder.ts +27 -0
- package/src/sdk/Sdk.ts +3 -0
- package/src/sdk/SdkBase.ts +4 -0
- package/src/sdk/SdkMock.ts +185 -0
- package/tests/unit/mocking/current/Ai.spec.ts +109 -0
- package/tests/unit/modeling/ai/DataDomainDelta.spec.ts +419 -0
- package/tests/unit/modeling/ai/DomainAiTools.spec.ts +29 -0
- package/tests/unit/modeling/ai/DomainSerialization.spec.ts +143 -0
- package/tests/unit/modeling/ai/message_parser.spec.ts +157 -0
- package/tests/unit/modeling/ai/tools/DataDomain.tools.spec.ts +64 -0
- package/tests/unit/modeling/ai/tools/Semantic.tools.spec.ts +55 -0
- package/tests/unit/models/AiMessage.spec.ts +216 -0
- package/tests/unit/models/AiSession.spec.ts +147 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { test } from '@japa/runner'
|
|
2
|
+
import { parseAiChatMessage, detectReasoning } from '../../../../src/modeling/ai/message_parser.js'
|
|
3
|
+
import type { AiUserMessageSchema, AiModelMessageSchema } from '../../../../src/models/AiMessage.js'
|
|
4
|
+
import { AiMessageKind } from '../../../../src/models/kinds.js'
|
|
5
|
+
import { AiModelMessageWithDelta } from '../../../../src/modeling/ai/types.js'
|
|
6
|
+
|
|
7
|
+
test.group('Message Parser | parseAiChatMessage', () => {
|
|
8
|
+
test('returns user message as is', ({ assert }) => {
|
|
9
|
+
const userMsg: AiUserMessageSchema = {
|
|
10
|
+
kind: AiMessageKind,
|
|
11
|
+
key: 'msg1',
|
|
12
|
+
session: 'sess1',
|
|
13
|
+
createdAt: Date.now(),
|
|
14
|
+
updatedAt: Date.now(),
|
|
15
|
+
role: 'user',
|
|
16
|
+
state: 'complete',
|
|
17
|
+
text: 'Hello AI',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const result = parseAiChatMessage(userMsg)
|
|
21
|
+
assert.deepEqual(result, userMsg)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('returns model message as is if state is not complete', ({ assert }) => {
|
|
25
|
+
const modelMsg: AiModelMessageSchema = {
|
|
26
|
+
kind: AiMessageKind,
|
|
27
|
+
key: 'msg2',
|
|
28
|
+
session: 'sess1',
|
|
29
|
+
createdAt: Date.now(),
|
|
30
|
+
updatedAt: Date.now(),
|
|
31
|
+
role: 'model',
|
|
32
|
+
state: 'loading',
|
|
33
|
+
text: '{"reasoning": "thinking...',
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const result = parseAiChatMessage(modelMsg)
|
|
37
|
+
assert.deepEqual(result, modelMsg)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('returns model message as is if JSON parsing fails', ({ assert }) => {
|
|
41
|
+
const modelMsg: AiModelMessageSchema = {
|
|
42
|
+
kind: AiMessageKind,
|
|
43
|
+
key: 'msg3',
|
|
44
|
+
session: 'sess1',
|
|
45
|
+
createdAt: Date.now(),
|
|
46
|
+
updatedAt: Date.now(),
|
|
47
|
+
role: 'model',
|
|
48
|
+
state: 'complete',
|
|
49
|
+
text: 'invalid json',
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const result = parseAiChatMessage(modelMsg)
|
|
53
|
+
assert.deepEqual(result, modelMsg)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('parses valid JSON text, sets delta and reasoning', ({ assert }) => {
|
|
57
|
+
const modelMsg: AiModelMessageSchema = {
|
|
58
|
+
kind: AiMessageKind,
|
|
59
|
+
key: 'msg4',
|
|
60
|
+
session: 'sess1',
|
|
61
|
+
createdAt: Date.now(),
|
|
62
|
+
updatedAt: Date.now(),
|
|
63
|
+
role: 'model',
|
|
64
|
+
state: 'complete',
|
|
65
|
+
text: JSON.stringify({
|
|
66
|
+
reasoning: 'I should add a new model',
|
|
67
|
+
delta: {
|
|
68
|
+
addedModels: [{ key: 'model1', name: 'Test Model' }],
|
|
69
|
+
},
|
|
70
|
+
}),
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const result = parseAiChatMessage(modelMsg)
|
|
74
|
+
assert.equal(result.text, 'I should add a new model')
|
|
75
|
+
assert.isDefined((result as AiModelMessageWithDelta).delta)
|
|
76
|
+
assert.deepEqual((result as AiModelMessageWithDelta).delta!.addedModels, [{ key: 'model1', name: 'Test Model' }])
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('keeps original text if JSON has no reasoning', ({ assert }) => {
|
|
80
|
+
const originalJSON = JSON.stringify({
|
|
81
|
+
delta: {
|
|
82
|
+
addedModels: [{ key: 'model1', name: 'Test Model' }],
|
|
83
|
+
},
|
|
84
|
+
})
|
|
85
|
+
const modelMsg: AiModelMessageSchema = {
|
|
86
|
+
kind: AiMessageKind,
|
|
87
|
+
key: 'msg5',
|
|
88
|
+
session: 'sess1',
|
|
89
|
+
createdAt: Date.now(),
|
|
90
|
+
updatedAt: Date.now(),
|
|
91
|
+
role: 'model',
|
|
92
|
+
state: 'complete',
|
|
93
|
+
text: originalJSON,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const result = parseAiChatMessage(modelMsg)
|
|
97
|
+
assert.equal(result.text, originalJSON)
|
|
98
|
+
assert.isDefined((result as AiModelMessageWithDelta).delta)
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test.group('Message Parser | detectReasoning', () => {
|
|
103
|
+
test('returns reasoning from valid JSON', ({ assert }) => {
|
|
104
|
+
const text = JSON.stringify({ reasoning: 'thinking process' })
|
|
105
|
+
const result = detectReasoning(text)
|
|
106
|
+
assert.equal(result, 'thinking process')
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('returns undefined from valid JSON without reasoning', ({ assert }) => {
|
|
110
|
+
const text = JSON.stringify({ other: 'data' })
|
|
111
|
+
const result = detectReasoning(text)
|
|
112
|
+
assert.isUndefined(result)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('extracts reasoning from incomplete JSON string', ({ assert }) => {
|
|
116
|
+
const text = '{"reasoning": "this is some incomplete string'
|
|
117
|
+
const result = detectReasoning(text)
|
|
118
|
+
assert.equal(result, 'this is some incomplete string')
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test('extracts reasoning with escaped characters in incomplete JSON', ({ assert }) => {
|
|
122
|
+
// Escaped newline and quotes within the incomplete JSON structure
|
|
123
|
+
const text = '{"reasoning": "line 1\\nline 2 with \\"quotes\\"'
|
|
124
|
+
const result = detectReasoning(text)
|
|
125
|
+
assert.equal(result, 'line 1\nline 2 with "quotes"')
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
test('extracts reasoning completely if it ends with quote in incomplete JSON', ({ assert }) => {
|
|
129
|
+
const text = '{"reasoning": "finished reasoning", "delta": '
|
|
130
|
+
const result = detectReasoning(text)
|
|
131
|
+
assert.equal(result, 'finished reasoning')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test('handles escaped trailing slashes gracefully', ({ assert }) => {
|
|
135
|
+
const text = '{"reasoning": "path\\\\to\\\\folder'
|
|
136
|
+
const result = detectReasoning(text)
|
|
137
|
+
assert.equal(result, 'path\\to\\folder')
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test('returns undefined if "reasoning" key is missing in incomplete JSON', ({ assert }) => {
|
|
141
|
+
const text = '{"delta": {"added'
|
|
142
|
+
const result = detectReasoning(text)
|
|
143
|
+
assert.isUndefined(result)
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
test('returns undefined if colon is missing after "reasoning"', ({ assert }) => {
|
|
147
|
+
const text = '{"reasoning"'
|
|
148
|
+
const result = detectReasoning(text)
|
|
149
|
+
assert.isUndefined(result)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
test('returns undefined if value quote is missing after "reasoning":', ({ assert }) => {
|
|
153
|
+
const text = '{"reasoning": '
|
|
154
|
+
const result = detectReasoning(text)
|
|
155
|
+
assert.isUndefined(result)
|
|
156
|
+
})
|
|
157
|
+
})
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { test } from '@japa/runner'
|
|
2
|
+
import { explain_schema, get_entity_details } from '../../../../../src/modeling/ai/tools/DataDomain.tools.js'
|
|
3
|
+
import { DataDomain } from '../../../../../src/index.js'
|
|
4
|
+
import type { ToolConfig } from '../../../../../src/modeling/ai/tools/config.js'
|
|
5
|
+
|
|
6
|
+
test.group('DataDomain.tools', () => {
|
|
7
|
+
test('explain_schema returns valid schemas', async ({ assert }) => {
|
|
8
|
+
const config = {} as ToolConfig
|
|
9
|
+
assert.isObject(await explain_schema(config, { dataType: 'string' }))
|
|
10
|
+
assert.isObject(await explain_schema(config, { dataType: 'number' }))
|
|
11
|
+
assert.isObject(await explain_schema(config, { dataType: 'boolean' }))
|
|
12
|
+
assert.isObject(await explain_schema(config, { dataType: 'date' }))
|
|
13
|
+
assert.isObject(await explain_schema(config, { dataType: 'datetime' }))
|
|
14
|
+
assert.isObject(await explain_schema(config, { dataType: 'time' }))
|
|
15
|
+
assert.isObject(await explain_schema(config, { dataType: 'binary' }))
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
await explain_schema(config, { dataType: 'unsupported' })
|
|
19
|
+
assert.fail('Should throw error for unsupported type')
|
|
20
|
+
} catch (err: unknown) {
|
|
21
|
+
assert.equal((err as Error).message, 'Unsupported data type: unsupported')
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('get_entity_details handles errors', ({ assert }) => {
|
|
26
|
+
const config = {} as ToolConfig
|
|
27
|
+
assert.throws(() => get_entity_details(config, {}), 'No current domain provided.')
|
|
28
|
+
|
|
29
|
+
config.domain = new DataDomain()
|
|
30
|
+
assert.throws(() => get_entity_details(config, {}), 'Entity key is required.')
|
|
31
|
+
|
|
32
|
+
assert.throws(
|
|
33
|
+
() => get_entity_details(config, { entityKey: 'missing' }),
|
|
34
|
+
'Entity with key missing not found in the current domain.'
|
|
35
|
+
)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('get_entity_details successfully returns details for a valid entity', ({ assert }) => {
|
|
39
|
+
const domain = new DataDomain()
|
|
40
|
+
const model = domain.addModel()
|
|
41
|
+
domain.addEntity(model.key, { key: 'valid-entity', info: { name: 'Valid' } })
|
|
42
|
+
const config = { domain } as ToolConfig
|
|
43
|
+
|
|
44
|
+
const details = get_entity_details(config, { entityKey: 'valid-entity' })
|
|
45
|
+
assert.equal(details.key, 'valid-entity')
|
|
46
|
+
assert.equal(details.name, 'Valid')
|
|
47
|
+
assert.equal(details.modelKey, model.key)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('get_entity_details handles entity without a parent model', ({ assert }) => {
|
|
51
|
+
const domain = new DataDomain()
|
|
52
|
+
const model = domain.addModel()
|
|
53
|
+
const entity = domain.addEntity(model.key, { key: 'orphan-entity', info: { name: 'Orphan' } })
|
|
54
|
+
|
|
55
|
+
// Mock getParentInstance
|
|
56
|
+
entity.getParentInstance = () => undefined
|
|
57
|
+
|
|
58
|
+
const config = { domain } as ToolConfig
|
|
59
|
+
assert.throws(
|
|
60
|
+
() => get_entity_details(config, { entityKey: 'orphan-entity' }),
|
|
61
|
+
'Entity with key orphan-entity has no parent model.'
|
|
62
|
+
)
|
|
63
|
+
})
|
|
64
|
+
})
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { test } from '@japa/runner'
|
|
2
|
+
import { list_semantics, get_semantic_details } from '../../../../../src/modeling/ai/tools/Semantic.tools.js'
|
|
3
|
+
import { SemanticType, DataSemantics } from '../../../../../src/modeling/Semantics.js'
|
|
4
|
+
import { AiSemanticsConfig } from '../../../../../src/modeling/ai/Semantics.js'
|
|
5
|
+
import type { ToolConfig } from '../../../../../src/modeling/ai/tools/config.js'
|
|
6
|
+
|
|
7
|
+
test.group('Semantic.tools', () => {
|
|
8
|
+
const dummyConfig = {
|
|
9
|
+
logger: {
|
|
10
|
+
silly: () => {},
|
|
11
|
+
},
|
|
12
|
+
} as unknown as ToolConfig
|
|
13
|
+
|
|
14
|
+
test('list_semantics returns all available data semantics', ({ assert }) => {
|
|
15
|
+
const result = list_semantics(dummyConfig)
|
|
16
|
+
assert.isArray(result)
|
|
17
|
+
assert.lengthOf(result, Object.keys(DataSemantics).length)
|
|
18
|
+
|
|
19
|
+
// Check first item structure
|
|
20
|
+
const firstItem = result[0]
|
|
21
|
+
assert.property(firstItem, 'id')
|
|
22
|
+
assert.property(firstItem, 'displayName')
|
|
23
|
+
assert.property(firstItem, 'description')
|
|
24
|
+
assert.property(firstItem, 'scope')
|
|
25
|
+
assert.property(firstItem, 'category')
|
|
26
|
+
assert.property(firstItem, 'hasConfig')
|
|
27
|
+
// It should map from DataSemantics correctly
|
|
28
|
+
const firstKey = Object.keys(DataSemantics)[0] as SemanticType
|
|
29
|
+
assert.equal(firstItem.id, DataSemantics[firstKey].id)
|
|
30
|
+
assert.equal(firstItem.displayName, DataSemantics[firstKey].displayName)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('get_semantic_details returns details', async ({ assert }) => {
|
|
34
|
+
const semanticId = SemanticType.Calculated
|
|
35
|
+
const result = await get_semantic_details(dummyConfig, { semanticId })
|
|
36
|
+
|
|
37
|
+
assert.equal(result.id, semanticId)
|
|
38
|
+
assert.equal(result.displayName, DataSemantics[semanticId].displayName)
|
|
39
|
+
|
|
40
|
+
const cnf = AiSemanticsConfig[semanticId] as { configSchema?: unknown }
|
|
41
|
+
assert.equal(result.configSchema, cnf.configSchema)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('get_semantic_details throws if semanticId is missing', async ({ assert }) => {
|
|
45
|
+
await assert.rejects(async () => {
|
|
46
|
+
await get_semantic_details(dummyConfig, {})
|
|
47
|
+
}, 'Semantic ID is required.')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('get_semantic_details throws if semanticId is invalid', async ({ assert }) => {
|
|
51
|
+
await assert.rejects(async () => {
|
|
52
|
+
await get_semantic_details(dummyConfig, { semanticId: 'invalid-id' })
|
|
53
|
+
}, 'Semantic with ID invalid-id not found')
|
|
54
|
+
})
|
|
55
|
+
})
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { test } from '@japa/runner'
|
|
2
|
+
import { AiUserMessage, AiModelMessage, AiMessageFactory } from '../../../src/models/AiMessage.js'
|
|
3
|
+
import { AiMessageKind } from '../../../src/models/kinds.js'
|
|
4
|
+
|
|
5
|
+
test.group('AiMessage | AiUserMessage', () => {
|
|
6
|
+
test('constants and static properties', ({ assert }) => {
|
|
7
|
+
assert.equal(AiUserMessage.Kind, AiMessageKind)
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
test('createSchema validates required session', ({ assert }) => {
|
|
11
|
+
assert.throws(() => AiUserMessage.createSchema({}), 'Session is required to create an AiMessage.')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test('createSchema builds valid user schema', ({ assert }) => {
|
|
15
|
+
const schema = AiUserMessage.createSchema({ session: 'session1', text: 'Hello' })
|
|
16
|
+
assert.isString(schema.key)
|
|
17
|
+
assert.isNotEmpty(schema.key)
|
|
18
|
+
assert.equal(schema.kind, AiMessageKind)
|
|
19
|
+
assert.equal(schema.session, 'session1')
|
|
20
|
+
assert.equal(schema.text, 'Hello')
|
|
21
|
+
assert.equal(schema.role, 'user')
|
|
22
|
+
assert.equal(schema.state, 'complete')
|
|
23
|
+
assert.equal(schema.createdAt, 0)
|
|
24
|
+
assert.equal(schema.updatedAt, 0)
|
|
25
|
+
assert.isUndefined(schema.deleted)
|
|
26
|
+
assert.isUndefined(schema.deletedInfo)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('constructor builds AiUserMessage instance', ({ assert }) => {
|
|
30
|
+
const msg = new AiUserMessage({
|
|
31
|
+
key: 'msg1',
|
|
32
|
+
session: 'sess1',
|
|
33
|
+
text: 'User utterance',
|
|
34
|
+
createdAt: 10,
|
|
35
|
+
updatedAt: 20,
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
assert.equal(msg.key, 'msg1')
|
|
39
|
+
assert.equal(msg.session, 'sess1')
|
|
40
|
+
assert.equal(msg.role, 'user')
|
|
41
|
+
assert.equal(msg.state, 'complete')
|
|
42
|
+
assert.equal(msg.text, 'User utterance')
|
|
43
|
+
assert.equal(msg.createdAt, 10)
|
|
44
|
+
assert.equal(msg.updatedAt, 20)
|
|
45
|
+
assert.isFalse(msg.deleted)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('toJSON correctly serializes an AiUserMessage', ({ assert }) => {
|
|
49
|
+
const msg = new AiUserMessage({
|
|
50
|
+
key: 'msg1',
|
|
51
|
+
session: 'sess1',
|
|
52
|
+
text: 'Message Data',
|
|
53
|
+
createdAt: 50,
|
|
54
|
+
updatedAt: 60,
|
|
55
|
+
deleted: true,
|
|
56
|
+
deletedInfo: { time: 100, byMe: true, user: 'user1' },
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const json = msg.toJSON()
|
|
60
|
+
assert.deepEqual(json, {
|
|
61
|
+
kind: AiMessageKind,
|
|
62
|
+
key: 'msg1',
|
|
63
|
+
role: 'user',
|
|
64
|
+
session: 'sess1',
|
|
65
|
+
state: 'complete',
|
|
66
|
+
text: 'Message Data',
|
|
67
|
+
createdAt: 50,
|
|
68
|
+
updatedAt: 60,
|
|
69
|
+
deleted: true,
|
|
70
|
+
deletedInfo: { time: 100, byMe: true, user: 'user1' },
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test.group('AiMessage | AiModelMessage', () => {
|
|
76
|
+
test('createSchema validates required session', ({ assert }) => {
|
|
77
|
+
assert.throws(() => AiModelMessage.createSchema({}), 'Session is required to create an AiMessage.')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('createSchema builds valid model schema', ({ assert }) => {
|
|
81
|
+
const schema = AiModelMessage.createSchema({ session: 'session1', text: 'Response' })
|
|
82
|
+
assert.isString(schema.key)
|
|
83
|
+
assert.isNotEmpty(schema.key)
|
|
84
|
+
assert.equal(schema.kind, AiMessageKind)
|
|
85
|
+
assert.equal(schema.session, 'session1')
|
|
86
|
+
assert.equal(schema.text, 'Response')
|
|
87
|
+
assert.equal(schema.role, 'model')
|
|
88
|
+
assert.equal(schema.state, 'loading') // default
|
|
89
|
+
assert.equal(schema.thoughts, '')
|
|
90
|
+
assert.isUndefined(schema.applied)
|
|
91
|
+
assert.isUndefined(schema.deleted)
|
|
92
|
+
assert.isUndefined(schema.deletedInfo)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('constructor builds AiModelMessage instance', ({ assert }) => {
|
|
96
|
+
const msg = new AiModelMessage({
|
|
97
|
+
key: 'msg1',
|
|
98
|
+
session: 'sess1',
|
|
99
|
+
text: 'Model utterance',
|
|
100
|
+
state: 'complete',
|
|
101
|
+
applied: true,
|
|
102
|
+
thoughts: 'Initial Thoughts',
|
|
103
|
+
createdAt: 10,
|
|
104
|
+
updatedAt: 20,
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
assert.equal(msg.key, 'msg1')
|
|
108
|
+
assert.equal(msg.session, 'sess1')
|
|
109
|
+
assert.equal(msg.role, 'model')
|
|
110
|
+
assert.equal(msg.state, 'complete')
|
|
111
|
+
assert.equal(msg.text, 'Model utterance')
|
|
112
|
+
assert.equal(msg.thoughts, 'Initial Thoughts')
|
|
113
|
+
assert.isTrue(msg.applied)
|
|
114
|
+
assert.equal(msg.createdAt, 10)
|
|
115
|
+
assert.equal(msg.updatedAt, 20)
|
|
116
|
+
assert.isFalse(msg.deleted)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test('toJSON correctly serializes an AiModelMessage', ({ assert }) => {
|
|
120
|
+
const msg = new AiModelMessage({
|
|
121
|
+
key: 'msg1',
|
|
122
|
+
session: 'sess1',
|
|
123
|
+
text: 'Message Data',
|
|
124
|
+
state: 'complete',
|
|
125
|
+
thoughts: 'Inner Monologue',
|
|
126
|
+
applied: false,
|
|
127
|
+
createdAt: 50,
|
|
128
|
+
updatedAt: 60,
|
|
129
|
+
deleted: true,
|
|
130
|
+
deletedInfo: { time: 100, byMe: true, user: 'user1' },
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
const json = msg.toJSON()
|
|
134
|
+
assert.deepEqual(json, {
|
|
135
|
+
kind: AiMessageKind,
|
|
136
|
+
key: 'msg1',
|
|
137
|
+
role: 'model',
|
|
138
|
+
session: 'sess1',
|
|
139
|
+
state: 'complete',
|
|
140
|
+
text: 'Message Data',
|
|
141
|
+
thoughts: 'Inner Monologue',
|
|
142
|
+
applied: false,
|
|
143
|
+
createdAt: 50,
|
|
144
|
+
updatedAt: 60,
|
|
145
|
+
deleted: true,
|
|
146
|
+
deletedInfo: { time: 100, byMe: true, user: 'user1' },
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
test('addThought safely appends thoughts', ({ assert }) => {
|
|
151
|
+
const msg = new AiModelMessage({ session: 's1' })
|
|
152
|
+
msg.addThought('Hello ')
|
|
153
|
+
msg.addThought('World!')
|
|
154
|
+
msg.addThought()
|
|
155
|
+
assert.equal(msg.thoughts, 'Hello World!')
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
test('addText safely appends text', ({ assert }) => {
|
|
159
|
+
const msg = new AiModelMessage({ session: 's1' })
|
|
160
|
+
msg.addText('Hello ')
|
|
161
|
+
msg.addText('World!')
|
|
162
|
+
msg.addText()
|
|
163
|
+
assert.equal(msg.text, 'Hello World!')
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
test('processMessage throws on invalid JSON', ({ assert }) => {
|
|
167
|
+
const msg = new AiModelMessage({ session: 's1', text: 'NOT JSON' })
|
|
168
|
+
assert.throws(() => msg.processMessage(), SyntaxError)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
test('processMessage parses delta and reasoning', ({ assert }) => {
|
|
172
|
+
const msg = new AiModelMessage({
|
|
173
|
+
session: 's1',
|
|
174
|
+
text: JSON.stringify({
|
|
175
|
+
reasoning: 'I need to make models',
|
|
176
|
+
delta: { addedModels: [{ key: 'model1' }] },
|
|
177
|
+
}),
|
|
178
|
+
})
|
|
179
|
+
msg.processMessage()
|
|
180
|
+
assert.equal(msg.reasoning, 'I need to make models')
|
|
181
|
+
assert.isDefined(msg.delta)
|
|
182
|
+
assert.deepEqual(msg.delta!.addedModels, [{ key: 'model1' }])
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
test('processMessageSafe swallows errors', ({ assert }) => {
|
|
186
|
+
const msg = new AiModelMessage({ session: 's1', text: 'NOT JSON' })
|
|
187
|
+
msg.processMessageSafe() // should not throw
|
|
188
|
+
assert.isUndefined(msg.delta)
|
|
189
|
+
assert.isUndefined(msg.reasoning)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
test('createEmpty generates loading stub', ({ assert }) => {
|
|
193
|
+
const msg = AiModelMessage.createEmpty('sess1')
|
|
194
|
+
assert.equal(msg.session, 'sess1')
|
|
195
|
+
assert.equal(msg.state, 'loading')
|
|
196
|
+
assert.equal(msg.role, 'model')
|
|
197
|
+
})
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
test.group('AiMessage | AiMessageFactory', () => {
|
|
201
|
+
test('fromSchema generates AiUserMessage when role=user', ({ assert }) => {
|
|
202
|
+
const schema = AiUserMessage.createSchema({ session: 'sess', text: 'Hello User' })
|
|
203
|
+
const msg = AiMessageFactory.fromSchema(schema)
|
|
204
|
+
assert.instanceOf(msg, AiUserMessage)
|
|
205
|
+
assert.equal(msg.role, 'user')
|
|
206
|
+
assert.equal(msg.text, 'Hello User')
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
test('fromSchema generates AiModelMessage when role=model', ({ assert }) => {
|
|
210
|
+
const schema = AiModelMessage.createSchema({ session: 'sess', text: 'Hello Model' })
|
|
211
|
+
const msg = AiMessageFactory.fromSchema(schema)
|
|
212
|
+
assert.instanceOf(msg, AiModelMessage)
|
|
213
|
+
assert.equal(msg.role, 'model')
|
|
214
|
+
assert.equal(msg.text, 'Hello Model')
|
|
215
|
+
})
|
|
216
|
+
})
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { test } from '@japa/runner'
|
|
2
|
+
import { AiSession } from '../../../src/models/AiSession.js'
|
|
3
|
+
import { AiSessionKind, AiMessageKind } from '../../../src/models/kinds.js'
|
|
4
|
+
import type { AiModelMessage } from '../../../src/models/AiMessage.js'
|
|
5
|
+
|
|
6
|
+
test.group('AiSession', () => {
|
|
7
|
+
test('constants and static properties', ({ assert }) => {
|
|
8
|
+
assert.equal(AiSession.Kind, AiSessionKind)
|
|
9
|
+
|
|
10
|
+
const session = new AiSession({ app: 'general' })
|
|
11
|
+
assert.equal(session.kind, AiSessionKind)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test('isSessionApp check valid and invalid apps', ({ assert }) => {
|
|
15
|
+
assert.isTrue(AiSession.isSessionApp('domain'))
|
|
16
|
+
assert.isTrue(AiSession.isSessionApp('api'))
|
|
17
|
+
assert.isTrue(AiSession.isSessionApp('general'))
|
|
18
|
+
assert.isFalse(AiSession.isSessionApp('unknown'))
|
|
19
|
+
assert.isFalse(AiSession.isSessionApp(null))
|
|
20
|
+
assert.isFalse(AiSession.isSessionApp(undefined))
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('createSchema validates required arguments', ({ assert }) => {
|
|
24
|
+
assert.throws(() => AiSession.createSchema({}), 'App is required to create an AiSession schema.')
|
|
25
|
+
// @ts-expect-error for testing invalid app
|
|
26
|
+
assert.throws(() => AiSession.createSchema({ app: 'invalid' }), 'Invalid app: invalid')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('createSchema generates schema with defaults', ({ assert }) => {
|
|
30
|
+
const schema = AiSession.createSchema({ app: 'domain' })
|
|
31
|
+
assert.isString(schema.key)
|
|
32
|
+
assert.isNotEmpty(schema.key)
|
|
33
|
+
assert.equal(schema.kind, AiSessionKind)
|
|
34
|
+
assert.equal(schema.app, 'domain')
|
|
35
|
+
assert.equal(schema.title, '')
|
|
36
|
+
assert.isFalse(schema.pinned)
|
|
37
|
+
assert.isFalse(schema.userRenamed)
|
|
38
|
+
assert.equal(schema.createdAt, 0)
|
|
39
|
+
assert.equal(schema.updatedAt, 0)
|
|
40
|
+
assert.isUndefined(schema.deleted)
|
|
41
|
+
assert.isUndefined(schema.deletedInfo)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('constructor builds AiSession instance', ({ assert }) => {
|
|
45
|
+
const session = new AiSession({
|
|
46
|
+
key: 'test-key',
|
|
47
|
+
app: 'api',
|
|
48
|
+
title: 'API Session',
|
|
49
|
+
pinned: true,
|
|
50
|
+
userRenamed: true,
|
|
51
|
+
createdAt: 100,
|
|
52
|
+
updatedAt: 200,
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
assert.equal(session.key, 'test-key')
|
|
56
|
+
assert.equal(session.app, 'api')
|
|
57
|
+
assert.equal(session.title, 'API Session')
|
|
58
|
+
assert.isTrue(session.pinned)
|
|
59
|
+
assert.isTrue(session.userRenamed)
|
|
60
|
+
assert.equal(session.createdAt, 100)
|
|
61
|
+
assert.equal(session.updatedAt, 200)
|
|
62
|
+
assert.isFalse(session.deleted)
|
|
63
|
+
assert.isUndefined(session.deletedInfo)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('toJSON correctly serializes an AiSession', ({ assert }) => {
|
|
67
|
+
const session = new AiSession({
|
|
68
|
+
key: 'test-key',
|
|
69
|
+
app: 'general',
|
|
70
|
+
createdAt: 50,
|
|
71
|
+
updatedAt: 50,
|
|
72
|
+
deleted: true,
|
|
73
|
+
deletedInfo: {
|
|
74
|
+
time: 100,
|
|
75
|
+
byMe: true,
|
|
76
|
+
user: 'user1',
|
|
77
|
+
},
|
|
78
|
+
})
|
|
79
|
+
session.title = 'Test Title'
|
|
80
|
+
|
|
81
|
+
const json = session.toJSON()
|
|
82
|
+
assert.deepEqual(json, {
|
|
83
|
+
kind: AiSessionKind,
|
|
84
|
+
key: 'test-key',
|
|
85
|
+
app: 'general',
|
|
86
|
+
title: 'Test Title',
|
|
87
|
+
pinned: false,
|
|
88
|
+
userRenamed: false,
|
|
89
|
+
createdAt: 50,
|
|
90
|
+
updatedAt: 50,
|
|
91
|
+
deleted: true,
|
|
92
|
+
deletedInfo: {
|
|
93
|
+
time: 100,
|
|
94
|
+
byMe: true,
|
|
95
|
+
user: 'user1',
|
|
96
|
+
},
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
test('extractSessionTitle extracts sessionTitle when valid', ({ assert }) => {
|
|
101
|
+
const msg: AiModelMessage = {
|
|
102
|
+
kind: AiMessageKind,
|
|
103
|
+
key: 'msg1',
|
|
104
|
+
session: 'sess1',
|
|
105
|
+
role: 'model',
|
|
106
|
+
state: 'complete',
|
|
107
|
+
createdAt: 1,
|
|
108
|
+
updatedAt: 1,
|
|
109
|
+
text: JSON.stringify({ sessionTitle: 'New Generated Title' }),
|
|
110
|
+
} as unknown as AiModelMessage
|
|
111
|
+
|
|
112
|
+
const title = AiSession.extractSessionTitle(msg)
|
|
113
|
+
assert.equal(title, 'New Generated Title')
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test('extractSessionTitle returns null for non-json response text', ({ assert }) => {
|
|
117
|
+
const msg: AiModelMessage = {
|
|
118
|
+
kind: AiMessageKind,
|
|
119
|
+
key: 'msg1',
|
|
120
|
+
session: 'sess1',
|
|
121
|
+
role: 'model',
|
|
122
|
+
state: 'complete',
|
|
123
|
+
createdAt: 1,
|
|
124
|
+
updatedAt: 1,
|
|
125
|
+
text: 'I am not JSON',
|
|
126
|
+
} as unknown as AiModelMessage
|
|
127
|
+
|
|
128
|
+
const title = AiSession.extractSessionTitle(msg)
|
|
129
|
+
assert.isNull(title)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
test('extractSessionTitle returns null for JSON without sessionTitle', ({ assert }) => {
|
|
133
|
+
const msg: AiModelMessage = {
|
|
134
|
+
kind: AiMessageKind,
|
|
135
|
+
key: 'msg1',
|
|
136
|
+
session: 'sess1',
|
|
137
|
+
role: 'model',
|
|
138
|
+
state: 'complete',
|
|
139
|
+
createdAt: 1,
|
|
140
|
+
updatedAt: 1,
|
|
141
|
+
text: JSON.stringify({ otherField: 'value' }),
|
|
142
|
+
} as unknown as AiModelMessage
|
|
143
|
+
|
|
144
|
+
const title = AiSession.extractSessionTitle(msg)
|
|
145
|
+
assert.isNull(title)
|
|
146
|
+
})
|
|
147
|
+
})
|