Skip to content

Commit d22209d

Browse files
authored
Merge pull request #1091 from objectstack-ai/copilot/merge-list-objects-and-metadata-objects
2 parents 6401571 + 5543980 commit d22209d

File tree

13 files changed

+156
-226
lines changed

13 files changed

+156
-226
lines changed

CHANGELOG.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Changed
11+
- **Unified `list_objects` / `describe_object` tools (`service-ai`)** — Merged the duplicate
12+
`list_metadata_objects``list_objects` and `describe_metadata_object``describe_object`
13+
tool pairs. Both `data_chat` and `metadata_assistant` agents now share the same unified tools
14+
with full `filter`, `includeFields`, snake_case validation, and `enableFeatures` support.
15+
`DATA_TOOL_DEFINITIONS` is reduced from 5 to 3 (query-only tools), while
16+
`METADATA_TOOL_DEFINITIONS` retains all 6 tools under the unified names. The duplicate
17+
`ObjectDef`/`FieldDef` type definitions in `data-tools.ts` are removed.
18+
1019
### Fixed
1120
- **Agent Chat: Vercel SSE Data Stream support** — The agent chat endpoint
1221
(`/api/v1/ai/agents/:agentName/chat`) now returns Vercel AI SDK v6 UI Message Stream Protocol
@@ -157,7 +166,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
157166
### Added
158167
- **Metadata Assistant Agent (`service-ai`)** — New `metadata_assistant` agent definition that
159168
binds all 6 metadata management tools (`create_object`, `add_field`, `modify_field`,
160-
`delete_field`, `list_metadata_objects`, `describe_metadata_object`). Includes a tailored
169+
`delete_field`, `list_objects`, `describe_object`). Includes a tailored
161170
system prompt that guides the AI to use snake_case naming, verify existing schemas before
162171
modifications, and warn about destructive operations. Configured with `react` planning
163172
strategy (10 iterations, replan enabled) for multi-step schema design conversations.

apps/studio/test/ai-chat-panel.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ describe('Messages with tool invocation parts', () => {
164164
role: 'assistant',
165165
toolParts: [
166166
{
167-
toolName: 'list_metadata_objects',
167+
toolName: 'list_objects',
168168
toolCallId: 'tc_2',
169169
state: 'output-available',
170170
input: {},

packages/services/service-ai/src/__tests__/chatbot-features.test.ts

Lines changed: 34 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -304,15 +304,13 @@ describe('AIService.chatWithTools', () => {
304304

305305
describe('Data Tools', () => {
306306
describe('DATA_TOOL_DEFINITIONS', () => {
307-
it('should define exactly 5 tools', () => {
308-
expect(DATA_TOOL_DEFINITIONS).toHaveLength(5);
307+
it('should define exactly 3 tools', () => {
308+
expect(DATA_TOOL_DEFINITIONS).toHaveLength(3);
309309
});
310310

311311
it('should include all expected tool names', () => {
312312
const names = DATA_TOOL_DEFINITIONS.map(t => t.name);
313313
expect(names).toEqual([
314-
'list_objects',
315-
'describe_object',
316314
'query_records',
317315
'get_record',
318316
'aggregate_data',
@@ -336,22 +334,24 @@ describe('Data Tools', () => {
336334
registry = new ToolRegistry();
337335
dataEngine = createMockDataEngine();
338336
metadataService = createMockMetadataService();
339-
registerDataTools(registry, { dataEngine, metadataService });
337+
registerDataTools(registry, { dataEngine });
340338
});
341339

342-
it('should register all 5 tools', () => {
343-
expect(registry.size).toBe(5);
344-
expect(registry.has('list_objects')).toBe(true);
345-
expect(registry.has('describe_object')).toBe(true);
340+
it('should register all 3 tools', () => {
341+
expect(registry.size).toBe(3);
346342
expect(registry.has('query_records')).toBe(true);
347343
expect(registry.has('get_record')).toBe(true);
348344
expect(registry.has('aggregate_data')).toBe(true);
349345
});
350346

351-
it('list_objects should return object names and labels', async () => {
347+
it('list_objects should return object names and labels (via metadata tools)', async () => {
348+
// list_objects is now part of metadata tools — register them
349+
const { registerMetadataTools } = await import('../tools/metadata-tools.js');
350+
registerMetadataTools(registry, { metadataService });
351+
352352
(metadataService.listObjects as any).mockResolvedValue([
353-
{ name: 'account', label: 'Account' },
354-
{ name: 'contact', label: 'Contact' },
353+
{ name: 'account', label: 'Account', fields: { name: { type: 'text' } } },
354+
{ name: 'contact', label: 'Contact', fields: {} },
355355
]);
356356

357357
const result = await registry.execute({
@@ -362,11 +362,15 @@ describe('Data Tools', () => {
362362
});
363363

364364
const parsed = JSON.parse((result.output as any).value);
365-
expect(parsed).toHaveLength(2);
366-
expect(parsed[0]).toEqual({ name: 'account', label: 'Account' });
365+
expect(parsed.objects).toHaveLength(2);
366+
expect(parsed.objects[0]).toEqual(expect.objectContaining({ name: 'account', label: 'Account' }));
367367
});
368368

369-
it('describe_object should return field schema', async () => {
369+
it('describe_object should return field schema (via metadata tools)', async () => {
370+
// describe_object is now part of metadata tools — register them
371+
const { registerMetadataTools } = await import('../tools/metadata-tools.js');
372+
registerMetadataTools(registry, { metadataService });
373+
370374
(metadataService.getObject as any).mockResolvedValue({
371375
name: 'account',
372376
label: 'Account',
@@ -385,12 +389,19 @@ describe('Data Tools', () => {
385389

386390
const parsed = JSON.parse((result.output as any).value);
387391
expect(parsed.name).toBe('account');
388-
expect(parsed.fields.name.type).toBe('text');
389-
expect(parsed.fields.name.required).toBe(true);
390-
expect(parsed.fields.revenue.type).toBe('number');
392+
// Unified handler returns fields as array (not object)
393+
const nameField = parsed.fields.find((f: any) => f.name === 'name');
394+
expect(nameField.type).toBe('text');
395+
expect(nameField.required).toBe(true);
396+
const revenueField = parsed.fields.find((f: any) => f.name === 'revenue');
397+
expect(revenueField.type).toBe('number');
391398
});
392399

393-
it('describe_object should return error for unknown object', async () => {
400+
it('describe_object should return error for unknown object (via metadata tools)', async () => {
401+
// describe_object is now part of metadata tools — register them
402+
const { registerMetadataTools } = await import('../tools/metadata-tools.js');
403+
registerMetadataTools(registry, { metadataService });
404+
394405
const result = await registry.execute({
395406
type: 'tool-call' as const,
396407
toolCallId: 'c1',
@@ -1067,8 +1078,8 @@ describe('METADATA_ASSISTANT_AGENT', () => {
10671078
expect(toolNames).toContain('add_field');
10681079
expect(toolNames).toContain('modify_field');
10691080
expect(toolNames).toContain('delete_field');
1070-
expect(toolNames).toContain('list_metadata_objects');
1071-
expect(toolNames).toContain('describe_metadata_object');
1081+
expect(toolNames).toContain('list_objects');
1082+
expect(toolNames).toContain('describe_object');
10721083
});
10731084

10741085
it('should use action type for mutation tools and query type for read tools', () => {
@@ -1099,7 +1110,7 @@ describe('METADATA_ASSISTANT_AGENT', () => {
10991110
it('should have instructions mentioning metadata management capabilities', () => {
11001111
const instructions = METADATA_ASSISTANT_AGENT.instructions;
11011112
expect(instructions).toContain('snake_case');
1102-
expect(instructions).toContain('list_metadata_objects');
1103-
expect(instructions).toContain('describe_metadata_object');
1113+
expect(instructions).toContain('list_objects');
1114+
expect(instructions).toContain('describe_object');
11041115
});
11051116
});

packages/services/service-ai/src/__tests__/metadata-tools.test.ts

Lines changed: 35 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ import { createObjectTool } from '../tools/create-object.tool.js';
1717
import { addFieldTool } from '../tools/add-field.tool.js';
1818
import { modifyFieldTool } from '../tools/modify-field.tool.js';
1919
import { deleteFieldTool } from '../tools/delete-field.tool.js';
20-
import { listMetadataObjectsTool } from '../tools/list-metadata-objects.tool.js';
21-
import { describeMetadataObjectTool } from '../tools/describe-metadata-object.tool.js';
20+
import { listObjectsTool } from '../tools/list-objects.tool.js';
21+
import { describeObjectTool } from '../tools/describe-object.tool.js';
2222

2323
// ── Helpers ────────────────────────────────────────────────────────
2424

@@ -63,8 +63,8 @@ describe('Metadata Tool Definitions', () => {
6363
'add_field',
6464
'modify_field',
6565
'delete_field',
66-
'list_metadata_objects',
67-
'describe_metadata_object',
66+
'list_objects',
67+
'describe_object',
6868
]);
6969
});
7070

@@ -86,8 +86,8 @@ describe('Individual Tool Metadata (.tool.ts)', () => {
8686
{ tool: addFieldTool, expectedName: 'add_field', expectedLabel: 'Add Field' },
8787
{ tool: modifyFieldTool, expectedName: 'modify_field', expectedLabel: 'Modify Field' },
8888
{ tool: deleteFieldTool, expectedName: 'delete_field', expectedLabel: 'Delete Field' },
89-
{ tool: listMetadataObjectsTool, expectedName: 'list_metadata_objects', expectedLabel: 'List Metadata Objects' },
90-
{ tool: describeMetadataObjectTool, expectedName: 'describe_metadata_object', expectedLabel: 'Describe Metadata Object' },
89+
{ tool: listObjectsTool, expectedName: 'list_objects', expectedLabel: 'List Objects' },
90+
{ tool: describeObjectTool, expectedName: 'describe_object', expectedLabel: 'Describe Object' },
9191
];
9292

9393
for (const { tool, expectedName, expectedLabel } of tools) {
@@ -132,8 +132,8 @@ describe('Individual Tool Metadata (.tool.ts)', () => {
132132
});
133133

134134
it('should not mark read-only tools as requiresConfirmation', () => {
135-
expect(listMetadataObjectsTool.requiresConfirmation).toBe(false);
136-
expect(describeMetadataObjectTool.requiresConfirmation).toBe(false);
135+
expect(listObjectsTool.requiresConfirmation).toBe(false);
136+
expect(describeObjectTool.requiresConfirmation).toBe(false);
137137
});
138138

139139
it('should not mark add_field and modify_field as requiresConfirmation', () => {
@@ -162,17 +162,17 @@ describe('registerMetadataTools', () => {
162162
expect(registry.has('add_field')).toBe(true);
163163
expect(registry.has('modify_field')).toBe(true);
164164
expect(registry.has('delete_field')).toBe(true);
165-
expect(registry.has('list_metadata_objects')).toBe(true);
166-
expect(registry.has('describe_metadata_object')).toBe(true);
165+
expect(registry.has('list_objects')).toBe(true);
166+
expect(registry.has('describe_object')).toBe(true);
167167
});
168168
});
169169

170170
// ═══════════════════════════════════════════════════════════════════
171171
// Dual registration (data tools + metadata tools)
172172
// ═══════════════════════════════════════════════════════════════════
173173

174-
describe('registerDataTools + registerMetadataTools — no collision', () => {
175-
it('should register both tool sets on the same registry without overwriting', () => {
174+
describe('registerDataTools + registerMetadataTools — unified list/describe', () => {
175+
it('should register both tool sets on the same registry with shared list_objects and describe_object', () => {
176176
const registry = new ToolRegistry();
177177
const metadataService = createMockMetadataService();
178178
const dataEngine = {
@@ -181,26 +181,32 @@ describe('registerDataTools + registerMetadataTools — no collision', () => {
181181
aggregate: vi.fn(),
182182
} as any;
183183

184-
registerDataTools(registry, { dataEngine, metadataService });
184+
registerDataTools(registry, { dataEngine });
185185
const sizeAfterData = registry.size;
186186

187187
registerMetadataTools(registry, { metadataService });
188188
const sizeAfterBoth = registry.size;
189189

190-
// Data tools define: list_objects, describe_object, query_records, get_record, aggregate_data
191-
// Metadata tools define: create_object, add_field, modify_field, delete_field, list_metadata_objects, describe_metadata_object
192-
// No overlap — total should be sum of both
190+
// Data tools define: query_records, get_record, aggregate_data (3)
191+
// Metadata tools define: create_object, add_field, modify_field, delete_field, list_objects, describe_object (6)
192+
// Total should be 3 + 6 = 9
193+
expect(sizeAfterData).toBe(3);
193194
expect(sizeAfterBoth).toBe(sizeAfterData + 6);
194195

195-
// Data tools should still be present
196+
// Unified list/describe should be present (from metadata tools)
196197
expect(registry.has('list_objects')).toBe(true);
197198
expect(registry.has('describe_object')).toBe(true);
199+
200+
// Data-only tools should be present
198201
expect(registry.has('query_records')).toBe(true);
202+
expect(registry.has('get_record')).toBe(true);
203+
expect(registry.has('aggregate_data')).toBe(true);
199204

200-
// Metadata tools should also be present with distinct names
201-
expect(registry.has('list_metadata_objects')).toBe(true);
202-
expect(registry.has('describe_metadata_object')).toBe(true);
205+
// Metadata-only tools should be present
203206
expect(registry.has('create_object')).toBe(true);
207+
expect(registry.has('add_field')).toBe(true);
208+
expect(registry.has('modify_field')).toBe(true);
209+
expect(registry.has('delete_field')).toBe(true);
204210
});
205211
});
206212

@@ -752,7 +758,7 @@ describe('list_metadata_objects handler', () => {
752758
const result = await registry.execute({
753759
type: 'tool-call' as const,
754760
toolCallId: 'c1',
755-
toolName: 'list_metadata_objects',
761+
toolName: 'list_objects',
756762
input: {},
757763
});
758764

@@ -767,7 +773,7 @@ describe('list_metadata_objects handler', () => {
767773
const result = await registry.execute({
768774
type: 'tool-call' as const,
769775
toolCallId: 'c2',
770-
toolName: 'list_metadata_objects',
776+
toolName: 'list_objects',
771777
input: { filter: 'account' },
772778
});
773779

@@ -780,7 +786,7 @@ describe('list_metadata_objects handler', () => {
780786
const result = await registry.execute({
781787
type: 'tool-call' as const,
782788
toolCallId: 'c3',
783-
toolName: 'list_metadata_objects',
789+
toolName: 'list_objects',
784790
input: { includeFields: true },
785791
});
786792

@@ -797,7 +803,7 @@ describe('list_metadata_objects handler', () => {
797803
const result = await registry.execute({
798804
type: 'tool-call' as const,
799805
toolCallId: 'c4',
800-
toolName: 'list_metadata_objects',
806+
toolName: 'list_objects',
801807
input: {},
802808
});
803809

@@ -836,7 +842,7 @@ describe('describe_metadata_object handler', () => {
836842
const result = await registry.execute({
837843
type: 'tool-call' as const,
838844
toolCallId: 'c1',
839-
toolName: 'describe_metadata_object',
845+
toolName: 'describe_object',
840846
input: { objectName: 'account' },
841847
});
842848

@@ -858,7 +864,7 @@ describe('describe_metadata_object handler', () => {
858864
const result = await registry.execute({
859865
type: 'tool-call' as const,
860866
toolCallId: 'c2',
861-
toolName: 'describe_metadata_object',
867+
toolName: 'describe_object',
862868
input: { objectName: 'nonexistent' },
863869
});
864870

@@ -909,7 +915,7 @@ describe('Metadata Tools — full lifecycle', () => {
909915
const descResult = await registry.execute({
910916
type: 'tool-call' as const,
911917
toolCallId: 's4',
912-
toolName: 'describe_metadata_object',
918+
toolName: 'describe_object',
913919
input: { objectName: 'invoice' },
914920
});
915921
const desc = JSON.parse((descResult.output as any).value);
@@ -941,7 +947,7 @@ describe('Metadata Tools — full lifecycle', () => {
941947
const descResult2 = await registry.execute({
942948
type: 'tool-call' as const,
943949
toolCallId: 's7',
944-
toolName: 'describe_metadata_object',
950+
toolName: 'describe_object',
945951
input: { objectName: 'invoice' },
946952
});
947953
const desc2 = JSON.parse((descResult2.output as any).value);
@@ -954,7 +960,7 @@ describe('Metadata Tools — full lifecycle', () => {
954960
const listResult = await registry.execute({
955961
type: 'tool-call' as const,
956962
toolCallId: 's8',
957-
toolName: 'list_metadata_objects',
963+
toolName: 'list_objects',
958964
input: {},
959965
});
960966
const list = JSON.parse((listResult.output as any).value);

packages/services/service-ai/src/agents/metadata-assistant-agent.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ Capabilities:
3636
- Describe the full schema of a specific object
3737
3838
Guidelines:
39-
1. Before creating a new object, use list_metadata_objects to check if a similar one already exists.
40-
2. Before modifying or deleting fields, use describe_metadata_object to understand the current schema.
39+
1. Before creating a new object, use list_objects to check if a similar one already exists.
40+
2. Before modifying or deleting fields, use describe_object to understand the current schema.
4141
3. Always use snake_case for object names and field names (e.g. project_task, due_date).
4242
4. Suggest meaningful field types based on the user's description (e.g. "deadline" → date, "active" → boolean).
4343
5. When creating objects, propose a reasonable set of initial fields based on the entity type.
@@ -59,8 +59,8 @@ Guidelines:
5959
{ type: 'action', name: 'add_field', description: 'Add a field to an existing object' },
6060
{ type: 'action', name: 'modify_field', description: 'Modify an existing field definition' },
6161
{ type: 'action', name: 'delete_field', description: 'Delete a field from an object' },
62-
{ type: 'query', name: 'list_metadata_objects', description: 'List all metadata objects' },
63-
{ type: 'query', name: 'describe_metadata_object', description: 'Describe an object schema' },
62+
{ type: 'query', name: 'list_objects', description: 'List all data objects' },
63+
{ type: 'query', name: 'describe_object', description: 'Describe an object schema' },
6464
],
6565

6666
active: true,

packages/services/service-ai/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ export {
3939
addFieldTool,
4040
modifyFieldTool,
4141
deleteFieldTool,
42-
listMetadataObjectsTool,
43-
describeMetadataObjectTool,
42+
listObjectsTool,
43+
describeObjectTool,
4444
} from './tools/metadata-tools.js';
4545

4646
// Agent runtime

0 commit comments

Comments
 (0)