From bb32fe5377f468d40b7e8cdb7d5b408fba756342 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Feb 2026 21:18:00 +0000 Subject: [PATCH 1/3] feat: implement search_dokument_fulltext, analyze_g0v_by_department, search_anforanden in motions.ts Co-authored-by: pethers <1726836+pethers@users.noreply.github.com> --- scripts/data-transformers/constants.ts | 14 +++ .../data-transformers/content-generators.ts | 17 ++++ scripts/data-transformers/types.ts | 2 + scripts/news-types/motions.ts | 97 ++++++++++++++++--- scripts/types/content.ts | 1 + tests/news-types/motions.test.ts | 68 ++++++++++++- 6 files changed, 183 insertions(+), 16 deletions(-) diff --git a/scripts/data-transformers/constants.ts b/scripts/data-transformers/constants.ts index fa46afbc..3caaa636 100644 --- a/scripts/data-transformers/constants.ts +++ b/scripts/data-transformers/constants.ts @@ -118,6 +118,7 @@ export const CONTENT_LABELS: Record = { generalMatters: 'General matters', responsesToProp: 'Responses to Government Propositions', independentMotions: 'Independent Motions', + govEngagement: 'Government Department Engagement', twitterLabel1: 'Reading time', twitterLabel2: 'Article type', jobTitle: 'Political Intelligence Analyst', @@ -196,6 +197,7 @@ export const CONTENT_LABELS: Record = { generalMatters: 'Övriga frågor', responsesToProp: 'Svar på propositioner', independentMotions: 'Övriga motioner', + govEngagement: 'Regeringsdepartementens engagemang', twitterLabel1: 'Lästid', twitterLabel2: 'Artikeltyp', jobTitle: 'Politisk underrättelseanalytiker', @@ -274,6 +276,7 @@ export const CONTENT_LABELS: Record = { generalMatters: 'Generelle spørgsmål', responsesToProp: 'Svar på regeringsforslag', independentMotions: 'Andre forslag', + govEngagement: 'Regeringsdepartementernes engagement', twitterLabel1: 'Læsetid', twitterLabel2: 'Artikeltype', jobTitle: 'Politisk efterretningsanalytiker', @@ -352,6 +355,7 @@ export const CONTENT_LABELS: Record = { generalMatters: 'Generelle spørsmål', responsesToProp: 'Svar på regjeringforslag', independentMotions: 'Andre forslag', + govEngagement: 'Regjeringsdepartementenes engasjement', twitterLabel1: 'Lesetid', twitterLabel2: 'Artikkeltype', jobTitle: 'Politisk etterretningsanalytiker', @@ -430,6 +434,7 @@ export const CONTENT_LABELS: Record = { generalMatters: 'Yleiset asiat', responsesToProp: 'Vastaukset hallituksen esityksiin', independentMotions: 'Muut aloitteet', + govEngagement: 'Ministeriöiden osallistuminen', twitterLabel1: 'Lukuaika', twitterLabel2: 'Artikkelityyppi', jobTitle: 'Poliittinen tiedusteluanalyytikko', @@ -508,6 +513,7 @@ export const CONTENT_LABELS: Record = { generalMatters: 'Allgemeine Angelegenheiten', responsesToProp: 'Antworten auf Regierungsvorlagen', independentMotions: 'Sonstige Anträge', + govEngagement: 'Engagement der Regierungsressorts', twitterLabel1: 'Lesezeit', twitterLabel2: 'Artikeltyp', jobTitle: 'Politischer Geheimdienstanalyst', @@ -586,6 +592,7 @@ export const CONTENT_LABELS: Record = { generalMatters: 'Questions générales', responsesToProp: 'Réponses aux propositions gouvernementales', independentMotions: 'Autres motions', + govEngagement: 'Engagement des ministères', twitterLabel1: 'Temps de lecture', twitterLabel2: "Type d'article", jobTitle: 'Analyste en renseignement politique', @@ -664,6 +671,7 @@ export const CONTENT_LABELS: Record = { generalMatters: 'Asuntos generales', responsesToProp: 'Respuestas a proposiciones del gobierno', independentMotions: 'Otras mociones', + govEngagement: 'Compromiso de los ministerios', twitterLabel1: 'Tiempo de lectura', twitterLabel2: 'Tipo de artículo', jobTitle: 'Analista de inteligencia política', @@ -742,6 +750,7 @@ export const CONTENT_LABELS: Record = { generalMatters: 'Algemene zaken', responsesToProp: 'Antwoorden op regeringsvoorstellen', independentMotions: 'Overige moties', + govEngagement: 'Betrokkenheid van ministeries', twitterLabel1: 'Leestijd', twitterLabel2: 'Artikeltype', jobTitle: 'Politiek inlichtingenanalist', @@ -820,6 +829,7 @@ export const CONTENT_LABELS: Record = { generalMatters: 'مسائل عامة', responsesToProp: 'ردود على مقترحات الحكومة', independentMotions: 'اقتراحات أخرى', + govEngagement: 'مشاركة الوزارات الحكومية', twitterLabel1: 'وقت القراءة', twitterLabel2: 'نوع المقال', jobTitle: 'محلل الاستخبارات السياسية', @@ -898,6 +908,7 @@ export const CONTENT_LABELS: Record = { generalMatters: 'עניינים כלליים', responsesToProp: 'תשובות להצעות הממשלה', independentMotions: 'הצעות אחרות', + govEngagement: 'מעורבות משרדי הממשלה', twitterLabel1: 'זמן קריאה', twitterLabel2: 'סוג כתבה', jobTitle: 'אנליסט מודיעין פוליטי', @@ -976,6 +987,7 @@ export const CONTENT_LABELS: Record = { generalMatters: '一般事項', responsesToProp: '政府提案への回答', independentMotions: 'その他の動議', + govEngagement: '政府省庁の関与', twitterLabel1: '読了時間', twitterLabel2: '記事タイプ', jobTitle: '政治インテリジェンスアナリスト', @@ -1054,6 +1066,7 @@ export const CONTENT_LABELS: Record = { generalMatters: '일반 사항', responsesToProp: '정부 제안에 대한 응답', independentMotions: '기타 동의', + govEngagement: '정부 부처 참여', twitterLabel1: '읽기 시간', twitterLabel2: '기사 유형', jobTitle: '정치 인텔리전스 분석가', @@ -1132,6 +1145,7 @@ export const CONTENT_LABELS: Record = { generalMatters: '一般事项', responsesToProp: '对政府提案的回应', independentMotions: '其他动议', + govEngagement: '政府部门参与', twitterLabel1: '阅读时间', twitterLabel2: '文章类型', jobTitle: '政治情报分析师', diff --git a/scripts/data-transformers/content-generators.ts b/scripts/data-transformers/content-generators.ts index 690926df..8c6ae888 100644 --- a/scripts/data-transformers/content-generators.ts +++ b/scripts/data-transformers/content-generators.ts @@ -774,6 +774,23 @@ export function generateMotionsContent(data: ArticleContentData, lang: Language content += ` \n \n`; } + // Government department engagement section (from analyze_g0v_by_department) + const govDeptData = data.govDeptData ?? []; + if (govDeptData.length > 0) { + content += `\n

${L(lang, 'govEngagement')}

\n`; + content += `
\n
    \n`; + govDeptData.slice(0, 5).forEach(dept => { + const deptName = escapeHtml(String(dept['name'] ?? dept['departement'] ?? dept['department'] ?? '')); + const deptCount = dept['count'] ?? dept['total'] ?? dept['document_count']; + if (deptName) { + content += deptCount + ? `
  • ${deptName} (${escapeHtml(String(deptCount))})
  • \n` + : `
  • ${deptName}
  • \n`; + } + }); + content += `
\n
\n`; + } + return content; } diff --git a/scripts/data-transformers/types.ts b/scripts/data-transformers/types.ts index 3193ec50..7190778f 100644 --- a/scripts/data-transformers/types.ts +++ b/scripts/data-transformers/types.ts @@ -91,4 +91,6 @@ export interface ArticleContentData { context?: string; /** CIA intelligence context for enriched analysis */ ciaContext?: CIAContext; + /** Government department analysis from analyze_g0v_by_department */ + govDeptData?: Record[]; } diff --git a/scripts/news-types/motions.ts b/scripts/news-types/motions.ts index 9f2c322e..dfcc41ff 100644 --- a/scripts/news-types/motions.ts +++ b/scripts/news-types/motions.ts @@ -54,10 +54,10 @@ * - Includes motion text, sponsorship, current status * - Enables systematic opposition coverage * - * TODO: Implement additional tools for comprehensive analysis: - * - search_dokument_fulltext: Full text analysis of motion content - * - analyze_g0v_by_department: Government department responses - * - search_anforanden: Parliamentary debate related to motion + * Additional tools (all implemented with graceful degradation): + * - search_dokument_fulltext: Full-text policy alternative analysis + * - analyze_g0v_by_department: Government department response tracking + * - search_anforanden: Debate context and party positioning * * **OPERATIONAL WORKFLOW:** * 1. Query MCP: Fetch recent motions (default: 10 most recent) @@ -186,16 +186,14 @@ import type { ArticleCategory, GeneratedArticle, GenerationResult, MCPCallRecord /** * Required MCP tools for motions articles * - * REQUIRED_TOOLS UPDATE (2026-02-14): - * Initially set to 4 tools ['get_motioner', 'search_dokument_fulltext', 'analyze_g0v_by_department', 'search_anforanden'] - * to match tests/validation expectations. However, this caused runtime validation failures - * since the implementation only calls get_motioner (line 56). - * - * Reverted to actual implementation (1 tool) to prevent validation failures. - * When additional tools are implemented in generateMotions(), add them back here. + * Restored to full 4-tool specification (2026-02-26): + * All four tools are now implemented with graceful degradation on failure. */ export const REQUIRED_TOOLS: readonly string[] = [ - 'get_motioner' + 'get_motioner', + 'search_dokument_fulltext', + 'analyze_g0v_by_department', + 'search_anforanden', ]; export interface TitleSet { @@ -251,6 +249,70 @@ export async function generateMotions(options: GenerationOptions = {}): Promise< console.log(' ℹ️ No new motions found, skipping'); return { success: true, files: 0, mcpCalls }; } + + // Tool 2: search_dokument_fulltext — full-text policy alternative analysis + try { + const topTitle = motions[0]?.titel || motions[0]?.title || ''; + if (topTitle) { + const ftResponse = await client.request('search_dokument_fulltext', { query: topTitle, limit: 3 }); + const ftDocs = (ftResponse['dokument'] ?? ftResponse['results'] ?? []) as RawDocument[]; + mcpCalls.push({ tool: 'search_dokument_fulltext', result: ftDocs }); + console.log(` 📄 Full text: ${ftDocs.length} results`); + // Attach full text to motions for "Policy Alternative" rendering in article + ftDocs.forEach((ftDoc, i) => { + const m = motions[i] as Record; + if (m && !m['fullText']) { + m['fullText'] = ftDoc.fullText || ftDoc.summary || ''; + } + }); + } + } catch (err) { + console.warn(' ⚠ search_dokument_fulltext unavailable:', (err as Error).message); + } + + // Tool 3: analyze_g0v_by_department — government department response tracking + let govDeptData: Record[] = []; + try { + const today0 = new Date(); + const thirtyDaysAgo = new Date(today0); + thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); + const govResp = await client.request('analyze_g0v_by_department', { + dateFrom: formatDateForSlug(thirtyDaysAgo), + dateTo: formatDateForSlug(today0), + }); + govDeptData = (govResp['departments'] ?? govResp['data'] ?? []) as Record[]; + mcpCalls.push({ tool: 'analyze_g0v_by_department', result: govDeptData }); + console.log(` 🏛 Gov dept analysis: ${govDeptData.length} departments`); + } catch (err) { + console.warn(' ⚠ analyze_g0v_by_department unavailable:', (err as Error).message); + } + + // Tool 4: search_anforanden — debate context and party positioning + try { + const debateQuery = motions[0]?.titel || motions[0]?.title || ''; + if (debateQuery) { + const speeches = await client.searchSpeeches({ text: debateQuery, rm: '2025/26', limit: 10 }) as Array>; + mcpCalls.push({ tool: 'search_anforanden', result: speeches }); + console.log(` 🗣 Debate speeches: ${speeches.length} found`); + // Attach speeches to the first motion without speeches for "Party Positioning" rendering + if (speeches.length > 0) { + for (const motion of motions) { + const m = motion as Record; + if (!m['speeches']) { + m['speeches'] = speeches.slice(0, 3).map((s: Record) => ({ + talare: s['talare'], + parti: s['parti'], + text: (s['anforande_text'] as string | undefined)?.slice(0, 300), + anforande_nummer: s['anforande_nummer'], + })); + break; + } + } + } + } + } catch (err) { + console.warn(' ⚠ search_anforanden unavailable:', (err as Error).message); + } const today = new Date(); const slug = `${formatDateForSlug(today)}-opposition-motions`; @@ -259,11 +321,16 @@ export async function generateMotions(options: GenerationOptions = {}): Promise< for (const lang of languages) { console.log(` 🌐 Generating ${lang.toUpperCase()} version...`); - const content: string = generateArticleContent({ motions }, 'motions', lang); + const content: string = generateArticleContent({ motions, govDeptData }, 'motions', lang); const watchPoints = extractWatchPoints({ motions }, lang); const metadata = generateMetadata({ motions }, 'motions', lang); const readTime: string = calculateReadTime(content); - const sources: string[] = generateSources(['get_motioner']); + const sources: string[] = generateSources([ + 'get_motioner', + 'search_dokument_fulltext', + 'analyze_g0v_by_department', + 'search_anforanden', + ]); const titles: TitleSet = getTitles(lang, motions.length, motions); @@ -304,7 +371,7 @@ export async function generateMotions(options: GenerationOptions = {}): Promise< mcpCalls, crossReferences: { event: `${motions.length} motions`, - sources: ['motioner'] + sources: ['motioner', 'fulltext', 'gov-dept', 'speeches'] } }; diff --git a/scripts/types/content.ts b/scripts/types/content.ts index 907d7b9b..7447e7e0 100644 --- a/scripts/types/content.ts +++ b/scripts/types/content.ts @@ -106,6 +106,7 @@ export interface ContentLabelSet { generalMatters: string; responsesToProp: string; independentMotions: string; + govEngagement: string; twitterLabel1: string; twitterLabel2: string; jobTitle: string; diff --git a/tests/news-types/motions.test.ts b/tests/news-types/motions.test.ts index 6986a7ec..a8a19bcd 100644 --- a/tests/news-types/motions.test.ts +++ b/tests/news-types/motions.test.ts @@ -18,6 +18,8 @@ interface MotionRecord { /** Mock MCP client shape */ interface MockMCPClientShape { fetchMotions: Mock<(limit: number) => Promise>; + request: Mock<(tool: string, params: Record) => Promise>>; + searchSpeeches: Mock<(params: Record) => Promise>; } /** Validation input */ @@ -53,7 +55,9 @@ const { mockClientInstance, mockMotions, MockMCPClient } = vi.hoisted(() => { ]; const mockClientInstance: MockMCPClientShape = { - fetchMotions: vi.fn().mockResolvedValue(mockMotions) as MockMCPClientShape['fetchMotions'] + fetchMotions: vi.fn().mockResolvedValue(mockMotions) as MockMCPClientShape['fetchMotions'], + request: vi.fn().mockResolvedValue({}) as MockMCPClientShape['request'], + searchSpeeches: vi.fn().mockResolvedValue([]) as MockMCPClientShape['searchSpeeches'], }; function MockMCPClient(): MockMCPClientShape { @@ -77,6 +81,8 @@ describe('Motions Article Generation', () => { beforeEach(() => { vi.clearAllMocks(); mockClientInstance.fetchMotions.mockResolvedValue(mockMotions); + mockClientInstance.request.mockResolvedValue({}); + mockClientInstance.searchSpeeches.mockResolvedValue([]); }); afterEach(() => { @@ -88,6 +94,14 @@ describe('Motions Article Generation', () => { expect(motionsModule.REQUIRED_TOOLS).toBeDefined(); expect(motionsModule.REQUIRED_TOOLS).toContain('get_motioner'); }); + + it('should include all four required tools', () => { + expect(motionsModule.REQUIRED_TOOLS).toContain('get_motioner'); + expect(motionsModule.REQUIRED_TOOLS).toContain('search_dokument_fulltext'); + expect(motionsModule.REQUIRED_TOOLS).toContain('analyze_g0v_by_department'); + expect(motionsModule.REQUIRED_TOOLS).toContain('search_anforanden'); + expect(motionsModule.REQUIRED_TOOLS).toHaveLength(4); + }); }); describe('Data Collection', () => { @@ -100,6 +114,38 @@ describe('Motions Article Generation', () => { expect(result.mcpCalls!.some((call: MCPCallRecord) => call.tool === 'get_motioner')).toBe(true); }); + it('should call search_dokument_fulltext when motions have titles', async () => { + await motionsModule.generateMotions({ languages: ['en'] }); + expect(mockClientInstance.request).toHaveBeenCalledWith( + 'search_dokument_fulltext', + expect.objectContaining({ query: expect.any(String), limit: 3 }) + ); + }); + + it('should call analyze_g0v_by_department', async () => { + await motionsModule.generateMotions({ languages: ['en'] }); + expect(mockClientInstance.request).toHaveBeenCalledWith( + 'analyze_g0v_by_department', + expect.objectContaining({ dateFrom: expect.any(String), dateTo: expect.any(String) }) + ); + }); + + it('should call search_anforanden for debate context', async () => { + await motionsModule.generateMotions({ languages: ['en'] }); + expect(mockClientInstance.searchSpeeches).toHaveBeenCalledWith( + expect.objectContaining({ text: expect.any(String), limit: 10 }) + ); + }); + + it('should record all tool calls in mcpCalls', async () => { + const result = await motionsModule.generateMotions({ languages: ['en'] }); + const toolNames = result.mcpCalls!.map((c: MCPCallRecord) => c.tool); + expect(toolNames).toContain('get_motioner'); + expect(toolNames).toContain('search_dokument_fulltext'); + expect(toolNames).toContain('analyze_g0v_by_department'); + expect(toolNames).toContain('search_anforanden'); + }); + it('should handle empty motions', async () => { mockClientInstance.fetchMotions.mockResolvedValue([]); @@ -110,6 +156,18 @@ describe('Motions Article Generation', () => { expect(result.success).toBe(true); expect(result.files).toBe(0); }); + + it('should degrade gracefully when search_dokument_fulltext fails', async () => { + mockClientInstance.request.mockRejectedValueOnce(new Error('Tool unavailable')); + const result = await motionsModule.generateMotions({ languages: ['en'] }); + expect(result.success).toBe(true); + }); + + it('should degrade gracefully when search_anforanden fails', async () => { + mockClientInstance.searchSpeeches.mockRejectedValueOnce(new Error('Tool unavailable')); + const result = await motionsModule.generateMotions({ languages: ['en'] }); + expect(result.success).toBe(true); + }); }); describe('Article Structure', () => { @@ -128,6 +186,14 @@ describe('Motions Article Generation', () => { expect(result.slug).toMatch(/opposition-motions$/); }); + + it('should include all four tool names in sources', async () => { + const result = await motionsModule.generateMotions({ languages: ['en'] }); + // Verify the crossReferences.sources reflect the enriched pipeline + expect(result.crossReferences?.sources).toContain('fulltext'); + expect(result.crossReferences?.sources).toContain('gov-dept'); + expect(result.crossReferences?.sources).toContain('speeches'); + }); }); describe('Validation', () => { From 1ef74922ebd741cd2a43f114fdb4da7145603421 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 06:43:10 +0000 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20add=20analyze=5Fg0v=5Fby=5Fdepartmen?= =?UTF-8?q?t=20degradation=20test=20and=20dynamic=20riksm=C3=B6te=20calcul?= =?UTF-8?q?ation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pethers <1726836+pethers@users.noreply.github.com> --- scripts/news-types/motions.ts | 14 +++++++++++++- tests/news-types/motions.test.ts | 9 +++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/scripts/news-types/motions.ts b/scripts/news-types/motions.ts index dfcc41ff..b3e7a467 100644 --- a/scripts/news-types/motions.ts +++ b/scripts/news-types/motions.ts @@ -227,6 +227,18 @@ export function formatDateForSlug(date: Date = new Date()): string { return date.toISOString().split('T')[0] ?? ''; } +/** + * Calculate the current Swedish riksmöte (parliamentary session) string. + * The session runs September–August: e.g. September 2025 → "2025/26". + */ +export function getCurrentRiksmote(date: Date = new Date()): string { + const year = date.getFullYear(); + const month = date.getMonth(); // 0-based; September = 8 + const startYear = month >= 8 ? year : year - 1; + const endYY = String(startYear + 1).slice(-2); + return `${startYear}/${endYY}`; +} + /** * Generate Opposition Motions article */ @@ -291,7 +303,7 @@ export async function generateMotions(options: GenerationOptions = {}): Promise< try { const debateQuery = motions[0]?.titel || motions[0]?.title || ''; if (debateQuery) { - const speeches = await client.searchSpeeches({ text: debateQuery, rm: '2025/26', limit: 10 }) as Array>; + const speeches = await client.searchSpeeches({ text: debateQuery, rm: getCurrentRiksmote(), limit: 10 }) as Array>; mcpCalls.push({ tool: 'search_anforanden', result: speeches }); console.log(` 🗣 Debate speeches: ${speeches.length} found`); // Attach speeches to the first motion without speeches for "Party Positioning" rendering diff --git a/tests/news-types/motions.test.ts b/tests/news-types/motions.test.ts index a8a19bcd..9675bdc6 100644 --- a/tests/news-types/motions.test.ts +++ b/tests/news-types/motions.test.ts @@ -163,6 +163,15 @@ describe('Motions Article Generation', () => { expect(result.success).toBe(true); }); + it('should degrade gracefully when analyze_g0v_by_department fails', async () => { + // First request call (search_dokument_fulltext) succeeds; second (analyze_g0v_by_department) fails + mockClientInstance.request + .mockResolvedValueOnce({}) + .mockRejectedValueOnce(new Error('Tool unavailable')); + const result = await motionsModule.generateMotions({ languages: ['en'] }); + expect(result.success).toBe(true); + }); + it('should degrade gracefully when search_anforanden fails', async () => { mockClientInstance.searchSpeeches.mockRejectedValueOnce(new Error('Tool unavailable')); const result = await motionsModule.generateMotions({ languages: ['en'] }); From ca4ea6ff6845bce8279d45273ffb5891e4f93285 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 07:30:01 +0000 Subject: [PATCH 3/3] fix: record failed MCP tool calls in mcpCalls, fix fullText index assignment, fix deptCount null check Co-authored-by: pethers <1726836+pethers@users.noreply.github.com> --- scripts/data-transformers/content-generators.ts | 3 ++- scripts/news-types/motions.ts | 17 +++++++++++------ tests/news-types/motions.test.ts | 6 ++++++ 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/scripts/data-transformers/content-generators.ts b/scripts/data-transformers/content-generators.ts index 8c6ae888..0f4c04d5 100644 --- a/scripts/data-transformers/content-generators.ts +++ b/scripts/data-transformers/content-generators.ts @@ -783,7 +783,8 @@ export function generateMotionsContent(data: ArticleContentData, lang: Language const deptName = escapeHtml(String(dept['name'] ?? dept['departement'] ?? dept['department'] ?? '')); const deptCount = dept['count'] ?? dept['total'] ?? dept['document_count']; if (deptName) { - content += deptCount + const hasDeptCount = deptCount !== null && deptCount !== undefined; + content += hasDeptCount ? `
  • ${deptName} (${escapeHtml(String(deptCount))})
  • \n` : `
  • ${deptName}
  • \n`; } diff --git a/scripts/news-types/motions.ts b/scripts/news-types/motions.ts index b3e7a467..f06a8956 100644 --- a/scripts/news-types/motions.ts +++ b/scripts/news-types/motions.ts @@ -270,16 +270,19 @@ export async function generateMotions(options: GenerationOptions = {}): Promise< const ftDocs = (ftResponse['dokument'] ?? ftResponse['results'] ?? []) as RawDocument[]; mcpCalls.push({ tool: 'search_dokument_fulltext', result: ftDocs }); console.log(` 📄 Full text: ${ftDocs.length} results`); - // Attach full text to motions for "Policy Alternative" rendering in article - ftDocs.forEach((ftDoc, i) => { - const m = motions[i] as Record; - if (m && !m['fullText']) { - m['fullText'] = ftDoc.fullText || ftDoc.summary || ''; + // Attach the best matching full text only to the primary motion (the one used for the query) + const primaryMotion = motions[0] as Record | undefined; + if (primaryMotion && ftDocs.length > 0 && !primaryMotion['fullText']) { + const bestDoc = ftDocs[0] as Record; + primaryMotion['fullText'] = (bestDoc['fullText'] as string) || (bestDoc['summary'] as string) || ''; + if (ftDocs.length > 1) { + primaryMotion['policyAlternativeDocs'] = ftDocs; } - }); + } } } catch (err) { console.warn(' ⚠ search_dokument_fulltext unavailable:', (err as Error).message); + mcpCalls.push({ tool: 'search_dokument_fulltext', result: [] }); } // Tool 3: analyze_g0v_by_department — government department response tracking @@ -297,6 +300,7 @@ export async function generateMotions(options: GenerationOptions = {}): Promise< console.log(` 🏛 Gov dept analysis: ${govDeptData.length} departments`); } catch (err) { console.warn(' ⚠ analyze_g0v_by_department unavailable:', (err as Error).message); + mcpCalls.push({ tool: 'analyze_g0v_by_department', result: [] }); } // Tool 4: search_anforanden — debate context and party positioning @@ -324,6 +328,7 @@ export async function generateMotions(options: GenerationOptions = {}): Promise< } } catch (err) { console.warn(' ⚠ search_anforanden unavailable:', (err as Error).message); + mcpCalls.push({ tool: 'search_anforanden', result: [] }); } const today = new Date(); diff --git a/tests/news-types/motions.test.ts b/tests/news-types/motions.test.ts index 9675bdc6..69787e1b 100644 --- a/tests/news-types/motions.test.ts +++ b/tests/news-types/motions.test.ts @@ -161,6 +161,8 @@ describe('Motions Article Generation', () => { mockClientInstance.request.mockRejectedValueOnce(new Error('Tool unavailable')); const result = await motionsModule.generateMotions({ languages: ['en'] }); expect(result.success).toBe(true); + const toolNames = result.mcpCalls?.map(c => c.tool) ?? []; + expect(toolNames).toContain('search_dokument_fulltext'); }); it('should degrade gracefully when analyze_g0v_by_department fails', async () => { @@ -170,12 +172,16 @@ describe('Motions Article Generation', () => { .mockRejectedValueOnce(new Error('Tool unavailable')); const result = await motionsModule.generateMotions({ languages: ['en'] }); expect(result.success).toBe(true); + const toolNames = result.mcpCalls?.map(c => c.tool) ?? []; + expect(toolNames).toContain('analyze_g0v_by_department'); }); it('should degrade gracefully when search_anforanden fails', async () => { mockClientInstance.searchSpeeches.mockRejectedValueOnce(new Error('Tool unavailable')); const result = await motionsModule.generateMotions({ languages: ['en'] }); expect(result.success).toBe(true); + const toolNames = result.mcpCalls?.map(c => c.tool) ?? []; + expect(toolNames).toContain('search_anforanden'); }); });