Skip to content

Commit 25a16b8

Browse files
committed
refactor: move agent tool bundles into skills metadata
1 parent 0a4aa56 commit 25a16b8

7 files changed

Lines changed: 212 additions & 91 deletions

File tree

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

Lines changed: 19 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { ToolRegistry } from '../tools/tool-registry.js';
1616
import { registerDataTools, DATA_TOOL_DEFINITIONS } from '../tools/data-tools.js';
1717
import type { DataToolContext } from '../tools/data-tools.js';
1818
import { AgentRuntime } from '../agent-runtime.js';
19+
import { SkillRegistry } from '../skill-registry.js';
1920
import type { AgentChatContext } from '../agent-runtime.js';
2021
import { buildAgentRoutes } from '../routes/agent-routes.js';
2122
import { DATA_CHAT_AGENT } from '../agents/data-chat-agent.js';
@@ -603,7 +604,8 @@ describe('AgentRuntime', () => {
603604

604605
beforeEach(() => {
605606
metadataService = createMockMetadataService();
606-
runtime = new AgentRuntime(metadataService);
607+
const skillRegistry = new SkillRegistry(metadataService);
608+
runtime = new AgentRuntime(metadataService, skillRegistry);
607609
});
608610

609611
describe('loadAgent', () => {
@@ -663,16 +665,17 @@ describe('AgentRuntime', () => {
663665
expect(options.maxTokens).toBe(4096);
664666
});
665667

666-
it('should resolve agent tool references against available tools', () => {
668+
it('should resolve skill tool references against available tools', async () => {
667669
const availableTools: AIToolDefinition[] = [
668670
{ name: 'list_objects', description: 'List objects', parameters: {} },
669671
{ name: 'query_records', description: 'Query records', parameters: {} },
670-
{ name: 'unrelated_tool', description: 'Not in agent', parameters: {} },
672+
{ name: 'unrelated_tool', description: 'Not in any skill', parameters: {} },
671673
];
672674

673-
const options = runtime.buildRequestOptions(DATA_CHAT_AGENT, availableTools);
675+
// DATA_CHAT_AGENT now references the data_explorer skill which carries the tools.
676+
const { DATA_EXPLORER_SKILL } = await import('../skills/data-explorer-skill.js');
677+
const options = runtime.buildRequestOptions(DATA_CHAT_AGENT, availableTools, [DATA_EXPLORER_SKILL]);
674678

675-
// Only tools declared in agent.tools that exist in available should be resolved
676679
const resolvedNames = options.tools?.map(t => t.name) ?? [];
677680
expect(resolvedNames).toContain('list_objects');
678681
expect(resolvedNames).toContain('query_records');
@@ -1036,14 +1039,9 @@ describe('DATA_CHAT_AGENT', () => {
10361039
expect(DATA_CHAT_AGENT.visibility).toBe('global');
10371040
});
10381041

1039-
it('should reference all 5 data tools', () => {
1040-
expect(DATA_CHAT_AGENT.tools).toHaveLength(5);
1041-
const toolNames = DATA_CHAT_AGENT.tools!.map(t => t.name);
1042-
expect(toolNames).toContain('list_objects');
1043-
expect(toolNames).toContain('describe_object');
1044-
expect(toolNames).toContain('query_records');
1045-
expect(toolNames).toContain('get_record');
1046-
expect(toolNames).toContain('aggregate_data');
1042+
it('should reference the data_explorer skill (capability bundle moved to skill metadata)', () => {
1043+
expect(DATA_CHAT_AGENT.tools ?? []).toHaveLength(0);
1044+
expect(DATA_CHAT_AGENT.skills).toEqual(['data_explorer']);
10471045
});
10481046

10491047
it('should have guardrails configured', () => {
@@ -1071,23 +1069,14 @@ describe('METADATA_ASSISTANT_AGENT', () => {
10711069
expect(METADATA_ASSISTANT_AGENT.visibility).toBe('global');
10721070
});
10731071

1074-
it('should reference all 6 metadata tools', () => {
1075-
expect(METADATA_ASSISTANT_AGENT.tools).toHaveLength(6);
1076-
const toolNames = METADATA_ASSISTANT_AGENT.tools!.map(t => t.name);
1077-
expect(toolNames).toContain('create_object');
1078-
expect(toolNames).toContain('add_field');
1079-
expect(toolNames).toContain('modify_field');
1080-
expect(toolNames).toContain('delete_field');
1081-
expect(toolNames).toContain('list_objects');
1082-
expect(toolNames).toContain('describe_object');
1072+
it('should reference the metadata_authoring skill (capability bundle moved to skill metadata)', () => {
1073+
expect(METADATA_ASSISTANT_AGENT.tools ?? []).toHaveLength(0);
1074+
expect(METADATA_ASSISTANT_AGENT.skills).toEqual(['metadata_authoring']);
10831075
});
10841076

1085-
it('should use action type for mutation tools and query type for read tools', () => {
1086-
const tools = METADATA_ASSISTANT_AGENT.tools!;
1087-
const actionTools = tools.filter(t => t.type === 'action');
1088-
const queryTools = tools.filter(t => t.type === 'query');
1089-
expect(actionTools).toHaveLength(4); // create, add, modify, delete
1090-
expect(queryTools).toHaveLength(2); // list, describe
1077+
it('should keep the schema-architect persona in instructions', () => {
1078+
const instructions = METADATA_ASSISTANT_AGENT.instructions;
1079+
expect(instructions).toMatch(/architect/i);
10911080
});
10921081

10931082
it('should have guardrails configured', () => {
@@ -1107,10 +1096,8 @@ describe('METADATA_ASSISTANT_AGENT', () => {
11071096
expect(METADATA_ASSISTANT_AGENT.planning!.allowReplan).toBe(true);
11081097
});
11091098

1110-
it('should have instructions mentioning metadata management capabilities', () => {
1099+
it('should have instructions mentioning the architect persona', () => {
11111100
const instructions = METADATA_ASSISTANT_AGENT.instructions;
1112-
expect(instructions).toContain('snake_case');
1113-
expect(instructions).toContain('list_objects');
1114-
expect(instructions).toContain('describe_object');
1101+
expect(instructions).toMatch(/architect|metadata/i);
11151102
});
11161103
});

packages/services/service-ai/src/agents/data-chat-agent.ts

Lines changed: 17 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,20 @@
33
import type { Agent } from '@objectstack/spec/ai';
44

55
/**
6-
* Built-in `data_chat` agent definition.
6+
* Built-in `data_chat` agent — a thin **persona** record.
77
*
8-
* This agent powers the Airtable-style data conversation Chatbot.
9-
* It is registered automatically by the AI service plugin when a
10-
* data engine is available.
8+
* Following the platform's metadata-driven philosophy, this agent no
9+
* longer hardcodes the tools it can call. The capability bundle lives
10+
* on the `data_explorer` *skill* (see `../skills/data-explorer-skill.ts`).
11+
* The agent record is now just:
12+
* - identity (name / label / role)
13+
* - persona (system prompt)
14+
* - model + safety config
15+
* - skills attached → `skills: ['data_explorer']`
16+
*
17+
* To grant data-exploration powers to a different agent, just add
18+
* `data_explorer` to its `skills[]`. To revoke globally, set the
19+
* skill's `active: false` in metadata.
1120
*
1221
* @example
1322
* ```
@@ -24,21 +33,7 @@ export const DATA_CHAT_AGENT: Agent = {
2433
role: 'Business Data Analyst',
2534
instructions: `You are a helpful data assistant that helps users explore and understand their business data through natural language.
2635
27-
Capabilities:
28-
- List available data objects (tables) and their schemas
29-
- Query records with filters, sorting, and pagination
30-
- Look up individual records by ID
31-
- Perform aggregations and statistical analysis (count, sum, avg, min, max)
32-
33-
Guidelines:
34-
1. Always use the describe_object tool first to understand a table's structure before querying it.
35-
2. Respect the user's current context — if they are viewing a specific object or record, use that as the default scope.
36-
3. When presenting data, format it in a clear and readable way using markdown tables or bullet lists.
37-
4. For large result sets, summarize the data and mention the total count.
38-
5. When performing aggregations, explain the results in plain language.
39-
6. If a query returns no results, suggest possible reasons and alternative queries.
40-
7. Never expose internal IDs unless the user explicitly asks for them.
41-
8. Always answer in the same language the user is using.`,
36+
Always answer in the same language the user is using. Detailed tool-usage guidance is supplied by the skills attached to this agent.`,
4237

4338
model: {
4439
provider: 'openai',
@@ -47,13 +42,8 @@ Guidelines:
4742
maxTokens: 4096,
4843
},
4944

50-
tools: [
51-
{ type: 'query', name: 'list_objects', description: 'List all available data objects' },
52-
{ type: 'query', name: 'describe_object', description: 'Get schema/fields of a data object' },
53-
{ type: 'query', name: 'query_records', description: 'Query records with filters and pagination' },
54-
{ type: 'query', name: 'get_record', description: 'Get a single record by ID' },
55-
{ type: 'query', name: 'aggregate_data', description: 'Aggregate/statistics on data' },
56-
],
45+
// Capability bundle lives on the skill; the agent only references it.
46+
skills: ['data_explorer'],
5747

5848
active: true,
5949
visibility: 'global',
@@ -77,3 +67,4 @@ Guidelines:
7767
},
7868
},
7969
};
70+

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

Lines changed: 12 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@
33
import type { Agent } from '@objectstack/spec/ai';
44

55
/**
6-
* Built-in `metadata_assistant` agent definition.
6+
* Built-in `metadata_assistant` agent — a thin **persona** record.
77
*
8-
* This agent powers AI-driven metadata management — users can create objects,
9-
* add/modify/delete fields, and inspect schema definitions through natural
10-
* language conversation.
8+
* Capability bundle is no longer hardcoded here; it lives on the
9+
* `metadata_authoring` *skill* (see
10+
* `../skills/metadata-authoring-skill.ts`). Studio's Universal
11+
* Assistant pins this agent via `?agent=metadata_assistant` because
12+
* Studio is a metadata-authoring host.
1113
*
12-
* It is registered automatically by the AI service plugin alongside the
13-
* `data_chat` agent when the metadata service is available.
14+
* To extend this agent (e.g. give it data-exploration too), just add
15+
* the skill name: `skills: ['metadata_authoring', 'data_explorer']`.
1416
*
1517
* @example
1618
* ```
@@ -27,25 +29,7 @@ export const METADATA_ASSISTANT_AGENT: Agent = {
2729
role: 'Schema Architect',
2830
instructions: `You are an expert metadata architect that helps users design and manage their data models through natural language.
2931
30-
Capabilities:
31-
- Create new data objects (tables) with fields
32-
- Add fields (columns) to existing objects
33-
- Modify field properties (label, type, required, default value)
34-
- Delete fields from objects
35-
- List all registered metadata objects and their schemas
36-
- Describe the full schema of a specific object
37-
38-
Guidelines:
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.
41-
3. Always use snake_case for object names and field names (e.g. project_task, due_date).
42-
4. Suggest meaningful field types based on the user's description (e.g. "deadline" → date, "active" → boolean).
43-
5. When creating objects, propose a reasonable set of initial fields based on the entity type.
44-
6. Explain what changes you are about to make before executing them.
45-
7. After making changes, confirm the result by describing the updated schema.
46-
8. For destructive operations (deleting fields), always warn the user about potential data loss.
47-
9. Always answer in the same language the user is using.
48-
10. If the user's request is ambiguous, ask clarifying questions before proceeding.`,
32+
Always answer in the same language the user is using. If the user's request is ambiguous, ask clarifying questions before proceeding. Detailed tool-usage guidance is supplied by the skills attached to this agent.`,
4933

5034
model: {
5135
provider: 'openai',
@@ -54,14 +38,8 @@ Guidelines:
5438
maxTokens: 4096,
5539
},
5640

57-
tools: [
58-
{ type: 'action', name: 'create_object', description: 'Create a new data object (table)' },
59-
{ type: 'action', name: 'add_field', description: 'Add a field to an existing object' },
60-
{ type: 'action', name: 'modify_field', description: 'Modify an existing field definition' },
61-
{ type: 'action', name: 'delete_field', description: 'Delete a field from an object' },
62-
{ type: 'query', name: 'list_objects', description: 'List all data objects' },
63-
{ type: 'query', name: 'describe_object', description: 'Describe an object schema' },
64-
],
41+
// Capability bundle lives on the skill; the agent only references it.
42+
skills: ['metadata_authoring'],
6543

6644
active: true,
6745
visibility: 'global',
@@ -85,3 +63,4 @@ Guidelines:
8563
},
8664
},
8765
};
66+

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { registerMetadataTools } from './tools/metadata-tools.js';
1515
import { AgentRuntime } from './agent-runtime.js';
1616
import { SkillRegistry } from './skill-registry.js';
1717
import { DATA_CHAT_AGENT, METADATA_ASSISTANT_AGENT } from './agents/index.js';
18+
import { DATA_EXPLORER_SKILL, METADATA_AUTHORING_SKILL } from './skills/index.js';
1819
import { VercelLLMAdapter } from './adapters/vercel-adapter.js';
1920
import { MemoryLLMAdapter } from './adapters/memory-adapter.js';
2021

@@ -311,6 +312,25 @@ export class AIServicePlugin implements Plugin {
311312
} catch (err) {
312313
ctx.logger.warn('[AI] Failed to register data_chat agent', err instanceof Error ? { error: err.message, stack: err.stack } : { error: String(err) });
313314
}
315+
316+
// Register the built-in data_explorer skill (capability bundle for data_chat)
317+
try {
318+
const skillExists =
319+
typeof metadataService.exists === 'function'
320+
? await withTimeout(metadataService.exists('skill', DATA_EXPLORER_SKILL.name))
321+
: false;
322+
323+
if (skillExists === null) {
324+
ctx.logger.warn('[AI] Metadata service timed out checking data_explorer skill, skipping');
325+
} else if (!skillExists) {
326+
await withTimeout(metadataService.register('skill', DATA_EXPLORER_SKILL.name, DATA_EXPLORER_SKILL));
327+
ctx.logger.info('[AI] data_explorer skill registered');
328+
} else {
329+
ctx.logger.debug('[AI] data_explorer skill already exists, skipping auto-registration');
330+
}
331+
} catch (err) {
332+
ctx.logger.warn('[AI] Failed to register data_explorer skill', err instanceof Error ? { error: err.message } : { error: String(err) });
333+
}
314334
}
315335
}
316336
} catch {
@@ -367,6 +387,25 @@ export class AIServicePlugin implements Plugin {
367387
} catch (err) {
368388
ctx.logger.warn('[AI] Failed to register metadata_assistant agent', err instanceof Error ? { error: err.message, stack: err.stack } : { error: String(err) });
369389
}
390+
391+
// Register the built-in metadata_authoring skill (capability bundle for metadata_assistant)
392+
try {
393+
const skillExists =
394+
typeof metadataService.exists === 'function'
395+
? await withTimeout(metadataService.exists('skill', METADATA_AUTHORING_SKILL.name))
396+
: false;
397+
398+
if (skillExists === null) {
399+
ctx.logger.warn('[AI] Metadata service timed out checking metadata_authoring skill, skipping');
400+
} else if (!skillExists) {
401+
await withTimeout(metadataService.register('skill', METADATA_AUTHORING_SKILL.name, METADATA_AUTHORING_SKILL));
402+
ctx.logger.info('[AI] metadata_authoring skill registered');
403+
} else {
404+
ctx.logger.debug('[AI] metadata_authoring skill already exists, skipping auto-registration');
405+
}
406+
} catch (err) {
407+
ctx.logger.warn('[AI] Failed to register metadata_authoring skill', err instanceof Error ? { error: err.message } : { error: String(err) });
408+
}
370409
} catch (err) {
371410
ctx.logger.debug('[AI] Failed to register metadata tools', err instanceof Error ? err : undefined);
372411
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import type { Skill } from '@objectstack/spec/ai';
4+
5+
/**
6+
* Built-in `data_explorer` skill — the read-only data-Q&A capability
7+
* bundle that the `data_chat` agent (and any other agent that wants
8+
* data-exploration powers) attaches to its `skills[]`.
9+
*
10+
* Following the platform's metadata-driven philosophy, the agent
11+
* itself no longer hardcodes which tools it can call; instead it
12+
* names this skill and the SkillRegistry resolves the tool list at
13+
* request time. Disabling this skill via the metadata service
14+
* disables data exploration for every agent that references it,
15+
* without code changes.
16+
*/
17+
export const DATA_EXPLORER_SKILL: Skill = {
18+
name: 'data_explorer',
19+
label: 'Data Explorer',
20+
description: 'Read-only Q&A over the user\'s business data — schema discovery, filtered queries, lookups, and aggregations.',
21+
instructions: `You can explore the user's business data through these tools.
22+
23+
Capabilities:
24+
- List available data objects (tables) and their schemas
25+
- Query records with filters, sorting, and pagination
26+
- Look up individual records by ID
27+
- Perform aggregations and statistical analysis (count, sum, avg, min, max)
28+
29+
Guidelines:
30+
1. Always use the describe_object tool first to understand a table's structure before querying it.
31+
2. Respect the user's current context — if they are viewing a specific object or record, use that as the default scope.
32+
3. When presenting data, format it in a clear and readable way using markdown tables or bullet lists.
33+
4. For large result sets, summarize the data and mention the total count.
34+
5. When performing aggregations, explain the results in plain language.
35+
6. If a query returns no results, suggest possible reasons and alternative queries.
36+
7. Never expose internal IDs unless the user explicitly asks for them.
37+
8. Always answer in the same language the user is using.`,
38+
tools: [
39+
'list_objects',
40+
'describe_object',
41+
'query_records',
42+
'get_record',
43+
'aggregate_data',
44+
],
45+
triggerPhrases: [
46+
'show me',
47+
'list',
48+
'how many',
49+
'count',
50+
'find records',
51+
'query',
52+
'aggregate',
53+
'sum',
54+
'average',
55+
],
56+
active: true,
57+
};
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
export { DATA_EXPLORER_SKILL } from './data-explorer-skill.js';
4+
export { METADATA_AUTHORING_SKILL } from './metadata-authoring-skill.js';

0 commit comments

Comments
 (0)