Skip to content

Commit f82cee3

Browse files
akoclaude
andcommitted
feat: add read support for agent-editor Model documents (SHOW/DESCRIBE MODEL)
First step of agent-editor document support. Adds the generic CustomBlobDocument parser framework (shared by all four agent-editor document types) and implements the MODEL handler end-to-end. Implements: - sdk/agenteditor package with Model, Agent, KnowledgeBase, and ConsumedMCPService type definitions (Model fully wired; others scaffolded for later phases) - sdk/mpr/parser_customblob.go — generic wrapper decoder plus Model Contents JSON decoder (dispatches by CustomDocumentType) - Reader.ListAgentEditorModels() returns all agenteditor.model docs - MDL keywords: MODEL, MODELS (added to lexer and keyword rule) - SHOW MODELS [IN module] grammar/AST/visitor/executor — lists models with qualified name, provider, and key constant reference - DESCRIBE MODEL Module.Name grammar/AST/visitor/executor — emits round-trippable CREATE MODEL MDL. User-set fields (Provider, Key) come first; Portal-populated fields (DisplayName, KeyName, etc.) only emitted when non-empty Verified against test3 project: - SHOW MODELS correctly shows Agents.MyFirstModel with provider MxCloudGenAI and key Agents.LLMKey - DESCRIBE MODEL Agents.MyFirstModel produces valid CREATE MODEL MDL - SHOW MODELS IN <module> filter works (shows/hides by module match) - DESCRIBE MODEL <non-existent> returns actionable error Test coverage: existing test suites pass. No new tests in this commit — will add round-trip test when CREATE MODEL lands so there is a document to write then read back. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent c0d63e8 commit f82cee3

File tree

16 files changed

+8257
-7551
lines changed

16 files changed

+8257
-7551
lines changed

cmd/mxcli/lsp_completions_gen.go

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

mdl/ast/ast_query.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ const (
8989
ShowJsonStructures // SHOW JSON STRUCTURES [IN module]
9090
ShowImportMappings // SHOW IMPORT MAPPINGS [IN module]
9191
ShowExportMappings // SHOW EXPORT MAPPINGS [IN module]
92+
ShowModels // SHOW MODELS [IN module] (agent-editor Model documents)
9293
)
9394

9495
// String returns the human-readable name of the show object type.
@@ -214,6 +215,8 @@ func (t ShowObjectType) String() string {
214215
return "IMPORT MAPPINGS"
215216
case ShowExportMappings:
216217
return "EXPORT MAPPINGS"
218+
case ShowModels:
219+
return "MODELS"
217220
default:
218221
return "UNKNOWN"
219222
}
@@ -291,6 +294,7 @@ const (
291294
DescribeNanoflow // DESCRIBE NANOFLOW Module.Name
292295
DescribeImportMapping // DESCRIBE IMPORT MAPPING Module.Name
293296
DescribeExportMapping // DESCRIBE EXPORT MAPPING Module.Name
297+
DescribeModel // DESCRIBE MODEL Module.Name (agent-editor Model document)
294298
)
295299

296300
// String returns the human-readable name of the describe object type.
@@ -364,6 +368,8 @@ func (t DescribeObjectType) String() string {
364368
return "IMPORT MAPPING"
365369
case DescribeExportMapping:
366370
return "EXPORT MAPPING"
371+
case DescribeModel:
372+
return "MODEL"
367373
default:
368374
return "UNKNOWN"
369375
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
// Package executor - Commands for agent-editor Model documents.
4+
//
5+
// Handles `SHOW MODELS [IN module]` and `DESCRIBE MODEL Module.Name`.
6+
// The underlying BSON is a CustomBlobDocuments$CustomBlobDocument with
7+
// CustomDocumentType = "agenteditor.model". See
8+
// docs/11-proposals/PROPOSAL_agent_document_support.md for schema.
9+
package executor
10+
11+
import (
12+
"fmt"
13+
14+
"github.com/mendixlabs/mxcli/mdl/ast"
15+
"github.com/mendixlabs/mxcli/sdk/agenteditor"
16+
)
17+
18+
// showAgentEditorModels handles SHOW MODELS [IN module].
19+
func (e *Executor) showAgentEditorModels(moduleName string) error {
20+
if e.reader == nil {
21+
return fmt.Errorf("not connected to a project")
22+
}
23+
24+
models, err := e.reader.ListAgentEditorModels()
25+
if err != nil {
26+
return fmt.Errorf("failed to list models: %w", err)
27+
}
28+
29+
h, err := e.getHierarchy()
30+
if err != nil {
31+
return err
32+
}
33+
34+
result := &TableResult{
35+
Columns: []string{"Qualified Name", "Module", "Name", "Provider", "Key Constant", "Display Name"},
36+
}
37+
38+
for _, m := range models {
39+
modID := h.FindModuleID(m.ContainerID)
40+
modName := h.GetModuleName(modID)
41+
if moduleName != "" && modName != moduleName {
42+
continue
43+
}
44+
45+
keyConstant := ""
46+
if m.Key != nil {
47+
keyConstant = m.Key.QualifiedName
48+
}
49+
50+
result.Rows = append(result.Rows, []any{
51+
fmt.Sprintf("%s.%s", modName, m.Name),
52+
modName,
53+
m.Name,
54+
m.Provider,
55+
keyConstant,
56+
m.DisplayName,
57+
})
58+
}
59+
60+
result.Summary = fmt.Sprintf("(%d model(s))", len(result.Rows))
61+
return e.writeResult(result)
62+
}
63+
64+
// describeAgentEditorModel handles DESCRIBE MODEL Module.Name.
65+
// Emits a round-trippable CREATE MODEL statement.
66+
func (e *Executor) describeAgentEditorModel(name ast.QualifiedName) error {
67+
if e.reader == nil {
68+
return fmt.Errorf("not connected to a project")
69+
}
70+
71+
m := e.findAgentEditorModel(name.Module, name.Name)
72+
if m == nil {
73+
return fmt.Errorf("model not found: %s", name)
74+
}
75+
76+
h, err := e.getHierarchy()
77+
if err != nil {
78+
return err
79+
}
80+
modID := h.FindModuleID(m.ContainerID)
81+
modName := h.GetModuleName(modID)
82+
qualifiedName := fmt.Sprintf("%s.%s", modName, m.Name)
83+
84+
if m.Documentation != "" {
85+
fmt.Fprintf(e.output, "/**\n * %s\n */\n", m.Documentation)
86+
}
87+
88+
fmt.Fprintf(e.output, "CREATE MODEL %s (\n", qualifiedName)
89+
90+
// Emit properties in stable order. User-set properties (Provider, Key)
91+
// come first; Portal-populated metadata comes last and only if non-empty.
92+
var lines []string
93+
if m.Provider != "" {
94+
lines = append(lines, fmt.Sprintf(" Provider: %s", m.Provider))
95+
}
96+
if m.Key != nil && m.Key.QualifiedName != "" {
97+
lines = append(lines, fmt.Sprintf(" Key: %s", m.Key.QualifiedName))
98+
}
99+
// Portal-populated fields — round-tripped but flagged read-only in MDL.
100+
if m.DisplayName != "" {
101+
lines = append(lines, fmt.Sprintf(" DisplayName: '%s'", escapeSQLString(m.DisplayName)))
102+
}
103+
if m.KeyName != "" {
104+
lines = append(lines, fmt.Sprintf(" KeyName: '%s'", escapeSQLString(m.KeyName)))
105+
}
106+
if m.KeyID != "" {
107+
lines = append(lines, fmt.Sprintf(" KeyId: '%s'", escapeSQLString(m.KeyID)))
108+
}
109+
if m.Environment != "" {
110+
lines = append(lines, fmt.Sprintf(" Environment: '%s'", escapeSQLString(m.Environment)))
111+
}
112+
if m.ResourceName != "" {
113+
lines = append(lines, fmt.Sprintf(" ResourceName: '%s'", escapeSQLString(m.ResourceName)))
114+
}
115+
if m.DeepLinkURL != "" {
116+
lines = append(lines, fmt.Sprintf(" DeepLinkURL: '%s'", escapeSQLString(m.DeepLinkURL)))
117+
}
118+
119+
for i, line := range lines {
120+
if i < len(lines)-1 {
121+
fmt.Fprintln(e.output, line+",")
122+
} else {
123+
fmt.Fprintln(e.output, line)
124+
}
125+
}
126+
127+
fmt.Fprintln(e.output, ");")
128+
fmt.Fprintln(e.output, "/")
129+
return nil
130+
}
131+
132+
// findAgentEditorModel looks up a model by module and name.
133+
func (e *Executor) findAgentEditorModel(moduleName, modelName string) *agenteditor.Model {
134+
models, err := e.reader.ListAgentEditorModels()
135+
if err != nil {
136+
return nil
137+
}
138+
h, err := e.getHierarchy()
139+
if err != nil {
140+
return nil
141+
}
142+
for _, m := range models {
143+
modID := h.FindModuleID(m.ContainerID)
144+
modName := h.GetModuleName(modID)
145+
if m.Name == modelName && modName == moduleName {
146+
return m
147+
}
148+
}
149+
return nil
150+
}

mdl/executor/executor_query.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ func (e *Executor) execShow(s *ast.ShowStmt) error {
110110
return e.showDatabaseConnections(s.InModule)
111111
case ast.ShowImageCollections:
112112
return e.showImageCollections(s.InModule)
113+
case ast.ShowModels:
114+
return e.showAgentEditorModels(s.InModule)
113115
case ast.ShowRestClients:
114116
return e.showRestClients(s.InModule)
115117
case ast.ShowPublishedRestServices:
@@ -196,6 +198,8 @@ func (e *Executor) execDescribe(s *ast.DescribeStmt) error {
196198
return e.describeFragment(s.Name)
197199
case ast.DescribeImageCollection:
198200
return e.describeImageCollection(s.Name)
201+
case ast.DescribeModel:
202+
return e.describeAgentEditorModel(s.Name)
199203
case ast.DescribeRestClient:
200204
return e.describeRestClient(s.Name)
201205
case ast.DescribePublishedRestService:
@@ -273,6 +277,8 @@ func describeObjectTypeLabel(t ast.DescribeObjectType) string {
273277
return "fragment"
274278
case ast.DescribeImageCollection:
275279
return "imagecollection"
280+
case ast.DescribeModel:
281+
return "model"
276282
case ast.DescribeRestClient:
277283
return "restclient"
278284
case ast.DescribePublishedRestService:

mdl/grammar/MDLLexer.g4

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,8 @@ ATTRIBUTES: A T T R I B U T E S;
316316
FILTERTYPE: F I L T E R T Y P E;
317317
IMAGE: I M A G E;
318318
COLLECTION: C O L L E C T I O N;
319+
MODEL: M O D E L;
320+
MODELS: M O D E L S;
319321
STATICIMAGE: S T A T I C I M A G E;
320322
DYNAMICIMAGE: D Y N A M I C I M A G E;
321323
CUSTOMCONTAINER: C U S T O M C O N T A I N E R;

mdl/grammar/MDLParser.g4

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2909,6 +2909,7 @@ showStatement
29092909
| showOrList JAVA ACTIONS (IN (qualifiedName | IDENTIFIER))?
29102910
| showOrList JAVASCRIPT ACTIONS (IN (qualifiedName | IDENTIFIER))?
29112911
| showOrList IMAGE COLLECTION (IN (qualifiedName | IDENTIFIER))?
2912+
| showOrList MODELS (IN (qualifiedName | IDENTIFIER))?
29122913
| showOrList JSON STRUCTURES (IN (qualifiedName | IDENTIFIER))?
29132914
| showOrList IMPORT MAPPINGS (IN (qualifiedName | IDENTIFIER))?
29142915
| showOrList EXPORT MAPPINGS (IN (qualifiedName | IDENTIFIER))?
@@ -3043,6 +3044,7 @@ describeStatement
30433044
| DESCRIBE FRAGMENT FROM PAGE qualifiedName WIDGET identifierOrKeyword // DESCRIBE FRAGMENT FROM PAGE Module.Page WIDGET name
30443045
| DESCRIBE FRAGMENT FROM SNIPPET qualifiedName WIDGET identifierOrKeyword // DESCRIBE FRAGMENT FROM SNIPPET Module.Snippet WIDGET name
30453046
| DESCRIBE IMAGE COLLECTION qualifiedName // DESCRIBE IMAGE COLLECTION Module.Name
3047+
| DESCRIBE MODEL qualifiedName // DESCRIBE MODEL Module.Name (agent-editor)
30463048
| DESCRIBE JSON STRUCTURE qualifiedName // DESCRIBE JSON STRUCTURE Module.Name
30473049
| DESCRIBE IMPORT MAPPING qualifiedName // DESCRIBE IMPORT MAPPING Module.Name
30483050
| DESCRIBE EXPORT MAPPING qualifiedName // DESCRIBE EXPORT MAPPING Module.Name
@@ -3645,7 +3647,7 @@ keyword
36453647
| INTEGER_TYPE | LONG_TYPE | STRING_TYPE | STRINGTEMPLATE_TYPE
36463648

36473649
// Module / project structure
3648-
| ACTIONS | COLLECTION | FOLDER | LAYOUT | LAYOUTS | LOCAL | MODULE | MODULES
3650+
| ACTIONS | COLLECTION | FOLDER | LAYOUT | LAYOUTS | LOCAL | MODEL | MODELS | MODULE | MODULES
36493651
| NOTEBOOK | NOTEBOOKS | PAGE | PAGES | PROJECT | SNIPPET | SNIPPETS
36503652
| STORE | STRUCTURE | STRUCTURES | VIEW
36513653

mdl/grammar/parser/MDLLexer.interp

Lines changed: 7 additions & 1 deletion
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)