Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions scripts/mcp-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export {
searchSpeeches,
fetchMPs,
fetchVotingRecords,
fetchVotingGroup,
fetchGovernmentDocuments,
fetchDocumentDetails,
enrichDocumentsWithContent,
Expand Down
11 changes: 11 additions & 0 deletions scripts/mcp-client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type {
SearchSpeechesParams,
FetchMPsFilters,
FetchVotingFilters,
FetchVotingGroupFilters,
GovDocSearchParams,
RiksdagDocument,
} from '../types/mcp.js';
Expand Down Expand Up @@ -467,6 +468,16 @@ export class MCPClient {
return (response['votes'] ?? []) as unknown[];
}

async fetchVotingGroup(params: FetchVotingGroupFilters = {}): Promise<unknown[]> {
const response = await this.request(
'get_voting_group',
params as unknown as Record<string, unknown>,
);
// MCP server returns 'groups' when groupBy is provided (grouped results),
// or 'votes' when no grouping is applied (flat voting list fallback)
return (response['groups'] ?? response['votes'] ?? []) as unknown[];
}

async fetchGovernmentDocuments(searchParams: GovDocSearchParams): Promise<unknown[]> {
const response = await this.request(
'search_regering',
Expand Down
6 changes: 6 additions & 0 deletions scripts/mcp-client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ export async function fetchVotingRecords(
return getDefaultClient().fetchVotingRecords(...args);
}

export async function fetchVotingGroup(
...args: Parameters<MCPClient['fetchVotingGroup']>
): ReturnType<MCPClient['fetchVotingGroup']> {
return getDefaultClient().fetchVotingGroup(...args);
}

export async function fetchGovernmentDocuments(
...args: Parameters<MCPClient['fetchGovernmentDocuments']>
): ReturnType<MCPClient['fetchGovernmentDocuments']> {
Expand Down
69 changes: 48 additions & 21 deletions scripts/news-types/breaking-news.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@
* - search_ledamoter: Member information for quote attribution
*
* **IMPLEMENTATION STATUS:**
* - Actual implementation calls: search_voteringar, search_anforanden
* - TODO implementation: get_voting_group, search_ledamoter
* Note: This causes validation warnings but allows tests to pass. Full
* implementation should complete all four tool integrations.
* - All four MCP tools implemented: search_voteringar, get_voting_group,
* search_anforanden, search_ledamoter (all called unconditionally;
* parameters are enriched when voteId is present for voting tools, and
* when a speaker name is found in speech results for search_ledamoter)
*
* **INTELLIGENCE ANALYSIS FRAMEWORK:**
* Breaking news articles incorporate:
Expand Down Expand Up @@ -168,15 +168,11 @@ import type {
/**
* Required MCP tools for breaking news articles
*
* CURRENT IMPLEMENTATION STATUS:
* - search_voteringar: ✅ Implemented (conditional, line 72)
* - search_anforanden: ✅ Implemented (conditional, line 79)
* - get_voting_group: ❌ TODO - Not yet implemented
* - search_ledamoter: ❌ TODO - Not yet implemented
*
* NOTE: REQUIRED_TOOLS lists the full specification for validation.
* Current implementation calls a subset. This causes validation warnings
* but allows tests to pass. Full implementation should add the missing tools.
* IMPLEMENTATION STATUS:
* - search_voteringar: ✅ Implemented (always called; parameters enriched when voteId is present)
* - get_voting_group: ✅ Implemented (always called; parameters enriched when voteId is present)
* - search_anforanden: ✅ Implemented (always called; parameters enriched when topic is present)
* - search_ledamoter: ✅ Implemented (always called; parameters enriched when a speaker name is found in search_anforanden results)
*/
export const REQUIRED_TOOLS: readonly string[] = [
'search_voteringar',
Expand Down Expand Up @@ -233,18 +229,49 @@ export async function generateBreakingNews(options: BreakingNewsOptions = {}): P
};
}

// Example: Fetch related votes if event involves a vote
if (eventData.voteId) {
// Tool 1: search_voteringar — vote records (enriched with voteId when available)
try {
console.log(' 🔄 Fetching voting details...');
const votes: unknown = await client.fetchVotingRecords({ punkt: eventData.voteId });
const votes: unknown = await client.fetchVotingRecords(eventData.voteId ? { punkt: eventData.voteId } : { limit: 5 });
mcpCalls.push({ tool: 'search_voteringar', result: votes });
} catch (err) {
console.warn(' ⚠ search_voteringar unavailable:', (err as Error).message);
mcpCalls.push({ tool: 'search_voteringar', result: [] });
}

// Example: Fetch related speeches
if (eventData.topic) {

// Tool 2: get_voting_group — party-level voting breakdown
try {
console.log(' 🔄 Fetching party voting breakdown...');
const votingGroup: unknown = await client.fetchVotingGroup(
eventData.voteId ? { punkt: eventData.voteId, groupBy: 'parti' } : { groupBy: 'parti', limit: 5 }
);
mcpCalls.push({ tool: 'get_voting_group', result: votingGroup });
} catch (err) {
console.warn(' ⚠ get_voting_group unavailable:', (err as Error).message);
mcpCalls.push({ tool: 'get_voting_group', result: [] });
}

// Tool 3: search_anforanden — speeches providing context and reactions
let speechList: Array<Record<string, unknown>> = [];
try {
console.log(' 🔄 Fetching related speeches...');
const speeches: unknown = await client.searchSpeeches({ sok: eventData.topic });
mcpCalls.push({ tool: 'search_anforanden', result: speeches });
const speeches = await client.searchSpeeches(eventData.topic ? { text: eventData.topic, limit: 10 } : { limit: 5 });
speechList = Array.isArray(speeches) ? speeches as Array<Record<string, unknown>> : [];
mcpCalls.push({ tool: 'search_anforanden', result: speechList });
} catch (err) {
console.warn(' ⚠ search_anforanden unavailable:', (err as Error).message);
mcpCalls.push({ tool: 'search_anforanden', result: [] });
}

// Tool 4: search_ledamoter — MP profiles for speaker context
try {
console.log(' 🔄 Fetching MP profiles for speaker context...');
const speakerName = speechList[0]?.['talare'] as string | undefined;
const mps: unknown = await client.fetchMPs(speakerName ? { namn: speakerName, limit: 1 } : { limit: 5 });
mcpCalls.push({ tool: 'search_ledamoter', result: mps });
} catch (err) {
console.warn(' ⚠ search_ledamoter unavailable:', (err as Error).message);
mcpCalls.push({ tool: 'search_ledamoter', result: [] });
}

const today = new Date();
Expand Down
10 changes: 10 additions & 0 deletions scripts/types/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,16 @@ export interface FetchVotingFilters {
[key: string]: unknown;
}

/** Filters for party-level voting group lookup */
export interface FetchVotingGroupFilters {
bet?: string;
punkt?: string;
groupBy?: 'parti' | 'valkrets' | 'namn';
rm?: string;
limit?: number;
[key: string]: unknown;
}

/** Parameters for government document search */
export interface GovDocSearchParams {
title?: string;
Expand Down
65 changes: 64 additions & 1 deletion tests/news-types/breaking-news.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import type { Language } from '../../scripts/types/language.js';
interface MockMCPClientShape {
fetchVotingRecords: Mock<(...args: unknown[]) => Promise<unknown[]>>;
searchSpeeches: Mock<(...args: unknown[]) => Promise<unknown[]>>;
fetchVotingGroup: Mock<(...args: unknown[]) => Promise<unknown[]>>;
fetchMPs: Mock<(...args: unknown[]) => Promise<unknown[]>>;
}

/** Validation input */
Expand Down Expand Up @@ -44,7 +46,9 @@ interface BreakingNewsModule {
const { mockClientInstance, MockMCPClient } = vi.hoisted(() => {
const mockClientInstance: MockMCPClientShape = {
fetchVotingRecords: vi.fn().mockResolvedValue([]) as MockMCPClientShape['fetchVotingRecords'],
searchSpeeches: vi.fn().mockResolvedValue([]) as MockMCPClientShape['searchSpeeches']
searchSpeeches: vi.fn().mockResolvedValue([]) as MockMCPClientShape['searchSpeeches'],
fetchVotingGroup: vi.fn().mockResolvedValue([]) as MockMCPClientShape['fetchVotingGroup'],
fetchMPs: vi.fn().mockResolvedValue([]) as MockMCPClientShape['fetchMPs']
};

function MockMCPClient(): MockMCPClientShape {
Expand Down Expand Up @@ -134,6 +138,65 @@ describe('Breaking News Article Generation', () => {

expect(mockClientInstance.searchSpeeches).toHaveBeenCalled();
});

it('should always fetch voting group (enriched with voteId when provided)', async () => {
const eventData: BreakingEventData = {
voteId: 'v123',
slug: 'test'
};

await breakingNewsModule.generateBreakingNews({
languages: ['en'],
eventData
});

expect(mockClientInstance.fetchVotingGroup).toHaveBeenCalledWith(
expect.objectContaining({
punkt: eventData.voteId,
groupBy: 'parti'
})
);
});

it('should always fetch MP profiles (enriched with speaker name from speech results)', async () => {
const eventData: BreakingEventData = {
topic: 'Budget debate',
slug: 'test'
};

const mockSpeakerName = 'Jane Doe';
mockClientInstance.searchSpeeches.mockResolvedValueOnce([
{ talare: mockSpeakerName } as unknown as Record<string, unknown>
]);

await breakingNewsModule.generateBreakingNews({
languages: ['en'],
eventData
});

expect(mockClientInstance.fetchMPs).toHaveBeenCalledWith(
expect.objectContaining({
namn: mockSpeakerName,
limit: 1
})
);
});

it('should call all 4 required tools even with minimal event data', async () => {
const eventData: BreakingEventData = {
slug: 'test'
};

await breakingNewsModule.generateBreakingNews({
languages: ['en'],
eventData
});

expect(mockClientInstance.fetchVotingRecords).toHaveBeenCalled();
expect(mockClientInstance.fetchVotingGroup).toHaveBeenCalled();
expect(mockClientInstance.searchSpeeches).toHaveBeenCalled();
expect(mockClientInstance.fetchMPs).toHaveBeenCalled();
});
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test suite lacks coverage for the unconditional tool calling pattern. All existing tests provide either voteId or topic, but none verify that all 4 required tools are still called when only a minimal eventData (e.g., just slug) is provided.

Consider adding a test case like:

it('should call all 4 required tools even with minimal event data', async () => {
  const eventData: BreakingEventData = { slug: 'test' }; // No voteId, no topic
  
  await breakingNewsModule.generateBreakingNews({
    languages: ['en'],
    eventData
  });
  
  expect(mockClientInstance.fetchVotingRecords).toHaveBeenCalled();
  expect(mockClientInstance.fetchVotingGroup).toHaveBeenCalled();
  expect(mockClientInstance.searchSpeeches).toHaveBeenCalled();
  expect(mockClientInstance.fetchMPs).toHaveBeenCalled();
});

This would explicitly verify the unconditional calling behavior required for cross-reference validation compliance.

Suggested change
});
});
it('should call all tools even with minimal event data', async () => {
const eventData: BreakingEventData = {
slug: 'test'
};
await breakingNewsModule.generateBreakingNews({
languages: ['en'],
eventData
});
expect(mockClientInstance.fetchVotingRecords).toHaveBeenCalled();
expect(mockClientInstance.fetchVotingGroup).toHaveBeenCalled();
expect(mockClientInstance.searchSpeeches).toHaveBeenCalled();
expect(mockClientInstance.fetchMPs).toHaveBeenCalled();
});

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in fb8366d: should call all 4 required tools even with minimal event data passes a { slug: 'test' } event (no voteId, no topic) and asserts that all four mock methods — fetchVotingRecords, fetchVotingGroup, searchSpeeches, fetchMPs — were called.

});

describe('Article Structure', () => {
Expand Down
Loading