feat: add read_chord_progression tool for the Agent
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { ReadChordProgressionTool } from './ReadChordProgressionTool';
|
||||
import { KGProject } from '../../core/KGProject';
|
||||
import { KGMidiTrack } from '../../core/track/KGMidiTrack';
|
||||
import { KGMidiRegion } from '../../core/region/KGMidiRegion';
|
||||
import { KGChordRegion } from '../../core/region/KGChordRegion';
|
||||
import { findGlobalTrackByType } from '../../util/globalTrackUtil';
|
||||
import { GlobalTrackType } from '../../core/global-track';
|
||||
import { KGCore } from '../../core/KGCore';
|
||||
|
||||
const storeState = {
|
||||
activeRegionId: null as string | null,
|
||||
};
|
||||
|
||||
vi.mock('../../stores/projectStore', () => ({
|
||||
useProjectStore: {
|
||||
getState: () => storeState,
|
||||
},
|
||||
}));
|
||||
|
||||
function buildProjectWithRegionAndOptionalChords(chords: string[] = []): {
|
||||
project: KGProject;
|
||||
midiRegion: KGMidiRegion;
|
||||
} {
|
||||
const project = new KGProject('tool-test', 8, 0, 120, { numerator: 4, denominator: 4 }, 'C major');
|
||||
const midiTrack = new KGMidiTrack('Melody', 1);
|
||||
const midiRegion = new KGMidiRegion('midi-region-1', '1', 0, 'Melody Region', 0, 32);
|
||||
midiTrack.addRegion(midiRegion);
|
||||
project.setTracks([midiTrack]);
|
||||
|
||||
const chordTrack = findGlobalTrackByType(project, GlobalTrackType.Chord);
|
||||
expect(chordTrack).not.toBeNull();
|
||||
|
||||
chords.forEach((symbol, index) => {
|
||||
chordTrack!.addRegion(new KGChordRegion(`chord-${index}`, chordTrack!.getId(), chordTrack!.getTrackIndex(), symbol, index * 4, 4));
|
||||
});
|
||||
|
||||
return { project, midiRegion };
|
||||
}
|
||||
|
||||
describe('ReadChordProgressionTool', () => {
|
||||
beforeEach(() => {
|
||||
storeState.activeRegionId = null;
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('reads chord progression from the active MIDI region', async () => {
|
||||
const { project, midiRegion } = buildProjectWithRegionAndOptionalChords(['Am', 'F', 'Dm', 'E7', 'Am', 'C', 'Dm', 'E7']);
|
||||
storeState.activeRegionId = midiRegion.getId();
|
||||
|
||||
vi.spyOn(KGCore, 'instance').mockReturnValue({
|
||||
getCurrentProject: () => project,
|
||||
getSelectedItems: () => [],
|
||||
} as unknown as KGCore);
|
||||
|
||||
const tool = new ReadChordProgressionTool();
|
||||
const result = await tool.execute({});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.result).toContain('Chord-symbol representation:');
|
||||
expect(result.result).toContain('[Am]4 | [F]4 | [Dm]4 | [E7]4 | [Am]4 | [C]4 | [Dm]4 | [E7]4 |');
|
||||
expect(result.result).toContain('[A, C E]4 | [F, A, C]4 | [D F A]4 | [E ^G B d]4 | [A, C E]4 | [C E G]4 | [D F A]4 | [E ^G B d]4 |');
|
||||
});
|
||||
|
||||
it('falls back to the selected MIDI region when no active region exists', async () => {
|
||||
const { project, midiRegion } = buildProjectWithRegionAndOptionalChords(['Am']);
|
||||
|
||||
vi.spyOn(KGCore, 'instance').mockReturnValue({
|
||||
getCurrentProject: () => project,
|
||||
getSelectedItems: () => [midiRegion],
|
||||
} as unknown as KGCore);
|
||||
|
||||
const tool = new ReadChordProgressionTool();
|
||||
const result = await tool.execute({});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.result).toContain('[Am]4 |');
|
||||
});
|
||||
|
||||
it('returns guidance when no chord progression is defined for the region range', async () => {
|
||||
const { project, midiRegion } = buildProjectWithRegionAndOptionalChords();
|
||||
storeState.activeRegionId = midiRegion.getId();
|
||||
|
||||
vi.spyOn(KGCore, 'instance').mockReturnValue({
|
||||
getCurrentProject: () => project,
|
||||
getSelectedItems: () => [],
|
||||
} as unknown as KGCore);
|
||||
|
||||
const tool = new ReadChordProgressionTool();
|
||||
const result = await tool.execute({});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.result).toContain('No chord progression is defined for the selected MIDI region range.');
|
||||
expect(result.result).toContain('read_music');
|
||||
});
|
||||
|
||||
it('returns a clear error when no active or selected MIDI region exists', async () => {
|
||||
const { project } = buildProjectWithRegionAndOptionalChords(['Am']);
|
||||
|
||||
vi.spyOn(KGCore, 'instance').mockReturnValue({
|
||||
getCurrentProject: () => project,
|
||||
getSelectedItems: () => [],
|
||||
} as unknown as KGCore);
|
||||
|
||||
const tool = new ReadChordProgressionTool();
|
||||
const result = await tool.execute({});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.result).toContain('No active or selected MIDI region found');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
import { BaseTool } from './BaseTool';
|
||||
import type { ToolResult, ToolParameter } from './BaseTool';
|
||||
import { KGMidiRegion } from '../../core/region/KGMidiRegion';
|
||||
import { useProjectStore } from '../../stores/projectStore';
|
||||
import { KGCore } from '../../core/KGCore';
|
||||
import { convertBeatRangeChordProgressionToABCNotation } from '../../util/abcNotationUtil';
|
||||
|
||||
/**
|
||||
* Tool for reading user-defined chord progression content from the global chord track.
|
||||
*/
|
||||
export class ReadChordProgressionTool extends BaseTool {
|
||||
readonly name = 'read_chord_progression';
|
||||
readonly description = 'Read the user-defined chord progression for the currently active or selected MIDI region. The output has two representations of the same progression: first symbolic chord names such as Em7b5, then note-based ABC chord tokens. Chord progression data comes only from chord regions the user defined on the global chord track, so it may be empty. If no chord progression is defined for this range, read the notes directly with read_music.';
|
||||
|
||||
readonly parameters: Record<string, ToolParameter> = {};
|
||||
|
||||
async execute(_params: Record<string, unknown>): Promise<ToolResult> {
|
||||
try {
|
||||
const targetRegion = this.findTargetRegion();
|
||||
if (!targetRegion) {
|
||||
return this.createErrorResult(
|
||||
'No active or selected MIDI region found. Please open the piano roll with a region or select a MIDI region first.'
|
||||
);
|
||||
}
|
||||
|
||||
const project = this.getCurrentProject();
|
||||
const startBeat = targetRegion.getStartFromBeat();
|
||||
const endBeat = startBeat + targetRegion.getLength();
|
||||
const result = convertBeatRangeChordProgressionToABCNotation(project, startBeat, endBeat);
|
||||
|
||||
return this.createSuccessResult(result);
|
||||
} catch (error) {
|
||||
return this.createErrorResult(`Failed to read chord progression: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
private findTargetRegion(): KGMidiRegion | null {
|
||||
const project = this.getCurrentProject();
|
||||
const tracks = project.getTracks();
|
||||
const storeState = useProjectStore.getState();
|
||||
|
||||
if (storeState.activeRegionId) {
|
||||
for (const track of tracks) {
|
||||
const region = track.getRegions().find(candidate => candidate.getId() === storeState.activeRegionId);
|
||||
if (region instanceof KGMidiRegion) {
|
||||
return region;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const selectedItems = KGCore.instance().getSelectedItems();
|
||||
for (const item of selectedItems) {
|
||||
if (item instanceof KGMidiRegion) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -6,14 +6,16 @@ export type { ToolResult, ToolParameter, ToolDefinition, OpenAIToolDefinition, O
|
||||
import { AddNotesTool } from './AddNotesTool';
|
||||
import { RemoveNotesTool } from './RemoveNotesTool';
|
||||
import { ReadMusicTool } from './ReadMusicTool';
|
||||
import { ReadChordProgressionTool } from './ReadChordProgressionTool';
|
||||
|
||||
export { AddNotesTool, RemoveNotesTool, ReadMusicTool };
|
||||
export { AddNotesTool, RemoveNotesTool, ReadMusicTool, ReadChordProgressionTool };
|
||||
|
||||
// Tool registry for easy access
|
||||
export const AVAILABLE_TOOLS = {
|
||||
add_notes: AddNotesTool,
|
||||
remove_notes: RemoveNotesTool,
|
||||
read_music: ReadMusicTool,
|
||||
read_chord_progression: ReadChordProgressionTool,
|
||||
} as const;
|
||||
|
||||
export type ToolName = keyof typeof AVAILABLE_TOOLS;
|
||||
|
||||
Reference in New Issue
Block a user