Skip to content

Commit 627a826

Browse files
committed
Add AI-driven domain model update functionality with unit tests
- Implemented an async function to execute AI prompts and update domain models based on AI responses. - Created a dialog for user input to gather AI prompt and model selection. - Gathered current class model data, including attributes and associations. - Integrated AI provider models and settings persistence. - Handled AI task execution and response parsing, including error handling. - Updated existing classes and associations based on AI output, including deletion of obsolete elements. - Added SVG icon for AI unit tests.
1 parent 0ec3413 commit 627a826

11 files changed

Lines changed: 571 additions & 640 deletions

File tree

Modules/Intent.Modules.AI.AutoImplementation/resources/scripts/ai-implement.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,14 +126,14 @@ async function execute(taskId) {
126126
if (!promptResult) {
127127
return;
128128
}
129-
const { providerId, modelId, thinkingLevel: thinkingType } = await collectAndPersistAiSettingsFromPromptResult(promptResult, providerModelsResult, settingName);
129+
const { providerId, modelId, thinkingLevel: thinkingLevel } = await collectAndPersistAiSettingsFromPromptResult(promptResult, providerModelsResult, settingName);
130130
await launchHostedModuleTask(taskId, [
131131
application.id,
132132
element.id,
133133
(_a = promptResult.prompt) !== null && _a !== void 0 ? _a : "",
134134
providerId,
135135
modelId,
136-
thinkingType
136+
thinkingLevel
137137
], {
138138
taskName: "AI: Handler for " + element.getName()
139139
});
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
async function execute(element: MacroApi.Context.IElementApi): Promise<void> {
2+
const providerModelsResult = await getAiProviderModels();
3+
const settingName = "AI.ChatDrivenDomain";
4+
5+
// Open a dialog for the user to enter an AI prompt
6+
let promptResult = await dialogService.openForm({
7+
title: "Execute Prompt",
8+
fields: [
9+
{
10+
id: "prompt",
11+
fieldType: "textarea",
12+
label: "Enter AI prompt",
13+
hint: "NOTE: Intent Architect designer data will also be sent to the AI service provider."
14+
},
15+
...await getAiModelSelectionFields(providerModelsResult, settingName)
16+
]
17+
});
18+
19+
// Check if the user cancelled or provided an empty prompt
20+
if (!promptResult || promptResult.prompt.trim() === "") {
21+
return;
22+
}
23+
24+
// Gather data about the current class model
25+
const currentClasses = lookupTypesOf("Class").map(clazz => {
26+
// Get all associations where this class is either source or target
27+
const associations = clazz.getAssociations().map(assoc => {
28+
//console.log(`classId = ${clazz.id}; className = ${clazz.getName()}; assoc = {classId: ${assoc.typeReference.getTypeId()}, name: ${assoc.getName()}, isSourceEnd: ${assoc.isSourceEnd()}, isCollection: ${assoc.typeReference.isCollection}}`)
29+
// Determine if this class is the source or target of the association
30+
const isSource = assoc.isSourceEnd() //assoc.getParent().id === clazz.id;
31+
const sourceEnd = isSource ? assoc : assoc.getOtherEnd();
32+
33+
if (isSource && !sourceEnd.typeReference.isNavigable) { return null; }
34+
35+
const targetEnd = isSource ? assoc.getOtherEnd() : assoc;
36+
37+
// Get type references for both ends
38+
const sourceTypeRef = sourceEnd.typeReference;
39+
const targetTypeRef = targetEnd.typeReference;
40+
41+
// Build the relationship string (e.g., "1 -> *")
42+
const sourceMultiplicity = sourceTypeRef.getIsCollection() ? "*" :
43+
sourceTypeRef.getIsNullable() ? "0..1" : "1";
44+
const targetMultiplicity = targetTypeRef.getIsCollection() ? "*" :
45+
targetTypeRef.getIsNullable() ? "0..1" : "1";
46+
const relationship = `${sourceMultiplicity} -> ${targetMultiplicity}`;
47+
48+
return {
49+
id: assoc.id,
50+
name: assoc.getName(),
51+
classId: assoc.typeReference.getTypeId(),
52+
type: relationship,
53+
specializationEndType: isSource ? "Source End" : "Target End",
54+
// Add these for compatibility with the C# model
55+
relationship: relationship,
56+
associationEndType: isSource ? "Source End" : "Target End",
57+
isNullable: assoc.typeReference.isNullable,
58+
isCollection: assoc.typeReference.isCollection
59+
};
60+
})
61+
.filter(x => x != null);
62+
63+
return {
64+
id: clazz.id,
65+
name: clazz.getName(),
66+
comment: clazz.getComment(),
67+
attributes: clazz.getChildren("Attribute")
68+
.filter(attr => !attr.hasMetadata("is-managed-key") && !attr.hasMetadata("set-by-infrastructure"))
69+
.map(attr => ({
70+
id: attr.id,
71+
name: attr.getName(),
72+
type: attr.typeReference ? attr.typeReference.getType().getName() : null,
73+
isNullable: attr.typeReference ? attr.typeReference.getIsNullable() : false,
74+
isCollection: attr.typeReference ? attr.typeReference.getIsCollection() : false,
75+
comment: attr.getComment()
76+
})),
77+
associations: associations
78+
};
79+
});
80+
81+
const { providerId, modelId, thinkingLevel: thinkingLevel } = await collectAndPersistAiSettingsFromPromptResult(
82+
promptResult, providerModelsResult, settingName);
83+
84+
const input = { prompt: promptResult.prompt, classes: currentClasses, providerId: providerId, modelId: modelId, thinkingLevel: thinkingLevel };
85+
86+
// Execute the AI module task
87+
let outputStr;
88+
try {
89+
outputStr = await executeModuleTask("Intent.Modules.ChatDrivenDomain.Tasks.ChatCompletionTask", JSON.stringify(input));
90+
} catch (error) {
91+
dialogService.error(`Failed to execute AI task: ${error.message}`);
92+
return;
93+
}
94+
95+
// Parse the result from the AI task
96+
let updatedClasses: any;
97+
try {
98+
updatedClasses = JSON.parse(outputStr);
99+
} catch (error) {
100+
dialogService.error(`Failed to parse AI task result: ${error.message}`);
101+
return;
102+
}
103+
104+
// Check for errors in the AI response
105+
if (updatedClasses.errorMessage) {
106+
dialogService.error(updatedClasses.errorMessage);
107+
return;
108+
}
109+
110+
// Create a lookup of type names to IDs
111+
const types = lookupTypesOf("Type-Definition");
112+
const typesLookup = Object.fromEntries(types.map(el => [el.getName(), el.id]));
113+
114+
// Create maps for existing classes and updated classes
115+
const existingClassesMap = new Map(lookupTypesOf("Class").map(clazz => [clazz.id, clazz]));
116+
const packageId = element.id;
117+
const updatedClassesMap = new Map(updatedClasses.map((jClass: any) => [jClass.id, jClass]));
118+
119+
// Process class updates and additions
120+
updatedClasses.forEach((jClass: any) => {
121+
let clazz = existingClassesMap.get(jClass.id);
122+
if (!clazz) {
123+
// Add new class
124+
clazz = createElement("Class", jClass.name, packageId);
125+
existingClassesMap.set(jClass.id, clazz);
126+
}
127+
128+
// Update class properties
129+
clazz.setName(jClass.name, true);
130+
clazz.setComment(jClass.comment);
131+
132+
// Process attributes - first delete all existing attributes
133+
const existingAttributes = clazz.getChildren("Attribute");
134+
existingAttributes.forEach(attr => attr.delete());
135+
136+
// Add all attributes from the updated model
137+
if (jClass.attributes && jClass.attributes.length > 0) {
138+
jClass.attributes.forEach((jAttr: any) => {
139+
const attr = createElement("Attribute", jAttr.name, clazz.id);
140+
attr.setComment(jAttr.comment || "");
141+
142+
if (jAttr.type && typesLookup[jAttr.type]) {
143+
attr.typeReference.setType(typesLookup[jAttr.type]);
144+
attr.typeReference.setIsCollection(jAttr.isCollection || false);
145+
attr.typeReference.setIsNullable(jAttr.isNullable || false);
146+
}
147+
});
148+
}
149+
});
150+
151+
// Delete classes not in the updated model
152+
existingClassesMap.forEach((clazz, id) => {
153+
if (!updatedClassesMap.has(id)) {
154+
clazz.delete();
155+
}
156+
});
157+
158+
// Clear all existing associations
159+
const existingAssociations = lookupTypesOf("Association");
160+
existingAssociations.forEach(assoc => {
161+
assoc.delete();
162+
});
163+
164+
// Create a map to track which associations we've already created
165+
// We'll use a compound key of sourceClassId + targetClassId to uniquely identify each association
166+
const createdAssociations = new Map();
167+
168+
// For each class in our updated model
169+
updatedClasses.forEach((jClass: any) => {
170+
// Process all associations for this class
171+
jClass.associations.forEach((jAssoc: any) => {
172+
const thisClassId = jClass.id;
173+
const otherClassId = jAssoc.classId;
174+
175+
const thisClass = existingClassesMap.get(thisClassId);
176+
const otherClass = existingClassesMap.get(otherClassId);
177+
178+
if (!thisClass || !otherClass) return;
179+
180+
// Determine the source and target classes based on associationEndType
181+
let sourceClassId, targetClassId;
182+
183+
if (jAssoc.associationEndType === "Target End") {
184+
// This class is the source
185+
sourceClassId = thisClassId;
186+
targetClassId = otherClassId;
187+
}
188+
else if (jAssoc.associationEndType === "Source End") {
189+
// This class is the target
190+
sourceClassId = otherClassId;
191+
targetClassId = thisClassId;
192+
}
193+
else {
194+
return; // Skip if associationEndType is not recognized
195+
}
196+
197+
// Create a compound key to identify this association
198+
const associationKey = sourceClassId + "->" + targetClassId;
199+
200+
// Only create the association if we haven't already created it
201+
if (!createdAssociations.has(associationKey)) {
202+
const sourceClass = existingClassesMap.get(sourceClassId);
203+
const targetClass = existingClassesMap.get(targetClassId);
204+
205+
// Create the association
206+
const association = createAssociation("Association", sourceClass.id, targetClass.id);
207+
208+
// Mark this association as created
209+
createdAssociations.set(associationKey, association);
210+
}
211+
212+
// Get the association we just created or previously created
213+
const association = createdAssociations.get(associationKey);
214+
215+
// Now apply the properties to the appropriate end
216+
if (jAssoc.associationEndType === "Target End") {
217+
// Set properties on the target end
218+
const targetEnd = association;
219+
targetEnd.setName(jAssoc.name || "");
220+
targetEnd.typeReference.setIsCollection(jAssoc.isCollection || false);
221+
targetEnd.typeReference.setIsNullable(jAssoc.isNullable || false);
222+
}
223+
else if (jAssoc.associationEndType === "Source End") {
224+
// Set properties on the source end
225+
// In this case, the association itself is the source end
226+
const sourceEnd = association.getOtherEnd();
227+
sourceEnd.setName(jAssoc.name || "");
228+
sourceEnd.typeReference.setIsCollection(jAssoc.isCollection || false);
229+
sourceEnd.typeReference.setIsNullable(jAssoc.isNullable || false);
230+
}
231+
});
232+
});
233+
234+
// Show a completion message
235+
dialogService.info("Domain model updated based on AI response.");
236+
}

Modules/Intent.Modules.AI.ChatDrivenDomain/Intent.AI.ChatDrivenDomain.imodspec

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<package>
33
<id>Intent.AI.ChatDrivenDomain</id>
4-
<version>0.0.7-beta.1</version>
4+
<version>0.0.7-beta.2</version>
55
<supportedClientVersions>[4.5.18-a,5.0.0)</supportedClientVersions>
66
<summary>(EXPERIMENTAL) AI Assistant to model your Domain with chat prompts. By default, uses Open AI and requires an API Key.</summary>
77
<description>(EXPERIMENTAL) AI Assistant to model your Domain with chat prompts. By default, uses Open AI and requires an API Key.</description>
@@ -14,7 +14,7 @@
1414
<moduleSettings />
1515
<dependencies>
1616
<dependency id="Intent.Common" version="3.7.3" />
17-
<dependency id="Intent.Common.AI" version="1.0.0-beta.9" />
17+
<dependency id="Intent.Common.AI" version="1.0.0-beta.10" />
1818
<dependency id="Intent.Common.CSharp" version="3.8.5" />
1919
<dependency id="Intent.Common.Types" version="4.1.0" />
2020
<dependency id="Intent.Modelers.Domain" version="3.12.1" />

Modules/Intent.Modules.AI.ChatDrivenDomain/Intent.Metadata/Module Builder/Intent.AI.ChatDrivenDomain/Elements/Package Extension/Package Extension__5sfktvrw.xml

Lines changed: 2 additions & 2 deletions
Large diffs are not rendered by default.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<outputFiles>
33
<files>
4-
<file relativePath="Intent.Modules.AI.ChatDrivenDomain/release-notes.md" state="once-off-generated" />
4+
<file relativePath="Intent.Modules.AI.ChatDrivenDomain/release-notes.md" state="ignored;once-off-generated" />
55
</files>
66
</outputFiles>

Modules/Intent.Modules.AI.ChatDrivenDomain/Intent.Modules.AI.ChatDrivenDomain.application.output.log

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
<OverwriteBehaviour>once-off</OverwriteBehaviour>
3939
<ApplicationRelativeFilePath>release-notes.md</ApplicationRelativeFilePath>
4040
<ProjectRelativeFilePath>release-notes.md</ProjectRelativeFilePath>
41-
<IsIgnored>false</IsIgnored>
41+
<IsIgnored>true</IsIgnored>
4242
</FileLog>
4343
</FileLogs>
4444
</outputLog>

Modules/Intent.Modules.AI.ChatDrivenDomain/Intent.Modules.AI.ChatDrivenDomain.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
<ItemGroup>
1111
<PackageReference Include="Intent.Modules.Common" Version="3.7.3" />
12-
<PackageReference Include="Intent.Modules.Common.AI" Version="1.0.0-beta.9" />
12+
<PackageReference Include="Intent.Modules.Common.AI" Version="1.0.0-beta.10" />
1313
<PackageReference Include="Intent.Modules.Common.CSharp" Version="3.8.5" />
1414
<PackageReference Include="Intent.Modules.Common.Types" Version="4.1.0" />
1515
<PackageReference Include="Intent.Packager" Version="3.5.0">

Modules/Intent.Modules.AI.ChatDrivenDomain/Tasks/ChatCompletionTask.cs

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public string Execute(params string[] args)
5353
builder.Plugins.AddFromObject(modelMutationPlugin);
5454
});
5555

56-
var requestFunction = CreatePromptFunction(kernel);
56+
var requestFunction = CreatePromptFunction(kernel, inputModel.ThinkingLevel);
5757
var result = requestFunction.InvokeAsync(kernel, new KernelArguments
5858
{
5959
["prompt"] = inputModel.Prompt
@@ -76,7 +76,7 @@ public string Execute(params string[] args)
7676
}
7777
}
7878

79-
private static KernelFunction CreatePromptFunction(Kernel kernel)
79+
private static KernelFunction CreatePromptFunction(Kernel kernel, string thinkingLevel)
8080
{
8181
const string promptTemplate =
8282
"""
@@ -151,13 +151,8 @@ You MUST apply proper domain modeling constraints and best practices.
151151
152152
DO NOT generate JSON directly. ONLY use the provided functions to modify the domain model.
153153
""";
154-
155-
var requestFunction = kernel.CreateFunctionFromPrompt(
156-
promptTemplate,
157-
new PromptExecutionSettings
158-
{
159-
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
160-
});
154+
155+
var requestFunction = kernel.CreateFunctionFromPrompt(promptTemplate, kernel.GetRequiredService<IAiProviderService>().GetPromptExecutionSettings(thinkingLevel));
161156
return requestFunction;
162157
}
163158

Modules/Intent.Modules.AI.ChatDrivenDomain/Tasks/Models.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ public class InputModel
88
public List<ClassModel> Classes { get; set; }
99
public string ProviderId { get; set; }
1010
public string ModelId { get; set; }
11-
public string ThinkingType { get; set; }
11+
public string ThinkingLevel { get; set; }
1212
}
1313

1414
public class ClassModel

Modules/Intent.Modules.AI.ChatDrivenDomain/modelers/Chat Driven Domain Settings.designer.settings

Lines changed: 11 additions & 621 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)